LastMile IQ
A delivery platform that quotes rates, assigns drivers automatically and keeps an append-only history of every order.
- Role
- Solo project: schema, API, business logic, UI and deployment.
- Stack
-
- Next.js 16
- TypeScript
- PostgreSQL
- Prisma
- JWT
- bcrypt
- Tailwind CSS
- Vercel
The problem
Last-mile pricing depends on how big a parcel is, not only how heavy. Assigning a driver means trading distance against how busy each driver already is. And every change to an order needs a record nobody can quietly edit later.
I built the backend that handles all three, with an admin, customer and delivery-agent view on top so the rules can be seen working.
How it's built
Next.js App Router serves both the pages and the API. Route handlers check the JWT cookie and the caller's role, then hand off to plain TypeScript services that own the business rules. Prisma is the only thing that talks to PostgreSQL.
Key decisions
Bill on whichever is heavier: the scale or the box
Couriers charge for the space a parcel takes up. The rate engine computes volumetric weight as length × width × height / 5000, bills the higher of that and the actual weight, rounds extra kilograms up and never goes below the card's minimum charge.
Rates live in the database as cards keyed by customer type (B2B or B2C) and route (same zone or cross zone), so admins change prices without a deploy. Cash-on-delivery adds a surcharge that is either a fixed fee or a percentage of the declared value, with a floor.
const volumetricKg = (lengthCm * widthCm * heightCm) / 5000;
const chargeableKg = Math.max(actualWeightKg, volumetricKg);
const extraKg = Math.max(0, chargeableKg - rateCard.baseWeightKg);
const shippingCharge = Math.max(
rateCard.minCharge,
rateCard.baseRate + Math.ceil(extraKg) * rateCard.perExtraKgRate,
); Score drivers instead of picking the nearest one
The nearest driver is often the busiest. Assignment first drops anyone at capacity, then scores the rest: great-circle distance to the pickup (Haversine), plus 25 km if the driver is outside the pickup zone, plus 2 km for every delivery they already hold. The lowest score gets the order.
Admins can override the choice by hand, and the override moves the load counters between drivers so capacity stays accurate.
const eligible = agents.filter((a) => a.activeDeliveries < a.maxCapacity);
const scored = eligible.map((agent) => {
const distanceKm = haversineKm(agent, pickup);
const zonePenalty = agent.currentZoneId === order.pickupZoneId ? 0 : 25;
const workloadPenalty = agent.activeDeliveries * 2;
return { agent, score: distanceKm + zonePenalty + workloadPenalty };
});
scored.sort((a, b) => a.score - b.score); // lowest score wins Make order history append-only
Status changes go through one lifecycle service. It checks the move against a table of allowed transitions, stops agents from touching orders that aren't theirs, and writes a tracking event with the actor, their role, a note and coordinates. No route updates or deletes those events.
A failed delivery lets the customer pick a new date, which creates a reschedule record and runs assignment again.
Results
- route handlers over a 9-model schema
- 16
- test assertions on pricing and the order lifecycle
- 17
- seeded demo accounts across 3 roles
- 6
Deployed on Vercel with a one-click role switcher, so anyone can try the admin, customer and agent views without signing up.
Worked example from the README: a 25 × 20 × 15 cm parcel weighing 1.2 kg is billed at its 1.5 kg volumetric weight.