@restorecommerce/cart

![Build Status][build]![Dependencies][depend]![Coverage Status][cover]

Usage no npm install needed!

<script type="module">
  import restorecommerceCart from 'https://cdn.skypack.dev/@restorecommerce/cart';
</script>

README

@restorecommerce/cart

Build StatusDependenciesCoverage Status

An backend agnostic purely data-driven shopping cart that can be used on client- and server side.

Features

  • Item handling (add, remove etc.)
  • VAT calculation with extensible tax model
    • EU tax calculation built-in (for sub-threshold based taxation)
  • Shipping cost calculation based on selected courier and plan
  • Extensibility for adding couriers
  • Totals calculation based on items, taxes and shipping
  • Can run completely offline
  • Local storage on browsers via pluggable serializers
  • Back-end agnostic
  • Fully typed

Usage

Basic Example

Cart Instantiation:

const cart = new Cart({
  serializer: new MockSerializer(),
  shippingMethod: new Courier({
    source: JSON.stringify(data.publicDHL),
    shipping: {originCountry: 'DE'}
  }),
  taxOriginCountry: 'DE',
  taxes: {
    vat_standard: {
      rate: new Decimal(1.19),
      desc: '+ VAT 19%'
    },
    vat_reduced: {
      rate: new Decimal(1.07),
      desc: '+ VAT 7%'
    }
  }
});

Setting serializer and shippingMethod is optional.

setDestinationCountry(country: string)

cart.setDestinationCountry('LV');

getShippingMethod()

cart.getShippingMethod();

Output:

Courier {
  _source: {
    assumptions: {
      currency: 'eur',
      dimensions: [Object],
      length: 'mm',
      ranges: [Object],
      weight: 'gram'
    },
    zones: {
      '1': [Object],
      '2': [Object],
      '3': [Object],
      '4': [Object],
      '5': [Object],
      '6': [Object],
      '7': [Object],
      '8': [Object],
      national: [Object]
    }
  },
  _shipping: {
    destinationCountry: 'LV',
    originCountry: 'DE' }
}

getShipping()

There is no setShipping setter, since info about shipping is taken from ShippingMethod's source property.

cart.getShipping(); // calculates shipping cost

Output:

{
  price: '15.99',
  taxType: 'vat_standard',
  maxWeight: 5000,
  type: 'package',
  zone: '1',
  human: {
    zone: '1 (all EU countries)',
    offer: 'Package up to 5kg',
  }
}

getSerializer()

cart.getSerializer();

getTaxRates()

cart.getTaxRates();

Output:

{
  taxes: {
    vat_standard: {
      rate: '1.19',
      desc: '+ VAT 19%'
    },
    vat_reduced: {
      rate: '1.07',
      desc: '+ VAT 7%'
    }
  }
}

addItems(items: IItem[])

cart.addItems([{
  sku: 'cr2-blue',
  price: new Decimal('12.95'), // Price
  taxType: 'vat_reduced',
  weight: 210, // grams
  height: 2.20, // cm
  width: 13.5, // cm
  depth: 8.22, // cm
  quantity: 7,
}, {
  sku: 'cr5-red',
  price: new Decimal('1.10'),
  taxType: 'vat_standard',
  weight: 210, // grams
  height: 2.20, // cm
  width: 13.5, // cm
  depth: 8.22, // cm
  quantity: 15,
}, {
  sku: 'cr3-yellow',
  price: new Decimal('2.48'),
  taxType: 'vat_standard',
  weight: 210, // grams
  height: 2.20, // cm
  width: 13.5, // cm
  depth: 8.22, // cm
  quantity: 3,
}]);

remItem(sku: string)

cart.remItem('cr3-yellow');

getItems()

cart.getItems();

Output:

[
  {
    sku: 'cr2-blue',
    price: 12.95,
    taxType: 'vat_reduced',
    weight: 210,
    height: 2.2,
    width: 13.5,
    depth: 8.22,
    quantity: 7
  },
  {
    sku: 'cr5-red',
    price: 1.10,
    taxType: 'vat_standard',
    weight: 210,
    height: 2.2,
    width: 13.5,
    depth: 8.22,
    quantity: 15
   }
]

setCustomer(customer: ICustomer)

cart.setCustomer({
  type: CustomerType.COMMERCIAL,
});

setCustomerType(type: CustomerType)

This modifies the customer..

cart.setCustomerType(CustomerType.PRIVATE);

getCustomer()

cart.getCustomer();

Output:

{
  type: 1 // 0 - COMMERCIAL / 1 - PRIVATE
}

modifyItem(item: any)

cart.modifyItem({
  sku: 'cr5-red',
  // no update for other Cart properties as no change
  quantity: 10, // change only quantity
});

