Skip to main content

Money & Currency

In Vendure, monetary values are stored as integers using the minor unit of the selected currency. For example, if the currency is set to USD, then the integer value 100 would represent $1.00. This is a common practice in financial applications, as it avoids the rounding errors that can occur when using floating-point numbers.

For example, here's the response from a query for a product's variant prices:

{
"data": {
"product": {
"id": "42",
"variants": [
{
"id": "74",
"name": "Bonsai Tree",
"currencyCode": "USD",
"price": 1999,
"priceWithTax": 2399,
}
]
}
}
}

In this example, the tax-inclusive price of the variant is $23.99.

info

To illustrate the problem with storing money as decimals, imagine that we want to add the price of two items:

  • Product A: $1.21
  • Product B: $1.22

We should expect the sum of these two amounts to equal $2.43. However, if we perform this addition in JavaScript (and the same holds true for most common programming languages), we will instead get $2.4299999999999997!

For a more in-depth explanation of this issue, see this StackOverflow answer

Displaying monetary values

When you are building your storefront, or any other client that needs to display monetary values in a human-readable form, you need to divide by 100 to convert to the major currency unit and then format with the correct decimal & grouping dividers.

In JavaScript environments such as browsers & Node.js, we can take advantage of the excellent Intl.NumberFormat API.

Here's a function you can use in your projects:

src/utils/format-currency.ts
export function formatCurrency(value: number, currencyCode: string, locale?: string) {
const majorUnits = value / 100;
try {
// Note: if no `locale` is provided, the browser's default
// locale will be used.
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currencyCode,
}).format(majorUnits);
} catch (e: any) {
// A fallback in case the NumberFormat fails for any reason
return majorUnits.toFixed(2);
}
}

If you are building an Admin UI extension, you can use the built-in LocaleCurrencyPipe:

src/plugins/my-plugin/ui/components/my-component/my.component.html
<div>
Variant price: {{ variant.price | localeCurrency : variant.currencyCode }}
</div>

Support for multiple currencies

Vendure supports multiple currencies out-of-the-box. The available currencies must first be set at the Channel level (see the Channels, Currencies & Prices section), and then a price may be set on a ProductVariant in each of the available currencies.

When using multiple currencies, the ProductVariantPriceSelectionStrategy is used to determine which of the available prices to return when fetching the details of a ProductVariant. The default strategy is to return the price in the currency of the current session request context, which is determined firstly by any ?currencyCode=XXX query parameter on the request, and secondly by the defaultCurrencyCode of the Channel.

The GraphQL Money scalar

In the GraphQL APIs, we use a custom Money scalar type to represent all monetary values. We do this for two reasons:

  1. The built-in Int type is that the GraphQL spec imposes an upper limit of 2147483647, which in some cases (especially currencies with very large amounts) is not enough.
  2. Very advanced use-cases might demand more precision than is possible with an integer type. Using our own custom scalar gives us the possibility of supporting more precision.

Here's how the Money scalar is used in the ShippingLine type:

type ShippingLine {
id: ID!
shippingMethod: ShippingMethod!
price: Money!
priceWithTax: Money!
discountedPrice: Money!
discountedPriceWithTax: Money!
discounts: [Discount!]!
}

If you are defining custom GraphQL types, or adding fields to existing types (see the Extending the GraphQL API doc), then you should also use the Money scalar for any monetary values.

The @Money() decorator

When defining new database entities, if you need to store a monetary value, then rather than using the TypeORM @Column() decorator, you should use Vendure's @Money() decorator.

Using this decorator allows Vendure to correctly store the value in the database according to the configured MoneyStrategy (see below).

src/plugins/quote/entities/quote.entity.ts
import { DeepPartial } from '@vendure/common/lib/shared-types';
import { VendureEntity, Order, EntityId, Money, CurrencyCode, ID } from '@vendure/core';
import { Column, Entity, ManyToOne } from 'typeorm';

@Entity()
class Quote extends VendureEntity {
constructor(input?: DeepPartial<Quote>) {
super(input);
}

@ManyToOne(type => Order)
order: Order;

@EntityId()
orderId: ID;

@Column()
text: string;

@Money()
value: number;

// Whenever you store a monetary value, it's a good idea to also
// explicitly store the currency code too. This makes it possible
// to support multiple currencies and correctly format the amount
// when displaying the value.
@Column('varchar')
currencyCode: CurrencyCode;

@Column()
approved: boolean;
}

Advanced configuration: MoneyStrategy

For advanced use-cases, it is possible to configure aspects of how Vendure handles monetary values internally by defining a custom MoneyStrategy.

The MoneyStrategy allows you to define:

  • How the value is stored and retrieved from the database
  • How rounding is applied internally
  • The precision represented by the monetary value (since v2.2.0)

For example, in addition to the DefaultMoneyStrategy, Vendure also provides the BigIntMoneyStrategy which stores monetary values using the bigint data type, allowing much larger amounts to be stored.

Here's how you would configure your server to use this strategy:

src/vendure-config.ts
import { VendureConfig, BigIntMoneyStrategy } from '@vendure/core';

export const config: VendureConfig = {
// ...
entityOptions: {
moneyStrategy: new BigIntMoneyStrategy(),
}
}

Example: supporting three decimal places

Let's say you have a B2B store which sells products in bulk, and you want to support prices with three decimal places. For example, you want to be able to sell a product for $1.234 per unit. To do this, you would need to:

  1. Configure the MoneyStrategy to use three decimal places
import { DefaultMoneyStrategy, VendureConfig } from '@vendure/core';

export class ThreeDecimalPlacesMoneyStrategy extends DefaultMoneyStrategy {
readonly precision = 3;
}

export const config: VendureConfig = {
// ...
entityOptions: {
moneyStrategy: new ThreeDecimalPlacesMoneyStrategy(),
}
};
  1. Set up your storefront to correctly convert the integer value to a decimal value with three decimal places. Using the formatCurrency example above, we can modify it to divide by 1000 instead of 100:
src/utils/format-currency.ts
export function formatCurrency(value: number, currencyCode: string, locale?: string) {
const majorUnits = value / 1000;
try {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currencyCode,
minimumFractionDigits: 3,
maximumFractionDigits: 3,
}).format(majorUnits);
} catch (e: any) {
return majorUnits.toFixed(3);
}
}