Shreeji
An IT retailer's storefront, where the price on the page is always one currency conversion away from the cost
A NestJS and Next.js commerce platform for a Zambian IT supplier: storefront, customer portal and admin console over one API. The catalogue was the easy part. Stock gets bought in dollars and sold in kwacha, and two of the three ways a customer can pay don't resolve during checkout at all.

StatusStorefront and admin live. Card checkout clears a manual live-card pass each release, since 3DS and OTP cannot be automated.
- Built for
- Shreeji
- My role
- Architecture and implementation across both repositories: the storefront and customer portal, the admin console, and the payments, inventory and content APIs behind them.
- When
- 2025 — present
- TypeScript
- NestJS
- TypeORM
- PostgreSQL
- Next.js
- Tailwind CSS
- DPO Pay
- MinIO
- Docker
The longer version
The problem
Shreeji sells computers, power systems and networking hardware in Lusaka, and has done since 1998. Putting that catalogue online is not the hard part. Two things about the business are.
The first is currency. Stock is bought in dollars and sold in kwacha, against a rate that moves, plus 16% VAT. So a price isn't a field someone types. It's the output of a calculation whose inputs keep changing, and every one of those inputs has to be inspectable months later, when a customer asks why the number differs from the quote you gave them last week.
The second is that a Zambian business buyer often doesn't pay with a card. They pay by bank transfer, or they collect and pay on pickup. Either way the order exists and sits unpaid for hours or days. A checkout flow modelled on card-or-nothing has nowhere to put that state.
What I built
Both halves: the NestJS API and the Next.js frontend that serves the storefront, the customer portal and the admin console.
Two parts are worth reading about. The first is the payment abstraction: one gateway interface, with a card path that's only ever confirmed server-side, an asynchronous bank-transfer path with a deadline and a job that enforces it, and a cash-on-pickup path that skips the gateway entirely. The second is the order transaction, which locks the stock it's about to spend instead of trusting a read.
Around those sits an admin console that owns the catalogue, inventory with stock valuation, coupons, loyalty, returns and the site's own page copy, so marketing changes don't need a deploy. Gateway credentials and saved card tokens are AES-256 encrypted at rest, with only the last four digits of a card kept in the clear. Product images run through a background-removal service and land in MinIO rather than the application's filesystem.
Guest checkout can look an order up afterwards without an account, through an endpoint that 404s when the email doesn't match the order. An unauthenticated lookup that answers "wrong email" differently from "no such order" is just an email enumeration oracle with extra steps.
What is still on the roadmap
DPO's card path puts redirect, 3DS and callback in one pass, and no automated suite can exercise that. It needs a live card and an OTP from a real handset. So it clears a manual check rather than a green CI run, and I'd rather say that plainly than blur it. A path covered by tests and a path covered by someone actually running it are different things.
Payment retries and refunds are specified rather than built. Refunds today are a manual reconciliation between the gateway and the order, which is the right call at the volume they happen and the wrong one as that volume grows.
There's also a performance question I'm partway through. The API has been intermittently slow on staging while Postgres itself stayed responsive, which rules out the database and points at the Node process. My money is on event-loop blocking or connection-pool starvation. The next step is per-request tracing to tell those two apart, rather than guessing.
Screens and demos
What it looks like in use, not just what it was designed to do.

Storefront: catalogue by category 
Featured product 
Product detail: the price shows its own arithmetic, base plus 16% VAT 
Related products 
Checkout: collection point, pickup instructions and totals 
Admin: product catalogue 
Admin: inventory, with stock valuation per SKU 
Admin: page content, editable without a deploy
What made it hard
The problems worth reading about. Everything else in this system was ordinary work.
- 01
Stock is bought in US dollars and sold in kwacha, so the shelf price is a derived number: base cost, converted at a rate, plus 16% VAT. That makes the rate an operational setting with an audit trail and a tracked monthly API quota, not a constant in the source. The product page then shows base and VAT separately, because a business buyer is reconciling that page against an invoice.
- 02
Three payment paths sit behind one gateway interface, and two of them don't finish during checkout. Card goes through DPO's XML API, where nothing is marked paid until the server has asked `verifyToken` itself, because a redirect back from the gateway is client-controlled. Bank transfer is asynchronous and human: the order carries a payment deadline, accepts a proof-of-payment upload, and an hourly job cancels whatever was never paid. Cash on pickup defers payment entirely.
- 03
Order creation takes a pessimistic write lock on every product row inside one transaction, with a 10-second `lock_timeout`, and refuses the whole order on insufficient stock rather than accepting it and reconciling later. Selling one machine twice is not a discrepancy you can fix in a report when there is only one machine.
- 04
Order status is pushed to both the portal and the admin console over SSE. That stream has to bypass the Next.js proxy. Routed through it, events got buffered and a status change landed as a batch minutes late, which to the person watching the screen is indistinguishable from nothing happening.
What it measures
Each figure says where it came from, so you can judge how much weight to give it. Some are measurements and some are chosen thresholds; the basis line tells you which.
- 43
- Entities in the domain model
- 3
- Payment paths behind one interface
- 10s
- Lock timeout on order creation
Files in src/entities, covering orders, payments, inventory, loyalty, returns, coupons, content and settings.
DPO card, bank transfer with proof upload and deadline, and cash on pickup. Each one implements the same `PaymentGateway` contract.
`SET LOCAL lock_timeout` inside the order transaction, so a contended row fails the checkout instead of holding a connection open indefinitely.