- Iyabo Hair, Dakar
- Sole designer and engineer
- 2026

One order, three ledgers
Iyabo HairWhat is real?
Simulation built on the code of Iyabo Hair, fictional data.
- The rules for what is owed, paid and remaining, for what a courier holds and hands over, and for how a delivery status moves the product come from Iyabo Hair's code, consolidated and checked against the original calculation. The statuses and labels are the application's.
- The twenty orders, the three couriers, the products and the prices are invented. No client and no phone number.
- The “What if it were a single record?” switch is an explanation added by this site. It is not a feature of the application. The screen layout is also recomposed for the demo.

Iyabo Hair
The demo loads only when you ask for it.
The problem
Iyabo Hair sells wigs across Dakar. A wig can leave the shelf on Monday, be delivered on Wednesday and be paid for on Friday, partly, and sometimes to the courier instead of the shop. A single order status cannot tell those three stories apart.
Three ledgers
Every order carries three separate records: where the product is, how the delivery stands, and what has been paid. Changing the delivery status moves the product physically (with the courier, delivered, returned) but never touches the money. Recording a payment never changes a status.
Under the hood
The order has a status for the product, a status for the delivery, and no money field: money is worked out from typed payments.
// Les TROIS suivis indépendants productStatus ProductStatus @default(EN_STOCK) deliveryStatus DeliveryStatus @default(A_PREPARER) // Le suivi ARGENT est calculé à partir des Payment liés : // resteAPayer perruque = quantity * unitPrice - somme(Payment PERRUQUE) // resteAPayer livraison = deliveryFee - somme(Payment LIVRAISON) // Qui est prévu pour encaisser ce qui n'est pas encore payé // (détermine ce que le livreur doit récupérer à la livraison) perruqueEncaisseur PlannedCollector @default(LIVREUR) livraisonEncaisseur PlannedCollector @default(LIVREUR)Every franc has a place
A payment is either for the wig or for the delivery, never both, and it records who physically received it. That is how the app can answer where each franc is. A delivery cannot be closed with a silent unpaid balance: the courier confirms what was collected and how, or reports a problem.
Under the hood
Before a delivery is closed, the remaining amount of each track is worked out and any unpaid balance blocks it.
const payeP = order.payments .filter(function (p) { return p.type === "PERRUQUE"; }) .reduce(function (sum, p) { return sum + p.amount; }, 0); const payeL = order.payments .filter(function (p) { return p.type === "LIVRAISON"; }) .reduce(function (sum, p) { return sum + p.amount; }, 0); const restePerruque = order.quantity * order.unitPrice - payeP; const resteLivraison = order.deliveryFee - payeL; // Cohérence : si un montant restait dû et que le livreur ne // l'a pas reçu, il doit passer par "Problème" plutôt que // valider une livraison avec un impayé silencieux if (restePerruque > 0 && perruqueRecue !== "OUI") { return "La perruque n'est pas payée (" + restePerruque.toLocaleString("fr-FR") + " F). Encaisse-la ou signale un problème."; } if (resteLivraison > 0 && livraisonRecue !== "OUI") { return "La livraison n'est pas payée (" + resteLivraison.toLocaleString("fr-FR") + " F). Encaisse-la ou signale un problème."; }A prepaid fee is not owed yet
When a customer pays the delivery fee in advance, the shop holds that money, but it only becomes the courier's once the order is delivered. A failed or postponed delivery leaves it pending. A courier's cash handover is the money they hold minus the fees now owed to them.
Under the hood
The fees collected by the shop count as owed to the courier only on delivered orders; the others stay pending.
const livraisonsAcquises = await prisma.payment.findMany({ where: { type: "LIVRAISON", remiseId: null, collectedBy: { role: "ADMIN" }, order: { livreurId: livreurId, deliveryStatus: "LIVREE" }, }, }); const totalAcquises = livraisonsAcquises.reduce(function (sum, p) { return sum + p.amount; }, 0); const livraisonsEnAttente = await prisma.payment.findMany({ where: { type: "LIVRAISON", remiseId: null, collectedBy: { role: "ADMIN" }, order: { livreurId: livreurId, deliveryStatus: { not: "LIVREE" } }, }, }); const totalEnAttente = livraisonsEnAttente.reduce(function (sum, p) { return sum + p.amount; }, 0);Try it yourself
Open an order, change its status, record a payment, deliver it as the courier, then hand over a courier's cash and validate it as the shop. Switch on the question about a single record to see what one status would hide. That block is an illustration added by this site, not a feature of the application.
What you can do here
- Change the delivery status of twenty made-up orders
- Record a payment and see what is still owed
- Close a delivery as the courier
- Hand over cash and validate it as the manager
- See the product, delivery and money ledgers update separately
Stack
Next.js 16 · TypeScript · Tailwind CSS v4 · Prisma 7 · Neon Postgres · NextAuth v5 · Vercel
Context
Iyabo Hair sells wigs across Dakar. Orders arrive through social media, stock moves through a small team, and couriers deliver on motorbikes and get paid in cash or mobile money. Before this app, all of it lived in notebooks and chat threads. Nobody could answer two basic questions at the same time: what do we actually have, and who currently owes whom.
The problem
The owner did not need a shop. She needed to stop losing money in the gap between three things that move at different speeds: the product, the delivery, and the cash. A wig can leave the shelf on Monday, be delivered on Wednesday, and be paid for on Friday, sometimes partially, sometimes to the courier rather than the business. Any system that treats those as one event will quietly produce wrong numbers, and wrong numbers in this business are somebody's salary.
Constraints
- Staff work entirely from mid-range Android phones, often on unstable mobile data
- No point of sale, no card terminal, no accounting software to integrate with
- Payments arrive through Wave and Orange Money, or in cash to a courier
- Couriers are not employees and their pay is calculated per delivery
- The team had never used an internal tool before, so anything confusing would be abandoned within a week
What I built
- Order entry with stock deduction and per-item margin
- Delivery assignment, status tracking, and courier settlement
- Cash ledger reconciling mobile money, cash, and outstanding balances
- Role-based access for the owner, staff administrators, and couriers
- Scannable Wave and Orange Money payment codes attached to each order
- Installable progressive web app, so staff open it from the home screen like any other app
Key decisions
Three ledgers that never write to each other
The obvious design is one order record that updates stock, delivery, and cash together. I refused it. Product, delivery, and money are tracked as three independent flows, and no action in one automatically mutates another. Marking a delivery complete does not settle the money. Recording a payment does not close the delivery. It costs a few extra taps, but it means a mistake stays local instead of silently corrupting the other two. In a business where the numbers decide who gets paid, a wrong figure is far more expensive than an extra tap.
Delivery fees are never the company's money
The delivery fee belongs to the courier and the wig price belongs to the business. The two are separated at the point of entry and never merged anywhere in the system, including in reports. This sounds trivial until a customer pays one lump sum through a single mobile money transfer. Modelling it as one amount to be split later is how businesses end up spending their couriers' wages without noticing, then arguing about it at the end of the month.
Prepaid deliveries do not count until they happen
When a customer pays in advance, the courier's fee is recorded but only counts toward what they are owed once the delivery is actually completed. The alternative, crediting on payment, makes the courier balance look right on the dashboard while being wrong in reality, and reverses badly on returns and failed deliveries. This one rule removed an entire category of end-of-week disputes.
Results
- Replaced notebooks and chat threads with a single source of truth
- Installed on staff phones as a home screen app, no store distribution needed