A Node.js + TypeScript project that calculates basket totals for Acme Widget using extensible pricing and delivery rules. This project demonstrates a clean separation of concerns, the strategy pattern for offers, and precise financial calculations using integer cents.
- Install & Run:
npm install && npm run dev - Example Totals: The four example baskets will print
PASSwith totals of$37.85,$54.37,$60.85, and$98.27. - Core Idea: Business rules are implemented with small, testable services and strategy objects.
- π§Ί ACME Widget Basket
- Precise Calculations: All monetary math is done in integer cents to avoid floating-point errors.
- Pluggable Offers: Easily add new promotions by implementing the
Offerinterface (Strategy Pattern). - Configurable Delivery: Delivery pricing is handled by a swappable
DeliveryRuleinterface. - Simple Catalog: A straightforward
ProductCatalogabstraction for easy product lookups. - Minimalist: A dependency-light TypeScript codebase focused on core logic.
- Node.js: v18+ recommended
- npm: v9+ recommended
Install dependencies and run the sample runner located in src/index.ts:
npm install
npm run devYou should see four scenarios printed to the console with PASS statuses.
The following example demonstrates how to wire up the components to calculate a basket's total.
import { Basket } from "./src/Basket";
import { SimpleProductCatalog } from "./src/catalogs/SimpleProductCatalog";
import { StandardDeliveryRule } from "./src/services/DeliveryService";
import { PricingService } from "./src/services/PricingService";
import { BuyOneGetHalfPrice } from "./src/strategies/BuyOneGetHalfPrice";
// 1. Set up services and rules
const catalog = new SimpleProductCatalog();
const deliveryRule = new StandardDeliveryRule();
const offers = [new BuyOneGetHalfPrice("R01")];
const pricing = new PricingService(offers);
// 2. Create and fill the basket
const basket = new Basket(catalog, deliveryRule, pricing);
basket.add("B01");
basket.add("G01");
// 3. Get the final total
console.log(`Total: $${basket.total().toFixed(2)}`);The SimpleProductCatalog provides the following products:
R01β Red Widget β$32.95G01β Green Widget β$24.95B01β Blue Widget β$7.95
The current offer strategy is Buy One Red Widget, Get the Second Half Price. This is implemented in src/strategies/BuyOneGetHalfPrice.ts. The PricingService applies all relevant offers to the basket to calculate the total discount.
Delivery charges are calculated based on the post-discount subtotal:
- Subtotal < $50 β
$4.95delivery - Subtotal β₯ $50 and < $90 β
$2.95delivery - Subtotal β₯ $90 β Free delivery
src/
ββ Basket.ts # Orchestrates totals, discounts, and delivery.
ββ catalogs/
β ββ SimpleProductCatalog.ts # In-memory product catalog implementation.
ββ interfaces/
β ββ DeliveryRule.ts # Contract for a delivery cost rule.
β ββ Offer.ts # Contract for an offer (returns discount in cents).
β ββ ProductCatalog.ts # Contract for a product lookup service.
ββ models/
β ββ BasketItem.ts # Tracks product code and quantity.
ββ services/
β ββ DeliveryService.ts # Standard delivery rule implementation.
β ββ PricingService.ts # Applies offers and sums discounts.
ββ strategies/
ββ BuyOneGetHalfPrice.ts # "Buy one, get one half price" offer logic.
The project uses ESLint for linting and Prettier for formatting. Vitest is used for testing.
npm run dev: Runs the demo script (src/index.ts).npm test: Runs the test suite once.npm run test:watch: Runs tests in watch mode for active development.npm run coverage: Generates a test coverage report.npm run lint: Lints the codebase for errors.npm run lint:fix: Automatically fixes linting errors.npm run format: Formats code using Prettier.
- New Products: Implement the
ProductCataloginterface (e.g., to fetch from a database or API calls). - New Offers: Create a new class that implements the
Offerinterface and add it to thePricingService. - New Delivery Rules: Implement the
DeliveryRuleinterface and pass it to theBasketconstructor.
- Integer Cents: All monetary calculations use integer cents to ensure accuracy and avoid floating-point rounding issues. Dollar conversion only happens at the final output.
- Truncation: The final total is truncated, not rounded (e.g.,
37.857becomes37.85), to match the provided test expectations. This is done viaMath.floor(totalInCents) / 100. - Offer Isolation: Offer logic is fully encapsulated behind the
Offerinterface. Adding or changing promotions does not require modifying theBasketor other services. - Efficiency: The basket uses an internal
Map<string, BasketItem>for efficient O(1) lookups and quantity updates.
- Product codes are validated by the
ProductCatalog. Adding an unknown code will throw an error. - The "Buy One Get One Half Price" offer is applied for every two items of the same product code.
- Delivery charges are always computed on the discounted subtotal.
- Let N be the number of distinct product codes in the basket and M the number of active offers:
- Subtotal computation: O(N)
- Offer lookup per item: O(M) naive (can be O(1) with a map); in this code M is tiny and lookup is a simple
find. - Overall per-total calculation: O(N + M)
- Composite Offers: Support applying multiple offers to a single product with rules (e.g., "max discount wins," stackable vs. exclusive).
- Money Type: Introduce a dedicated
Moneyclass to encapsulate currency, formatting, and conversions. - Configuration-Driven Rules: Load delivery thresholds and fees from a configuration file instead of hardcoding them.
- More Offer Strategies: Implement additional offer types like bulk discounts, "spend X get Y free," or coupon codes.
π€ Derek Akrasi Konadu
A full-stack developer passionate about building clean, scalable applications.
- GitHub: @obibaadoma
- LinkedIn: derek-akrasi-konadu
- Email: akrasikonadu@yahoo.com