Skip to content
Shreeji2025 — present
In productionarchitecturepaymentsfrontend

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.

Shreeji interface

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
Write-up

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.

01Look & feel

Screens and demos

What it looks like in use, not just what it was designed to do.

  • Shreeji storefront browsing computers, with a category sidebar and a row of featured all-in-one machines
    Storefront: catalogue by category
  • Featured product panel on the storefront with a Shop Now call to action
    Featured product
  • Product page for a Lenovo ThinkPad, with a spec table and a kwacha price broken into base cost and VAT
    Product detail: the price shows its own arithmetic, base plus 16% VAT
  • Related products carousel below a product page, each card priced with an add-to-cart button
    Related products
  • Checkout collection step showing the Lusaka pickup address on a map, pickup instructions, and an order total
    Checkout: collection point, pickup instructions and totals
  • Admin product catalogue as a filterable grid of products with active and inactive status badges
    Admin: product catalogue
  • Admin inventory table listing SKUs with stock health, in-stock status, unit price and stock valuation
    Admin: inventory, with stock valuation per SKU
  • Admin content editor for the home page, editing the hero tagline and its rotating slides
    Admin: page content, editable without a deploy
02The engineering

What made it hard

The problems worth reading about. Everything else in this system was ordinary work.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

03Numbers

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

Files in src/entities, covering orders, payments, inventory, loyalty, returns, coupons, content and settings.

3
Payment paths behind one interface

DPO card, bank transfer with proof upload and deadline, and cash on pickup. Each one implements the same `PaymentGateway` contract.

10s
Lock timeout on order creation

`SET LOCAL lock_timeout` inside the order transaction, so a contended row fails the checkout instead of holding a connection open indefinitely.