@timbouc/cart

A flexible and fluent way to manage shopping carts.

Usage no npm install needed!

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

README

@timbouc/cart

A Shopping Cart Implementation for Node.js and browsers.

:yellow_heart: Features

  • Pluggable storage drivers
  • Manage vouchers, shipping and other conditional factors with Conditions
  • Sync frontend and backend instances with remote storage connection
  • Comes with a default local storage (Node.js)
  • Save miscellaneous data to cart (e.g. currency, purchase metadata)
  • Customise final items' and conditions' prices with hooks for complex (e.g. graduated pricing) use case

Getting Started

This package is available in the npm registry. It can easily be installed with npm or yarn.

$ npm i @timbouc/cart
# or
$ yarn add @timbouc/cart

Instantiate with a configuration.

import { Cart } from '@timbouc/cart';
const cart = new Cart(context.uudid, config);

Usage

const item = await cart.add({
    id: product.id,
    name: product.name,
    price: product.price,
});
await cart.apply([
    {
        name: 'Voucher 1',
        type: 'voucher',
        target: item.item_id,
        value: '-10%', // removes 10% of the value of product.price
    },
    {
        name: 'Shipping',
        type: 'shipping',
        target: 'subtotal', // add 10 to subtotal
        value: 10,
    }
]);

const subtotal = await cart.subtotal(),
      total = await cart.total();

Registering Storage

const { RedisStorage, PostgresStorage } from './StorageDrivers';

const session = context.uudid
const cart = new Cart(session, config)
cart.registerDriver('redis', RedisStorage)
cart.registerDriver('pg', PostgresStorage())

// use redis storage for different shopping carts
cart.driver('redis')
    .add(...)

// use postgres storage for wishlist
cart.driver('pg')
    .session(session + ':wishlist') // can also change session (user)
    .add(...)

See RedisStorage example from custom storage.

Response interface

Asynchronous methods will always return a Promise which resolves with a Response object.

Methods

session(session: string): Cart
// Set/switch cart session instance
cart.session(user_id)
    .add(...)
add(item: CartInputItem|Array<CartInputItem>): Promise<CartItem|Array>

Add an item or an array of items to cart

// add one item to cart
const item = await cart.add({
    // item_id: 1,        // Manually set item id
    id: product.id,
    name: product.name,
    price: product.price,
    quantity: 3, // defaults to one
    conditions: conditions as Array<CartCondition>,
    options: options as Array<CartItemOption>,
})

// add multiple items to cart
const [item1, item2] = await cart.add([
    {
        id: product1.id,
        name: product1.name,
        price: product1.price,
        options: [{
            name: "Color",
            value: "pink",
        }]
    },
    {
        id: product2.id,
        name: product2.name,
        price: product2.price,
    },
])

// Add item with custom field(s)
// cannot be updated afterwords
const item = await cart.add({
    id: product.id,
    name: product.name,
    price: product.price,
    workspace: 'Timbouc',
})
update(item_id: number|string, options: object): Promise<CartItem>

Update a cart item. Accumulates quantity by default but override can be specified

// new item price, price can also be a string format like so: '98.67'
cart.update(5, {
    name: 'New Item Name',
    price: 99.99,
});

// update a product's quantity
cart.update(5, {
    quantity: 2, // if the current product has a quantity of 4, another 2 will be added so this will result to 6
});

// update a product by reducing its quantity
cart.update(5, {
    quantity: -1, // if the current product has a quantity of 4, another 1 will be subtracted so this will result to 3
});

// To totally replace the quantity instead of incrementing or decrementing its current quantity value
// pass an object
cart.update(5, {
    quantity: {
        relative: false,
        value: 5
    }
});
remove(item_id: number | string | Array<string|number>): Promise<CartContent>

Remove an item or an array of items from cart

cart.remove(5);

// Remove multiple
cart.remove([2, 3, 4]);
get(item_id: number | string): Promise<CartItem>
// Get cart item
const item = await cart.get(item_id);
apply(condition: CartCondition | Array<CartCondition>): Promise<CartCondition | Array<CartCondition>>

Apply a cart condition or an array of conditions. Conditions are used to account for discounts, taxes and other conditional factors. The field target specifies the entity the condition applies to. This value can be total, subtotal or an item ID.

const voucher1 = await cart.apply({
    name: 'Voucher 1',
    type: 'voucher',
    target: 1,  // cart item id
    value: -10, // removes the value `10` from i1
});

// apply multiple conditions
const [voucher1b, tax] = await cart.apply([
    {
        name: 'Voucher 1', // Replaces `Voucher 1` as it already exists (handy for managing tax, shipping and other standard conditions)
        type: 'voucher',
        target: 2,  // cart item id
        value: '-10%', // removes 10% of the value of item 2
    },
    {
        name: 'Tax',
        type: 'tax',
        target: 'subtotal',
        value: '10%', // adds 10% of subtotal to total
    }
]);
conditions(): Promise<Array<CartCondition>>
// List cart conditions
await cart.conditions()
condition(name: string): Promise<CartCondition>
// Get condition
await cart.condition('Voucher 2')
removeCondition(name: string): Promise<Boolean>
// Remove condition
await cart.removeCondition('Voucher 2')
clearConditions(): Promise<Boolean>
// Clear all cart conditions
await cart.clearConditions()
data(key?: string | Record<string, any>, value?: any): Promise<any>
// Save currency. Returns `AUD`
let d1 = await cart.data('currency', 'AUD')

// Get saved data
let d2 = await cart.data('currency')

// Use dot notation
await cart.data('customer.name', 'Johnn Doe')
await cart.data('customer.email', 'johndoe@mail.com')
await cart.data('customer') // returns { name: 'Johnn Doe', email: 'johndoe@mail.com' }
await cart.data('token', '1233')
await cart.data() // returns { currency: 'AUD', customer: { name: 'Johnn Doe', email: 'johndoe@mail.com' }, token: '1233' }


// Alternatively, set the above data by passing an object
// Note that this will NOT override `token` field set above
await cart.data({
  currency: 'AUD',
  customer: { name: 'Johnn Doe', email: 'johndoe@mail.com' }
})
empty(): Promise<Boolean>
// If cart is empty
await cart.empty()
count(): Promise<number>
// Count item entries in cart
await cart.count()
content(): Promise<CartContent>

Return the cart content.

// Get cart contents
await cart.content()
items(): Promise<Array<CartItem>>
// List cart items
await cart.items()
subtotal(): Promise<number>
// Get cart subtotal
await cart.subtotal()
total(): Promise<number>
// Get cart total
await cart.total()
clear({ conditions: true, data: true }): Promise<void>
await cart.clear()

// Do not clear conditions and data
await cart.clear({ conditions: false, data: false })
copy({ conditions: true, data: true }): Promise<Cart>
const cart2 = await cart.copy(new_session_id)

// Do not copy conditions and data. Also pass a new config
const cart3 = await cart.copy(new_session_id, { conditions: false, data: false }, new_config)
storage<T extends Storage = Storage>(name?: string): T
// Get the storage instance
const storage = cart.storage()
loader(): DataLoader
// Get theunderlying data loader
const loader = cart.loader()

Contribution Guidelines

Any pull requests or discussions are welcome. Note that every pull request providing new feature or correcting a bug should be created with appropriate unit tests.