Case study 05 · Parcel forwarding & resale consignment platform · 2021
We built the software that turns a warehouse shelf into a shop window in four countries
One scan identifies the owner, the contents, the shelf and the price.
Written by Hesham Ghoniem, lead engineer ·
- Sector
- Cross-border logistics in Asia · parcel forwarding and resale consignment
- Engagement
- Full product design and build, 2021
- Scope
- Customer portal · warehouse operations · payments · admin control room · public site
- Stack, as built in 2021
- PHP
- FastRoute
- MySQL
- Server-rendered views
- 6 third-party services
- 330 — Server endpoints: 66 screens, 264 single-purpose operations
- 43 — Database tables, one schema serving portal, warehouse and public site
- 16 — Self-service panels, so the client changes the business without us
- 4 — Languages the product ships in
- 2 — Revenue lines running on one shelf
Overview
We built the whole operation as one system, on one database.
A cross-border parcel forwarding company in Asia was running on spreadsheets and memory. It gives online shoppers a warehouse address in several countries: customers buy from stores that won't ship to them, send the parcel to the warehouse instead, and the business consolidates it and forwards it home. Most of what arrives is sneakers, and a large share of customers aren't just buying. They're reselling, listing the same goods on the major sneaker resale marketplaces.
The problem is that a box arrives with no name on it. The only thing physically attached to an incoming parcel is a courier tracking number. Who owns it, what's inside, whether it's been paid for and where it's going all lived somewhere else: in a spreadsheet, in a group chat, or in the head of whoever happened to be on shift. Customers asked "where is my order?" and the honest answer was "let me go and look."
We designed and built the whole operation as one system: customer portal, warehouse floor, payment rails, admin control room and the public website, all on a single database. A customer declares a parcel before it arrives, so by the time the box hits the receiving bench the system is already expecting it. One scan then resolves the owner, checks the count against the declaration, flags a duplicate, assigns a shelf and a batch, and moves the parcel into a status the customer can watch, in their own language.
The second revenue line runs on the same shelf. Goods consigned to resale marketplaces share the same receiving, shelving, payment and dispatch machinery as ordinary freight, instead of running as a parallel business that has to be reconciled with the first one.
A courier tracking number and the customer's declaration, made before the box arrived, meet in one scan at the receiving bench. The scan resolves the owner, checks the count against the declaration, flags a duplicate, assigns a shelf and a batch, and sets a status the customer can watch.
The challenges
Seven problems, and every handover is a place a parcel can go missing.
A box arrives with no name on it
Everything about an incoming parcel, its owner, contents, destination and price, has to be inferred from a courier tracking number and nothing else. Get that inference wrong and the parcel is physically present and commercially invisible at the same time.
A tracking number is not a clean string
Customers type them by hand. They arrive in the wrong case, with stray spaces, several to a single declared item, and sometimes the same number is declared twice by two different people.
The naive version of this system, match the text and find the owner, fails on real data within days, and it fails silently. The box is on the shelf, the screen says undeclared, and nobody knows there's a problem until the customer complains.
Goods nobody declared
A real warehouse receives parcels it was never told about. Without a named workflow, those become a pile in the corner and eventually a dispute. This is the unglamorous half of logistics, and it's where most systems simply give up.
Two warehouses, two shifts, two countries, no shared truth
Two warehouses in two countries receive. Overseas and local couriers send. The customer is somewhere else entirely. Nobody in that chain is ever in the same room, and every handover is a place a parcel can quietly go missing.
One shelf, two businesses
Forwarding and resale consignment share the same warehouse, the same staff and the same physical inventory, but they're different products, with different lifecycles, different pricing and different ways of going wrong. A sneaker sent for resale can be rejected by the marketplace and need returning. A forwarded parcel can't be. Running them as two systems would mean reconciling them forever.
Money in four currencies, and a price that has to stay explicable
Shipping fees, service fees and miscellaneous charges are quoted in the currency the customer pays in, against a rate that moves. A price quoted in June still has to make sense when someone questions it in October. And customers don't pay one parcel at a time. They pay for a dozen at once, with a promo code, half by card and half by bank transfer.
A business that changes faster than software ships
Routes, couriers, warehouses, product types, shelf locations, fee structures, promo codes, statuses and legal pages all change with commercial reality. If each change needs a developer, the system becomes the bottleneck it was built to remove. Mid-project, the client even changed the company's name.
June 2021 · four bugs, one design flaw
The month the screen stopped telling the truth
In June 2021, a few weeks after the first version went onto the warehouse floor, staff started scanning boxes that the system insisted did not exist.
The parcel was physically in their hands. The customer had definitely declared it. The screen said undeclared.
That failure mode is worse than a crash. A crash tells you something is wrong. This told staff a confident lie, and the only cost of believing it was that a customer's parcel got treated as anonymous stock and shelved with no owner attached.
The cause wasn't one bug. It was one assumption.
The system had been written as if a tracking number were a clean, single, exact string. Real ones aren't. A customer declares AB1234XYZ; the courier label reads ab1234xyz. A customer declares three boxes as one item and types all three numbers into one field, separated by commas and inconsistent spaces. A customer declares a number, the declaration gets rejected, and that number stays locked, so when they re-declare it correctly, the system says it's already taken. Two customers declare the same number, one of them by mistake, and whoever scans first wins.
Each of those looked like a separate defect. They were one design flaw wearing four hats.
A date rail with three markers. In June 2021 boxes scan as undeclared. Across June and July 2021 matching is rebuilt as identity resolution and a claims queue is added. In August 2021 duplicate-scan warnings and bulk claim approval arrive.
So we stopped matching text and started resolving identity.
Tracking numbers are now normalised before anything else happens: trimmed, case-flattened, and split apart when a single field holds several. A lookup searches for a number anywhere inside a declared list, not just as an exact match on the whole field. Numbers held by rejected declarations are released instead of held hostage. Numbers already consumed into a dispatched parcel drop out of the pool, so they can't be claimed twice.
And a number that turns up with a plausible owner but no clean match no longer fails silently. It goes into a pending claims queue, where staff approve or reject it, one at a time or in bulk.
Then we made the scan itself defensive.
Scanning the same number twice no longer overwrites anything. It shows a red banner naming the exact date and time it was scanned before, and who scanned it.
What came out of it is the shape of the whole product
- Ambiguity became a workflow instead of a failure. Unmatched, duplicate and disputed tracking numbers each have a named queue and an approval path. Nothing lands in a silent state.
- Exceptions got treated as the real product. Most of the system isn't the happy path. It's undeclared arrivals, rejected payments, returned consignments, count discrepancies and claims.
- Nothing gets deleted to make a problem go away. Rejected declarations, rejected payments and lost-and-found items are kept with a reason, a timestamp and a name attached.
The system never tells a warehouse worker something it isn't sure about. When it doesn't know, it says so and gives them a queue.
What we built
Three chapters in 2021, and the floor drove the work.
Chapter one · May to June 2021 · Everything hangs off it
The spine
Declaration, receiving, shelving and consolidation came first, because everything else hangs off them. Customers register what's coming before it arrives (tracking number, product, quantity, declared value and a photo) against one of the overseas warehouse addresses. Staff scan it in, and the system reconciles the count against what was declared.
Reference data was normalised into managed tables from day one instead of being retrofitted. That is why shipping routes, couriers, countries and warehouses became configuration instead of code.
Chapter two · July 2021 · Resale consignment
The second business, on the same rails
Consignment to resale marketplaces was added as a module sharing the existing warehouse, shelving, payment and dispatch machinery. Sneakers carry an SKU, a retail price and US, UK and EU sizes. They move through their own status pipeline (in review, awaiting quotation, waiting to be sold, picked up, or rejected by the platform and awaiting return) while sitting on exactly the same shelf as ordinary freight.
A faulty-item dispute path with photographic evidence was built alongside it, because a resale marketplace rejecting a pair of shoes is a normal Tuesday, not an edge case.
One warehouse shelf in the middle with two lifecycles running out of it. Forwarding goes consolidate, weigh, price, pay and ship. Consignment goes list, quote, sell and pick up, or is rejected and awaits return.
The same month brought the commercial layer.
Promo codes that discount shipping fees, service fees, miscellaneous fees or the total. Batched payments, so a customer settles a dozen parcels in one transaction instead of a dozen. And a new Region Manager permission tier, scoped deliberately to give a senior operations person the entire floor and every system panel while withholding user administration.
Chapter three · August to December 2021 · Watching people use it
The floor drove the work
Almost everything after July came from watching people use it. The receiving bench got a remembered default warehouse, because staff were re-selecting the same one all day. Warehouse inventory started showing the full address instead of a label, because pickers were cross-referencing another screen to get it. The search component was replaced outright when its matching proved too slow against the number of tracking numbers staff were actually looking up, and then tuned again a day later so it matched anywhere in a string instead of only at the start.
Customer reviews arrived in August with photographs, likes, comments and a moderation queue. They publish straight to the public marketing site, because the site reads the same database. There is no copy-paste step, because there's nothing to copy.
And a rename, mid-build.
In June the client changed the company's name. Because the brand was content rather than code, it was a content change. The platform as delivered is consistent about it throughout, which is the clearest evidence that "configuration over code" wasn't a slogan on this project.
Technical deep-dive · the receiving bench
One scan, three worlds
The single hardest moment in this business is the second a box hits the receiving bench. Everything upstream and downstream depends on getting it right, and the person doing it is standing up, holding a parcel, with a queue behind them.
The problem is that a tracking number could belong to three completely different things. It could be declared freight waiting to be forwarded. It could be a sneaker consigned for resale, which lives in a different table with a different lifecycle. Or it could be something nobody declared at all. The scanner can't tell the difference, and neither can the person holding the box.
So a single scan runs a resolution cascade across all three, in order.
One tracking number falls through three checks in order. Declared freight resolves the owner and route. A consignment listing advances to waiting to be sold. Lost and found surfaces the existing record, flagged. A number that matches none of them goes to the claims queue, so no path ends in nothing.
Declared freight
First, declared freight.
The number is matched against every declared item: as an exact match, as a member of a comma-separated list, and case-insensitively, with rejected declarations excluded from the search so their numbers don't block a re-declaration. If it hits, the system pulls the owning declaration, its shipping route and its customer.
Consignment
Second, consignment.
If it isn't declared freight, the same normalised search runs against resale listings. If it hits, the listing's status advances automatically to waiting for the item to be sold, because physical arrival is what makes that transition true.
Lost and found
Third, lost and found.
If it isn't either, the system checks whether it has been logged before as an undeclared arrival. If it has, the scan surfaces the existing record, its shelf and its batch, flagged in red. If it hasn't, staff can confirm it into the undeclared queue on the spot, creating a shelf location or a batch inline if the one they need doesn't exist yet, without leaving the screen.
One write
Whatever it resolves to, one write closes the loop.
A receiving record is created against the customer, the warehouse, the declaration or listing, the shelf, the batch, the timestamp and the member of staff who scanned it. If a record already exists, nothing is overwritten: the screen shows when it was scanned before, and by whom.
Then the screen answers the questions a person standing at a bench actually has.
All of it comes back in one response, in place, without a page reload, on a shared warehouse terminal:
- a photo of what should be in the box
- the customer's short code
- declared or undeclared
- its route
- counted against declared
- its shelf
- its batch
- an editable count
The photograph comes from the shared SKU image library. The count is editable because the number on the box and the number in the box are not always the same, and that discrepancy needs to surface at the receiving door instead of at dispatch.
The scan isn't a lookup. It's the moment the business decides what it's holding.
How we work
Seven rules, starting with the operation, not the screens.
Model the operation, not the screens
Before any interface work we mapped the physical journey of a parcel across two warehouses and three outbound routes, and built the data around chain of custody. The screens came after. That's why adding a second revenue line later was a module instead of a rebuild.
Ship working modules, not one big reveal
Declarations and receiving first, then consignment, then promo codes and batched payments, then a new staff permission tier, then customer reviews. Each landed as a usable piece the client could put in front of real staff that week.
Let the floor drive the fixes
A large share of the work after launch came from watching people use it: a remembered default at the receiving bench, full addresses in inventory, a search component replaced outright when it couldn't keep up with warehouse lookup volumes. None of that comes out of a requirements document. It comes out of standing next to the work.
Treat the exceptions as the real product
Rejections carry written reasons the customer reads. Duplicate scans warn instead of overwriting. Count discrepancies surface at receiving. Rejected payments are archived with a reason instead of deleted. The happy path is the easy part.
Settings, not releases
Routes, couriers, countries, currencies, warehouses, product types, shelf locations, statuses, bank accounts, promo codes, banners and legal pages are all editable data. The status pipeline itself is data: the client can add a status without calling us.
Log everything, because goods and money are both moving
Every page view and every significant action is written to an audit trail with the user, the time, the IP address, the operating system and the browser. In a business where a customer and a warehouse worker never meet, that log is what disputes get settled with.
Build for handover from day one
A system that needs an engineer for every change isn't finished. Sixteen panels put the commercial reality of the business in the client's own hands.
Technologies used
Eight layers, chosen for a warehouse in 2021.
Application
Deliberately few moving parts: no build step, no asset pipeline, nine dependencies in total. A small team can read the whole thing.
- PHP
- Custom MVC
- FastRoute
Data
One schema serving the customer portal, the warehouse floor and the public website. Nothing to sync, because there's only one copy.
- MySQL
- 43 tables
- Shared prepared-statement layer
Interface
264 single-purpose endpoints mean a warehouse screen updates in place instead of reloading after every scan.
- Server-rendered views
- Bootstrap 4
- jQuery
Payments
Card details never touch our servers. The payment is re-fetched from the provider and verified on our server before a parcel is released, never trusted from the browser.
- Hosted card checkout
Verification
A one-time passcode at sign-up and whenever a customer changes their phone number. A status alert is only useful if it reaches the right handset. The passcode service has since been retired by its vendor.
- Phone one-time passcode
Notifications
Staff see a new declaration or a submitted receipt appear without refreshing. Customers get alerted in their own language.
- Real-time push
- Transactional SMS
- Email delivery
Currency
Rates are stored per currency with the time they were fetched, so a price quoted months ago stays explicable.
- Exchange-rate service
Localisation
Four languages, English among them.
- Built-in language layer
Automated testing
Automated tests check each change before it reaches production.
Built in 2021, with the front-end libraries of 2021: Bootstrap 4 and jQuery. They were what the brief, the budget and the hardware called for then, because a warehouse running shared terminals needed pages that rendered on the server and updated without a reload. They are not what anyone would choose today, and our more recent case studies show what we build on now.
Integrations, by what they do
- Hosted card-checkout provider
- Transactional SMS
- Phone one-time-passcode service
- Real-time push service
- Exchange-rate service
- Email delivery provider
Two of the six have since changed hands or been retired.
How the platform works
Eight steps, from a sign-up to a parcel shipped or sold.
Step 1
Sign up and get an address
A customer registers, verifies their phone by SMS code, and receives a personal short code (the company's two initials followed by six random letters) plus warehouse addresses in several countries. That short code, not their name, is what travels with their goods.
Step 2
Declare before it arrives
They shop anywhere, ship to the warehouse address, and register the parcel in the portal: tracking number, product, quantity, declared value and a photo. SKU entry autocompletes from a shared image library, so the same product is named the same way every time. Staff approve it, correct it, or return it with a written reason the customer reads, and every declaration carries its own message thread, so the conversation about a parcel is attached to the parcel.
Step 3
Receive and shelve
Staff scan. The system resolves the owner across declared freight, consignment listings and lost-and-found, counts the item against the declaration, flags anything scanned before, and assigns a shelf and a batch. Undeclared arrivals go to a claims queue instead of a pile in the corner.
Step 4
See the shelf, both sides
Held goods are visible to the customer and to the floor at once, with shelf locations and batches that travel with items through consolidation, so picking is a lookup, not a search. Items consumed into a parcel leave the available pool automatically.
Step 5
Consolidate, price, request payment
Staff group held items into an outbound parcel, weigh it, set its route and issue a priced payment request: shipping fee, service fee and miscellaneous fees, in the currency the customer pays in.
Step 6
Settle
Card via hosted checkout, or bank transfer with a receipt upload that staff approve or return with a reason. One parcel or a dozen at once. Promo codes validate and apply at checkout: a percentage off, or a flat amount converted into the payment currency at the stored rate and split evenly across a batch. Rejected payments are archived with a timestamp and a reason and superseded by the next attempt, never deleted.
Step 7
Ship, or sell
The parcel moves through its status pipeline (repacking, scheduled, shipped, waiting for pickup, completed) and the customer is notified at each handover. Or, if it was consigned, it moves through the resale pipeline instead: in review, quoted, waiting to sell, picked up, or rejected by the platform and awaiting return.
Step 8
Review
Customers leave a rated review with photographs. Staff moderate it. Approved reviews appear on the public marketing site directly: same database, no republishing step.
Behind all of it
Four permission tiers (member, retail staff, region manager and super admin) decide who reaches which screen, checked centrally on every single one. Sixteen panels cover routes, couriers, countries, currencies, warehouses, product types, shelf locations, statuses, bank accounts, promo codes, announcements, banners, SKU images and legal pages. And every page view and every action lands in an audit trail.
Results
What the business can do now that it couldn't before.
Know what it's holding
Ownership, count, shelf, batch and status resolve in one scan, across freight and consignment at the same time. The warehouse stopped guessing.
Stop losing parcels to ambiguity
Mixed-case, multi-value and duplicate tracking numbers resolve correctly. Anything genuinely ambiguous goes to a named claims queue instead of failing silently.
Handle what nobody declared
Undeclared arrivals have a workflow (logged, shelved, claimed, approved or rejected) instead of a corner of the warehouse.
Run two revenue lines on one shelf
Resale consignment shares receiving, shelving, payment and dispatch with forwarding. The business added a second product without a second system to reconcile.
Take money the way customers actually pay
Card or bank transfer, one parcel or a dozen at once, with promo codes and dated exchange rates, in the customer's own currency.
Answer "where is my parcel?" without asking anyone
Customers track their own goods from declaration to doorstep, and get notified at every handover.
Sell in four languages
Four languages ship in the product, with rejection alerts sent bilingually by SMS.
Change the business without changing the software
A new route, courier, warehouse, fee, promo code, status or legal page is configuration. Even the company's own name changed mid-project as a content edit.
Settle a dispute with evidence
Every page view and every action is logged with user, time, IP and device. Nothing is deleted to make a problem disappear.
Publish social proof automatically
Moderated customer reviews appear on the public site with no republishing step, because both read the same database.
The engineering, in numbers
- ~46,000 Lines of application code
- 330 Server endpoints: 66 screens, 264 single-purpose operations
- 43 Database tables
- 67 · 53 · 29 Page controllers · view templates · shared server components
- 16 Self-service configuration panels
- 4 Permission tiers, enforced per screen
- 4 Interface languages
- 6 External services integrated
- 2 · 3 · 2 Warehouses · outbound routes · revenue lines
- ~20 Freight statuses, plus consignment's own, all editable by the client
- 68 Commits across 34 active development days
Under the hood
Engineering decisions worth naming
Payments
Verified on our server, never in the browser
When a customer returns from the hosted checkout, the platform fetches the session directly from the payment provider and reads the amount, currency and payment status from that response. Nothing about the payment is taken from the URL the customer arrived on.
Money
Financial records are append-only in practice
A rejected payment isn't deleted or edited. It's marked archived with the time it was archived, and the next attempt is recorded beside it, so every attempt against a parcel stays queryable. That is the only version that survives a chargeback conversation.
Currency
Exchange rates are stored with their timestamp
Rates are fetched per currency and saved with the moment they were retrieved, so a quote issued in one month can still be explained in another. Flat-amount promo codes are converted through that stored rate at the time of payment, and split evenly when applied across a batch.
Access
Declared per screen, checked centrally
Every screen states which permission tiers may reach it, and the check runs in one place before the screen loads, not inside each handler. That's why a new Region Manager tier could be introduced mid-build, scoped to the entire operations floor minus user administration, without structural rework.
Database
Database access is parameterised
Queries run through a shared prepared-statement layer with bound values instead of assembled strings.
Identity
Hashed passwords, verified phones
Passwords are stored hashed with a per-user salt, and phone numbers are verified by one-time passcode at registration and on every change.
Honest status
This was a 2021 engagement, and the codebase is pinned to PHP 7. Twelve call sites use an argument order that PHP 8 removed, so it needs work before it runs on a currently supported PHP version, and PHP 7.4 lost security support in November 2022. The front-end libraries are from the same era.
Related work
Air Cargo & Customs Platform
Logistics · 2025 – 2026 · Specify, build, deploy
7 days — Empty repository to a live, documented API. Both sides signed the contract on it
Cross-Border Forwarding Platform
Logistics · 2018 – present · Build, run, maintain
8 years — Continuous development, one data-model rebuild, zero downtime
Next step
Running an operation on spreadsheets, a group chat and whoever is on shift?
We model the operation before we design the screens, treat the exceptions as the real product, and hand over a system your own team can change without calling us.
Client anonymised · every figure counted from the delivered code and its commit history · October 2026