diff --git a/CHANGES.md b/CHANGES.md index 9c38282..7859cf4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased +- Added checkout(items, { coupon }) returning subtotal, discount, tax and total, with SAVE10 and FLAT5 coupons. ## 1.2.0 - total() rounds through roundCents(). diff --git a/src/lib/cart.js b/src/lib/cart.js index 385fd69..aee7e1d 100644 --- a/src/lib/cart.js +++ b/src/lib/cart.js @@ -8,3 +8,29 @@ export function subtotal(items) { export function total(items) { return roundCents(subtotal(items) * (1 + TAX_RATE)); } + +const COUPONS = new Map([ + ['SAVE10', (sub) => roundCents(sub * 0.1)], + ['FLAT5', (sub) => Math.min(5, sub)], +]); + +export function checkout(items, { coupon } = {}) { + const live = items.filter((item) => item.qty > 0); + const sub = roundCents(subtotal(live)); + + const code = (coupon ?? '').trim(); + let discount = 0; + if (code !== '') { + const rule = COUPONS.get(code.toUpperCase()); + if (!rule) throw new Error(`Unknown coupon: ${code}`); + discount = roundCents(rule(sub)); + } + + const tax = roundCents((sub - discount) * TAX_RATE); + return { + subtotal: sub, + discount, + tax, + total: roundCents(sub - discount + tax), + }; +} diff --git a/tests/lib/cart.test.js b/tests/lib/cart.test.js index 7a9eab3..76adb2f 100644 --- a/tests/lib/cart.test.js +++ b/tests/lib/cart.test.js @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { subtotal, total } from '../../src/lib/cart.js'; +import { subtotal, total, checkout } from '../../src/lib/cart.js'; const items = [{ price: 10, qty: 2 }, { price: 5, qty: 1 }]; @@ -11,3 +11,72 @@ test('subtotal adds price times quantity', () => { test('total applies tax and rounds to cents', () => { assert.equal(total(items), 27); }); + +test('checkout without a coupon charges tax on the full subtotal', () => { + assert.deepEqual(checkout(items), { subtotal: 25, discount: 0, tax: 2, total: 27 }); +}); + +test('checkout ignores items with qty 0 or less', () => { + const cart = [{ price: 10, qty: 2 }, { price: 99, qty: 0 }, { price: 99, qty: -1 }]; + assert.deepEqual(checkout(cart), { subtotal: 20, discount: 0, tax: 1.6, total: 21.6 }); +}); + +test('checkout of an empty cart is all zeros', () => { + assert.deepEqual(checkout([]), { subtotal: 0, discount: 0, tax: 0, total: 0 }); +}); + +test('SAVE10 takes 10% off the subtotal before tax', () => { + assert.deepEqual(checkout(items, { coupon: 'SAVE10' }), { + subtotal: 25, + discount: 2.5, + tax: 1.8, + total: 24.3, + }); +}); + +test('FLAT5 takes $5 off the subtotal before tax', () => { + assert.deepEqual(checkout(items, { coupon: 'FLAT5' }), { + subtotal: 25, + discount: 5, + tax: 1.6, + total: 21.6, + }); +}); + +test('FLAT5 never discounts more than the subtotal', () => { + const cart = [{ price: 3, qty: 1 }]; + assert.deepEqual(checkout(cart, { coupon: 'FLAT5' }), { subtotal: 3, discount: 3, tax: 0, total: 0 }); +}); + +test('coupon codes are case-insensitive and ignore surrounding spaces', () => { + assert.deepEqual(checkout(items, { coupon: ' save10 ' }), checkout(items, { coupon: 'SAVE10' })); + assert.deepEqual(checkout(items, { coupon: 'flat5' }), checkout(items, { coupon: 'FLAT5' })); +}); + +test('empty or missing coupon means no discount', () => { + const plain = checkout(items); + assert.deepEqual(checkout(items, { coupon: '' }), plain); + assert.deepEqual(checkout(items, { coupon: ' ' }), plain); + assert.deepEqual(checkout(items, { coupon: undefined }), plain); + assert.deepEqual(checkout(items, {}), plain); +}); + +test('unknown coupon throws with the trimmed code in the message', () => { + assert.throws(() => checkout(items, { coupon: ' nope ' }), { + name: 'Error', + message: 'Unknown coupon: nope', + }); +}); + +test('coupon names that exist on Object.prototype are unknown coupons', () => { + assert.throws(() => checkout(items, { coupon: 'constructor' }), { + name: 'Error', + message: 'Unknown coupon: constructor', + }); +}); + +test('subtotal, discount and tax are rounded to cents', () => { + // 0.1 * 3 is 0.30000000000000004 in floating point; tax on 0.3 is 0.024. + const cart = [{ price: 0.1, qty: 3 }]; + assert.deepEqual(checkout(cart), { subtotal: 0.3, discount: 0, tax: 0.02, total: 0.32 }); +});