An backend agnostic purely data-driven shopping cart that can be used on client- and server side.
- 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
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
More examples of using the Cart
can be found in test/index.ts
.
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 |
To lint, transpile and test, run:
npm test
IShippingMethod
interface implementations:
Courier Plans:
- DHL premium business based on contract based pricing not publicly available.
- DHL private customers based on private customer pricing info pdf | de | intl | web
- DHL express based on contract based pricing not publicly available.
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: