@restorecommerce/cart
![](http://img.shields.io/npm/v/@restorecommerce/cart.svg?style=flat-square)
![Build Status](http://img.shields.io/travis/restorecommerce/cart/master.svg?style=flat-square)
![Dependencies](https://img.shields.io/david/restorecommerce/cart.svg?style=flat-square)
![Coverage Status](http://img.shields.io/coveralls/restorecommerce/cart/master.svg?style=flat-square)
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();
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'),
taxType: 'vat_reduced',
weight: 210,
height: 2.20,
width: 13.5,
depth: 8.22,
quantity: 7,
}, {
sku: 'cr5-red',
price: new Decimal('1.10'),
taxType: 'vat_standard',
weight: 210,
height: 2.20,
width: 13.5,
depth: 8.22,
quantity: 15,
}, {
sku: 'cr3-yellow',
price: new Decimal('2.48'),
taxType: 'vat_standard',
weight: 210,
height: 2.20,
width: 13.5,
depth: 8.22,
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
}
modifyItem(item: any)
cart.modifyItem({
sku: 'cr5-red',
quantity: 10,
});
modifyItemQuantity(sku: string, quantity: number)
cart.modifyItemQuantity('cr5-red', 5);
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);
Cart.round('1.123');
Cart.round(new Decimal(1.125));
Cart.round(new Decimal('1.127'));
getTotalNet()
Total net (without taxes).
Cart.round(cart.getTotalNet());
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());
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: