Work 2024
Invoice Portal
A supplier relation portal that lets suppliers submit invoices against their purchases, and keeps the system in step automatically.
- Year
- 2024
- Role
- Full-stack Engineer
- Client
- SAPPHIRE / Diamond Fabrics
The Invoice Portal supplier dashboard, showing submitted invoices against purchase records.
Stack
- Next.js
- NestJS
- Tailwind CSS
- TypeScript
- SSR
A supplier relation management portal built for Diamond Fabrics, part of the SAPPHIRE group. Suppliers sign in, submit invoices against the purchases made from them, and the system updates accordingly — no email threads, no paper chase.
The front end is Next.js with Tailwind; the back end is NestJS. The portal leans on Next.js server-side rendering so pages arrive already built and ship less JavaScript to the browser — which matters when the people using it are suppliers on modest hardware and unpredictable connections, not engineers on fibre.
The first thirty seconds explain the whole product. You sign in with credentials issued to you, and you see one thing: the purchases recorded against your account. You pick a purchase, attach the invoice for it, and submit. On the other side of that button, the invoice is stored against that specific purchase — not in an inbox, not in a folder — so when finance looks at the order, the paperwork is already sitting on it.
Problem
The portal exists to make the link between an order and its receipt verifiable. Purchases at Diamond Fabrics are made by people on the company's behalf, and each purchase is expected to have a receipt behind it. Before the portal, there was no systematic, order-by-order way to check that — matching a receipt to the order it belonged to was a manual step, done after the fact, which meant a mismatched or incorrect receipt could sit against an order without anyone easily noticing.
The request came from the top: SAPPHIRE's CEOs asked for this on behalf of finance and management, not as a developer-led initiative. What they wanted was a way to check the money moving through orders against what was actually being spent — a control the existing process didn't have. The portal answers that directly: each purchaser gets their own credentials, sees only their own orders, and uploads the receipt against the specific order it belongs to. The receipt is attached to the order automatically, at the point of purchase, rather than matched to it later by someone working from loose paperwork.
Research
There was no formal research phase, and it would be dishonest to dress one up. The requirement arrived already shaped — finance and management knew exactly what control they were missing — so the work was understanding the existing process well enough to encode it: how a purchase gets made and recorded, what a receipt is supposed to prove, and where the old match-it-later step actually broke down. That meant learning the company's purchase records before writing any code — what a purchase looks like in the system, which fields identify it, and what finance needs attached to trust it. The design decision that came out of that understanding is the one the whole portal rests on: attach the invoice to the purchase at submission time, by the person who made the purchase, instead of reconstructing the link later from paperwork.
Architecture
Next.js (SSR) front end, NestJS API, Tailwind for styling.
The boxes are few and the arrows all point one way. The browser talks only to the Next.js app; the Next.js server talks to the NestJS API; the API is the single thing that touches the data. Purchase records come from the company's existing systems — the portal doesn't invent purchases, it fronts ones already made — and the portal's own tables carry what those systems don't: the submitted invoices, their files, their statuses, and the accounts that submitted them. The core schema is close to what you'd sketch on a napkin: an account, the purchases recorded against it, and invoices that each belong to exactly one purchase, with a status that tracks where the submission stands.
Auth is credential-based — accounts are issued, not self-registered, since every user is someone the company already has a purchasing relationship with. The scoping rule is the part that actually matters: every query the API runs is filtered by the authenticated account, at the API layer, on every endpoint. The client never gets to ask for "purchases" — only "my purchases" — so seeing someone else's orders isn't a bug that could slip in through a missing if; the question can't be asked.
NestJS was a deliberate choice on the back end, for two reasons, and both are real. The technical one: NestJS's structure — modules, dependency injection — scales past a solo file in a way a bare Express app doesn't once there's more than one contributor or more than one route file's worth of logic. The other reason is just as honest: this was a chance to learn NestJS properly, rather than a framework picked to sound good on a résumé. SSR earned its place for the reason already noted above — it means suppliers on modest hardware and unpredictable connections get a page that arrives already built, rather than one assembled client-side after a slower first paint.
Development
Built during my time at Sapphire's Diamond Fabrics, alongside DCRM — same employer, same period, a small in-house team rather than a big product organisation. The portal was mine across the stack: the Next.js front end and the NestJS API both. It ran on company infrastructure at its internal domain (invportal.dfl.com.pk); this was a corporate on-premises environment, so releases went through the company's own process, with changes checked against real purchase records before they went in front of users, rather than a public cloud pipeline with automated previews.
The part I'd defend hardest is the submission flow, because it's where the product's one promise gets enforced. An invoice isn't accepted as a file upload; it's accepted as a claim against a specific purchase, and the API checks the claim before storing anything: the purchase exists, it belongs to the authenticated account, and it isn't already carrying a submission it shouldn't. Only when the claim holds does the file get stored and the record written — so a row in the invoices table doesn't mean "someone uploaded something", it means "this purchase has its paperwork". That's the difference between a shared drive with a login screen and a system finance can actually trust: the invariant is enforced at write time, not audited after the fact.
Challenges
Keeping accounts sealed off from each other. The portal's whole value is that each account sees only its own purchases, and that guarantee is easy to state and easy to lose — one endpoint that forgets the filter and the control is gone. The fix wasn't vigilance, it was structure: scoping by the authenticated account happens in the API's data access itself, not in each route handler, so an endpoint can't forget what it never had to remember.
Users on the far end of the connection quality curve. The people submitting invoices weren't on developer machines — modest hardware, old browsers, unreliable connections. That's why SSR earned its place, but it also shaped the submission flow: uploads had to fail loudly and recoverably, so a dropped connection mid-submit left a purchase clearly unsubmitted rather than half-attached, and the user could simply try again without creating a duplicate.
Building against records I didn't own. Purchase data came from the company's existing systems, which meant the portal had to treat it as read-only input that could carry surprises — records shaped by processes older than the portal, edge cases nobody had written down. The resolution was defensive posture at that one boundary: validate what comes in, refuse what doesn't match, and never write back into a system whose invariants weren't mine to guarantee.
Results
The Invoice Portal wasn't pushed further after I left the company, and since then no React developer has been hired to maintain it.
Lessons learned
The Results section above is the honest lesson. The portal worked, and then it stopped being pushed forward — not because the code failed, but because I left and nobody with the stack was hired behind me. If I started again on Monday I'd build the same system, but I'd weigh the framework choices against the team that has to keep it, not just the one building it. NestJS was the right call technically and I learned it properly, exactly as intended — but "the right call technically" and "the call the organisation can sustain" are different questions, and I only learned to ask the second one by watching what happened after the first.
What I got wrong at the start was thinking the hard part was the code. It wasn't. The hard part was the process the code encodes — understanding who buys, what a receipt proves, and where the old way actually leaked. The weeks spent understanding that before building were worth more than any technical decision in the repo, and that ordering — process first, code second — is the thing from this project I've carried into everything since.