# Commerce

> Price formatting and cart math for React shops and marketplaces: prices, discounts, unit prices, coupons and cart totals with VAT.

- Type: React helper (Hooks & Utilities)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add commerce`
- Docs: https://wingo-ui.com/components/commerce

## Interaction

Change the quantities in the cart with the minus and plus buttons: the subtotal, the savings from sale prices, the coupon, the free-delivery bar, the VAT line and the total all come from one computeCartTotals call and always add up. Pick a coupon to see it apply or explain why it does not (a minimum order shows what is still missing). The strip above shows the same prices through formatPrice, a range, a discount and a price per m² in the chosen locale and currency.

## Installation

```bash
npx wingo-ui@latest add commerce
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/commerce` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `lib/commerce.ts`

Wingo UI dependencies (installed automatically):

- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))

## Usage

```tsx
import { computeCartTotals, formatPrice, RO_VAT_RATE } from "@/lib/commerce"

const totals = computeCartTotals(
  [
    { id: "sofa", price: 850, quantity: 1 },
    { id: "pillows", price: 49.9, quantity: 2, compareAtPrice: 69.9 },
  ],
  { shipping: 19.99, freeShippingThreshold: 1000, taxRate: RO_VAT_RATE, coupons: [{ code: "FALL15", type: "percent", value: 15 }] },
)

