Shipping Overview
How Eziseller ships orders today: one integration (Shiprocket), a multi-step manual workflow from rate quote to label to polled tracking. Audience: new dev with Node/Express experience. Status: partially built — read section 3 before relying on anything here.
1. Overview
Shipping in Eziseller is a seller-initiated, dashboard-driven workflow on top of Shiprocket's REST API. Sellers store per-tenant Shiprocket credentials, quote rates for a pincode pair, create a Shiprocket "ad-hoc" order, generate an AWB (Air Waybill), schedule pickup, download a label URL, and then poll Shiprocket for tracking updates. It is not wired into customer checkout — catalog checkout charges a flat shippingAmount from the order and does not call /api/shipping/rates. The label template for in-house PDF labels (shipping-label-template.ts) is separate from Shiprocket's hosted label URL. Only Shiprocket is implemented; the ShippingCarrier enum lists 8 other carriers but none have code paths.
2. Architecture
Every shipping action is an inline request from the dashboard. There is no scheduler, queue, or webhook — tracking only refreshes when a user opens a shipment and hits the track endpoint (shipping.ts:L728-L787).
3. What's ready vs stubbed
| Area | Status | Notes |
|---|---|---|
| Provider CRUD | Ready | Credentials validated against Shiprocket on create/update (shipping.ts:L113-L308) |
| Rate quote | Ready | /api/shipping/rates hits /courier/serviceability, caches results in ShippingRate for 24h |
| Shiprocket order creation | Ready | /api/shipping/shipments → /orders/create/adhoc |
| AWB assignment | Ready | /shipments/:id/generate-awb — but requires a courierId the FE must pick from rate response, no DB link |
| Pickup scheduling | Ready | /shipments/:id/schedule-pickup |
| Label download | Ready | Returns Shiprocket-hosted labelUrl; in-house PDF via ShippingLabelTemplate is a separate path used by /api/documents |
| Tracking | Stubbed as polling | No cron refresh, no webhook. trackingEvents is only updated when a user opens the shipment |
| Returns | Schema + Shiprocket wrapper exist (shiprocket.ts:L243-L282, ShipmentReturn model) | No route exposes return creation |
| Cancellation | Shiprocket wrapper exists | No route exposes it |
| Checkout integration | Not built | Customers pay order.shippingAmount entered by the seller; no rate lookup during checkout |
| Ready-to-ship workflow | Not built | No state for "packed / ready for pickup" distinct from "created" |
| Multi-carrier | Not built | Only ShippingCarrier.shiprocket has code; other enum values exist but are unreachable |
| Shiprocket inbound webhook | Not built | See webhooks-overview.md — polling only |
| Two services for the same thing | Both exist | backend/lib/shiprocket.ts (class wrapper, unused by routes) and inline helpers in backend/routes/shipping.ts (actually used). The class is dead code today. |
4. Data model
ShippingProvider(schema.prisma:L673) — one row per carrier per user;credentialsJSON holds the Shiprocket email/password (plaintext — see gotchas).Shipment(schema.prisma:L693) — links toOrder, carriesawbNumber,trackingNumber,labelUrl,trackingEvents[], and a localShipmentStatus.ShippingRate(schema.prisma:L779) — serviceability cache,validUntil = now + 24h.Order.shippingAmount(schema.prisma:L413) andOrder.fulfillmentStatus(L410) are the only shipping-related fields on Order. Creating a Shipment flipsfulfillmentStatustopartial(shipping.ts:L576-L579) — it is never flipped tofulfilledautomatically.
5. Primary flow
6. Lifecycle
Transitions after pickup_scheduled only advance when a user triggers /track — see gotchas.
7. Key files
- backend/routes/shipping.ts:L76-L110 — token + API helpers (the path actually used)
- backend/routes/shipping.ts:L311-L397 — rate quote + cache write
- backend/routes/shipping.ts:L465-L589 — shipment creation +
fulfillmentStatus='partial' - backend/routes/shipping.ts:L592-L668 — AWB assignment
- backend/routes/shipping.ts:L728-L787 — tracking (poll)
- backend/routes/shipping.ts:L847-L859 — Shiprocket →
ShipmentStatusmapping - backend/lib/shiprocket.ts — class-based wrapper, not currently wired to routes (dead code)
- backend/templates/shipping-label-template.ts:L1-L30 — in-house 4x6 label PDF (separate from Shiprocket's label URL)
- backend/prisma/schema.prisma:L673-L810 — ShippingProvider, Shipment, ShipmentReturn, ShippingRate
- src/app/dashboard/shipping/page.tsx, src/components/shipping/ — FE entry points
8. Env vars & config
Shiprocket credentials are per-tenant in the DB, not env vars. There are no SHIPROCKET_* env vars today.
| Config | Where | Purpose | What breaks |
|---|---|---|---|
ShippingProvider.credentials.email / .password | DB JSON | Login to Shiprocket per seller | Rate/ship/track calls throw "Shiprocket authentication failed" |
ShippingProvider.credentials.apiToken | DB JSON | Optional pre-issued token; skips login | If stale, you get 401s until cleared |
SHIPROCKET_BASE_URL | Hardcoded apiv2.shiprocket.in/v1/external | — | Not overridable for sandbox/mocks |
9. Gotchas & troubleshooting
- Shiprocket credentials stored in plaintext.
ShippingProvider.credentialsisJson, no encryption despite the schema comment saying "Encrypted" (schema.prisma:L680). Rotate via dashboard, do not dump the table. - Two parallel implementations. Routes use inline
shiprocketAPI/getShiprocketTokenhelpers;backend/lib/shiprocket.tshas a richerShiprocketServiceclass that no route imports. Don't "fix" a bug in one without checking the other, and prefer consolidating on the class before adding features. - Token caching differs. The class caches tokens for 9 days on the instance (shiprocket.ts:L134-L163); the route helper re-authenticates on every call. Extra calls + rate-limit risk if Shiprocket tightens login throttling.
- Tracking is lazy-pull only. Nothing advances a shipment from
pickup_scheduled→deliveredunless a human opens the UI. Delivered orders sit stale. If auto-refresh matters, add a scheduler entry (see cron-and-jobs.md). - No Shiprocket webhook. Referenced in webhooks-overview.md:L48. Shiprocket does offer push tracking in their dashboard — we haven't wired a receiver.
ShippingRate.gstAmountis not order GST. It's Shiprocket's tax on the shipping charge, cached with the rate. It must not be summed intoOrder.taxAmount— see tax-engine.md.fulfillmentStatusnever auto-completes. Creating a Shipment setspartial; no code path setsfulfilledwhenShipmentStatus=delivered. Dashboards that pivot onfulfillmentStatuswill under-count completed orders.- Hardcoded HSN
441122. Every Shiprocket line item ships with HSN441122(shipping.ts:L518). Fine for testing, wrong for real B2B invoicing. - Checkout has no rate lookup.
Order.shippingAmountis whatever the seller entered; customers cannot pick a courier. Don't assumeshippingAmountreflects the Shiprocket quote. - No ready-to-ship state.
createdin our enum means "Shiprocket order exists". There is no intermediate "packed" state — implement one before adding a warehouse workflow. - Shadow
courierIdon AWB generation./shipments/:id/generate-awbrequiresbody.courierIdbut theShipmentrow doesn't persist which courier was selected from the rate response. The FE must hold that ID between steps.
10. Extension points
- Add a Shiprocket webhook. Mirror the Meta pattern in webhooks-overview.md: raw-body route outside
/api, HMAC verify, updateShipment.trackingEventsandstatus. Removes the polling gap. - Add a new carrier. Add a case in
ShippingCarrier(already present), build a service class matchingShiprocketService's surface (rates, createOrder, assignAwb, label, track, cancel), branch in/api/shipping/rates+/shipmentsonprovider.carrier. Consolidate helpers inbackend/lib/before doing this — do not fork the inline route helpers. - Auto-advance tracking. Add a scheduler that picks
Shipmentrows wherestatus NOT IN (delivered, cancelled, returned)andlastTrackedAt < now - 6h, batch-calls/courier/track/awb/:awb. - Encrypt provider credentials. Wrap reads/writes of
ShippingProvider.credentialswith a KMS/cryptoenvelope; migrate existing rows. - Expose returns + cancellation. The Shiprocket wrapper already implements both; only routes are missing.
11. Related docs
- ../../01-core/orders/order-lifecycle.md — where
fulfillmentStatusfits in the order state machine - ../../01-core/tax-and-invoices/tax-engine.md — why
ShippingRate.gstAmountis not order tax - ../webhooks/webhooks-overview.md — inbound webhook catalog; Shiprocket is explicitly absent
- ../../06-ops/cron-and-jobs.md — where an auto-track scheduler would live