diff --git a/CHANGES.md b/CHANGES.md index 9c38282..45e8e9b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased +- discount(items, percent) takes a percentage off before tax. ## 1.2.0 - total() rounds through roundCents(). diff --git a/src/lib/cart.js b/src/lib/cart.js index 385fd69..2cfa726 100644 --- a/src/lib/cart.js +++ b/src/lib/cart.js @@ -8,3 +8,12 @@ export function subtotal(items) { export function total(items) { return roundCents(subtotal(items) * (1 + TAX_RATE)); } + +/** Returns the cart total after taking `percent` off the subtotal before tax. */ +export function discount(items, percent) { + if (!Number.isFinite(percent) || percent < 0 || percent > 100) { + throw new RangeError('percent must be between 0 and 100'); + } + const validItems = items.filter((item) => item.qty > 0); + return roundCents(subtotal(validItems) * (1 - percent / 100) * (1 + TAX_RATE)); +} diff --git a/tests/lib/cart.test.js b/tests/lib/cart.test.js index 7a9eab3..bb10f0f 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, discount } from '../../src/lib/cart.js'; const items = [{ price: 10, qty: 2 }, { price: 5, qty: 1 }]; @@ -11,3 +11,42 @@ test('subtotal adds price times quantity', () => { test('total applies tax and rounds to cents', () => { assert.equal(total(items), 27); }); + +test('discount takes the percentage off before tax', () => { + assert.equal(discount(items, 10), 24.3); +}); + +test('discount of 0 percent equals the total', () => { + assert.equal(discount(items, 0), total(items)); +}); + +test('discount of 100 percent is free', () => { + assert.equal(discount(items, 100), 0); +}); + +test('discount of an empty cart is 0', () => { + assert.equal(discount([], 10), 0); +}); + +test('discount ignores items with zero or negative quantity', () => { + const extra = [...items, { price: 100, qty: 0 }, { price: 100, qty: -2 }]; + assert.equal(discount(extra, 10), 24.3); +}); + +test('discount handles large numbers', () => { + assert.equal(discount([{ price: 1000000, qty: 1000 }], 50), 540000000); +}); + +test('discount rounds a half cent up', () => { + assert.equal(discount([{ price: 0.05, qty: 1 }], 50), 0.03); +}); + +test('discount rounds 1.005 once at the end', () => { + assert.equal(discount([{ price: 1.005, qty: 1 }], 0), 1.09); +}); + +test('discount rejects percentages outside 0 to 100', () => { + assert.throws(() => discount(items, -1), RangeError); + assert.throws(() => discount(items, 101), RangeError); + assert.throws(() => discount(items, Number.NaN), RangeError); +});