formatPrice(totals.total, { locale: "en-US", currency: "USD" }) // "$827.32"
formatPrice(1200, { locale: "ro-RO", currency: "RON" }) // "1.200 lei"
```

## API

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `locale?` | `string` | `"en-US"` | The demo's locale. Every formatter takes options.locale; pass the one from useLocale so the server and the browser agree. |
| `currency?` | `string` | `"USD"` | options.currency: an ISO 4217 code (the lib's own default is "USD"). Printed with the narrow symbol ("$", "€", "lei"), never Intl's "US$" or "RON". |
| `fractionDigits?` | `"auto" \| number` | `"auto"` | options.fractionDigits: auto prints whole amounts without decimals ("$1,200") and the currency's digits otherwise ("$1,284.90"); a number forces that many. |
| `coupon?` | `string` | `"none"` | The demo's coupon: a percent (FALL15, capped at 150), a fixed amount (SAVE50), free shipping (FREESHIP) or one with a minimum order (MIN2000). |
| `freeShippingThreshold?` | `number \| null` | `1000` | computeCartTotals options.freeShippingThreshold: spend this much on items (after coupons) and delivery is free. 0 or null turns the offer off. |
| `taxRate?` | `number` | `21` | options.taxRate as a fraction (0.21, exported as RO_VAT_RATE; RO_REDUCED_VAT_RATE is 0.11); the demo shows it in percent. Lines can override it with their own taxRate (the book below is at 11%). |
| `pricesIncludeTax?` | `boolean` | `true` | options.pricesIncludeTax: true (consumer retail) reports the VAT inside the total; false adds it on top (B2B price lists). |
| `formatPrice?` | `(amount: number \| null \| undefined, options?: FormatPriceOptions) => string` |  | The main helper, a price as text: "$1,200", "$1,284.90", "1.284,90 lei" (ro-RO, RON), "489.000 €" (de-DE, EUR); "" for null and NaN. Options: currency (default USD), locale, fractionDigits, currencyDisplay ("narrowSymbol" \| "symbol" \| "code" \| "name" \| "none"), notation ("compact" gives "$489K"), signDisplay. Thin and narrow spaces become one no-break space so the server and the browser print the same text. |

## Parts

### formatPriceParts (function)

The pieces of a price for styling, in the locale's order: "$" before "1,284.90" in en-US, "1.284,90" then "lei" after a space in ro-RO. Returns { formatted, amount, sign, currency, currencyPosition, spaced } or null.

### formatPriceRange (function)

Two prices as one range joined by a spaced en dash, with the currency written once when it follows the number (ro-RO) and on both ends otherwise (en-US). A missing end prints the other alone.

### discountPercent (function)

The whole-percent saving of a sale price against its old price: discountPercent(1199, 1599) is 25. 0 without a higher old price.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `price` | `number` |  | The sale price. |
| `compareAtPrice` | `number \| null \| undefined` |  | The old price. |
| `returns?` | `number` |  | A whole percent, e.g. 25. |

### formatDiscount (function)

A discount badge in the locale: formatDiscount(25, "en-US") is "-25%" ("-25 %" in ro-RO); "" for 0.

### unitPrice (function)

The price per unit, rounded to cents: unitPrice(489000, 54) is 9055.56. null when the quantity is not positive.

### formatUnitPrice (function)

A unit price as text: $489,000 for 54 m² reads "$9,056/m²". Whole units from 100 up, cents below ("$4.90/kg").

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `amount` | `number` |  | The total price. |
| `quantity` | `number` |  | How many units it covers. |
| `unit` | `string` |  | The unit after the slash: m², kg, sq ft. |
| `options?` | `FormatPriceOptions` |  | The same options as formatPrice. |

### roundMoney (function)

Rounds half away from zero without float noise: roundMoney(1.005) is 1.01. Every amount the lib returns goes through it.

### couponDiscount (function)

What one coupon takes off, or why it does not apply: { valid: true, amount }, { valid: false, reason: "min-subtotal", missing } or { valid: false, reason: "not-applicable" }. Checking that a code exists and has not expired is the server's job; this is the math.

### computeCartTotals (function)

The summary of a cart or checkout: item count, subtotal, savings from sale prices, coupon discounts and rejected coupons, shipping with the free-shipping progress, VAT and total. Every amount is rounded to cents, so the lines always add up to the total.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `lines` | `CartLineInput[]` |  | { id, price, quantity, compareAtPrice?, taxRate? } per line. |
| `options.coupons?` | `Coupon[]` | `"[]"` | Applied in order, each on what the ones before it left. |
| `options.shipping?` | `number \| null` | `"null"` | The delivery price; null means not chosen yet. |
| `options.freeShippingThreshold?` | `number \| null` | `"null"` | Spend this much on items (after coupons) and delivery is free. |
| `options.taxRate?` | `number` | `0` | The VAT rate as a fraction (0.21); 0 leaves out the VAT line. |
| `options.pricesIncludeTax?` | `boolean` | `true` | true reports the VAT inside the total; false adds it on top. |
| `returns?` | `CartTotals` |  | itemCount, subtotal, savings, discount, discounts, rejectedCoupons, shipping, freeShippingRemaining, freeShippingProgress, tax, total and more. |

### DEFAULT_CURRENCY (constant)

The currency every formatter uses when options.currency is left out: "USD".

### RO_VAT_RATE (constant)

Romania's standard VAT rate as a fraction: 0.21, in force since Aug 1, 2025.

### RO_REDUCED_VAT_RATE (constant)

Romania's reduced VAT rate for food, medicines and books: 0.11.

### Coupon (type)

{ code, type: "percent" | "fixed" | "shipping", value, minSubtotal?, maxDiscount?, label? }.

### FormatPriceOptions (type)

currency, locale, fractionDigits, currencyDisplay, notation and signDisplay, shared by every formatter.

### CartTotals (type)

What computeCartTotals returns, every amount in major units (dollars, not cents).

## Examples

- **Product card prices**: A sale price, the old price struck through and the discount badge.
- **Prices by locale**: The same amounts through formatPrice in four locales and in compact notation.
- **Coupon checks**: couponDiscount explains a minimum order instead of failing silently.
- **Unit prices**: formatUnitPrice for real estate and groceries.

## See also

- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Currency Input](https://wingo-ui.com/components/currency-input) ([Markdown](https://wingo-ui.com/components/currency-input.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))

---

Canonical: https://wingo-ui.com/components/commerce
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