modifyItemQuantity(sku: string, quantity: number)

cart.modifyItemQuantity('cr5-red', 5); // adds 5 to initial quantity

getItemQuantity(sku: string)

`Amount of cr2-blue = ${cart.getItemQuantity('cr5-red')}`;

Output:

Amount of cr5-red = 15 // 10 + 5

getItemCount()

`Amount of unique products = ${cart.getItemCount()}`;

Output:

Amount of unique products = 2

getGrandQuantity()

`Amount of all products = ${cart.getGrandQuantity()}`;

Output:

Amount of all products = 22 // 15 + 7

getTaxes(keepOriginalTaxType?: boolean )

cart.getTaxes();

Output:

netPrice and rate are instances of Decimal.

{
  vat_standard: {
    netPrice: '32.49',
    rate: '1.19',
    desc: '+ VAT 19%'
  },
  vat_reduced: {
    netPrice: '90.65',
    rate: '1.07',
    desc: '+ VAT 7%'
  }
}

static round(money: Money)

Parameter could be number|string|Decimal. Returns string.

Cart.round(1.120); // '1.12'
Cart.round('1.123'); // '1.13'
Cart.round(new Decimal(1.125)); // '1.13'
Cart.round(new Decimal('1.127')); // '1.13'

getTotalNet()

Total net (without taxes).

Cart.round(cart.getTotalNet()); // '123.14'

Calculation:

1) customer.billing.countryCode === 'LV' => VAT + 19% / 7%
2) cr2_blue: Price * quantity => 12.95 * 7  = 90.65
3) cr5_red:  Price * quantity => 1.10 * 15  = 16.5
4) Max Weight of all items = 22 items * 210 grams  = 4620 grams =>
shipping = 15.99 euro for package less than 5000 grams to EU
5) sum = 90.65 + 16.5 + 15.99 = 123.14

getTotalGross()

Total gross (with taxes).

Cart.round(cart.getTotalGross()); // '135.66'

Calculation:

1) cr2_blue => cr2_blue + VAT 7% = 90.65 * 1.07 = 96.9955
2) cr5_red => cr5_red + VAT 19% = 16.5 * 1.19 = 19.635
3) shipping => shipping + VAT 19% = 15.99 * 1.19 = 19.0281
4) sum  =>  96.9955 + 19.635 + 19.0281 = 135.6586
5) round(135.6586) = 135.66

Tests

More examples of using the Cart can be found in test/index.ts.

Reference

Method Description
getItems(): IItems / undefined Get Items
private setItems(items: IItem[]): void Set items
getCustomer(): ICustomer / undefined Get customer
setCustomer(customer: ICustomer): void Set customer
getShippingMethod(): IShippingMethod/ undefined Get shipping method
setShippingMethod(shippingMethod: IShippingMethod): void Set shipping method
getSerializer(): ISerializer / undefined Get serializer
setSerializer(serializer: ISerializer): void Set serializer
getTaxRates(): TaxRates Get tax rates
private setTaxRates(taxRates: TaxRates) Set tax rates
setCustomerType(type: CustomerType): void Set customer's type (PRIVATE/COMMERCIAL)
setDestinationCountry(country: string): void Set destination country
addItems(items: IItem[]): void Add item/items to the cart
remItem(sku: string): void Remove item from the cart by SKU (Stock Keeping Unit)
modifyItem(item: any) Update item
modifyItemQuantity(sku: string, quantity: number): void Modify item's quantity
getItemCount(): number Get item count
getItemQuantity(sku: string): number Get quantity of particular item
getGrandQuantity(): number Item count x quantity of each item
getTaxes(keepOriginalTaxType?: boolean ): { [taxType: string]: { netPrice: Decimal, rate: Decimal, desc: string, price: Decimal } } Get tax list, with ratios and additive costs
getTotalNet(): number Get sum of item and shipping costs (taxes excluded)
getTotalGross(): number Get sum of item and shipping costs (taxes included)
getShipping(): { price: Money, [prop: string]: any } Get item's shipping info
static round(money: Money): string Convert money type (number/string/Decimal) to string with rounding

Development

To lint, transpile and test, run:

npm test

Couriers

IShippingMethod interface implementations:

Courier Plans:

Calculation Logic

Taxes

For VAT calculation, the cart applies the EU ruleset for taxation which is exemplified in the following with a shop that is located in Germany and customer location location/ type being:

  • Germany private: VAT applies
  • Germany commercial: VAT applies
  • Other EU countries private: VAT applies
  • Other EU countries commercial: VAT free
  • Non-EU countries private: VAT free
  • Non-EU countries commercial: VAT free

This also works for other countries as it's just a simplification of this for most countries. Sales taxes and other country specific taxes can be added.

Legal background: