Integrations
Shipping
Shipping Overview

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

AreaStatusNotes
Provider CRUDReadyCredentials validated against Shiprocket on create/update (shipping.ts:L113-L308)
Rate quoteReady/api/shipping/rates hits /courier/serviceability, caches results in ShippingRate for 24h
Shiprocket order creationReady/api/shipping/shipments/orders/create/adhoc
AWB assignmentReady/shipments/:id/generate-awb — but requires a courierId the FE must pick from rate response, no DB link
Pickup schedulingReady/shipments/:id/schedule-pickup
Label downloadReadyReturns Shiprocket-hosted labelUrl; in-house PDF via ShippingLabelTemplate is a separate path used by /api/documents
TrackingStubbed as pollingNo cron refresh, no webhook. trackingEvents is only updated when a user opens the shipment
ReturnsSchema + Shiprocket wrapper exist (shiprocket.ts:L243-L282, ShipmentReturn model)No route exposes return creation
CancellationShiprocket wrapper existsNo route exposes it
Checkout integrationNot builtCustomers pay order.shippingAmount entered by the seller; no rate lookup during checkout
Ready-to-ship workflowNot builtNo state for "packed / ready for pickup" distinct from "created"
Multi-carrierNot builtOnly ShippingCarrier.shiprocket has code; other enum values exist but are unreachable
Shiprocket inbound webhookNot builtSee webhooks-overview.md — polling only
Two services for the same thingBoth existbackend/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; credentials JSON holds the Shiprocket email/password (plaintext — see gotchas).
  • Shipment (schema.prisma:L693) — links to Order, carries awbNumber, trackingNumber, labelUrl, trackingEvents[], and a local ShipmentStatus.
  • ShippingRate (schema.prisma:L779) — serviceability cache, validUntil = now + 24h.
  • Order.shippingAmount (schema.prisma:L413) and Order.fulfillmentStatus (L410) are the only shipping-related fields on Order. Creating a Shipment flips fulfillmentStatus to partial (shipping.ts:L576-L579) — it is never flipped to fulfilled automatically.

5. Primary flow

6. Lifecycle

Transitions after pickup_scheduled only advance when a user triggers /track — see gotchas.

7. Key files

8. Env vars & config

Shiprocket credentials are per-tenant in the DB, not env vars. There are no SHIPROCKET_* env vars today.

ConfigWherePurposeWhat breaks
ShippingProvider.credentials.email / .passwordDB JSONLogin to Shiprocket per sellerRate/ship/track calls throw "Shiprocket authentication failed"
ShippingProvider.credentials.apiTokenDB JSONOptional pre-issued token; skips loginIf stale, you get 401s until cleared
SHIPROCKET_BASE_URLHardcoded apiv2.shiprocket.in/v1/externalNot overridable for sandbox/mocks

9. Gotchas & troubleshooting

  • Shiprocket credentials stored in plaintext. ShippingProvider.credentials is Json, 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 / getShiprocketToken helpers; backend/lib/shiprocket.ts has a richer ShiprocketService class 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_scheduleddelivered unless 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.gstAmount is not order GST. It's Shiprocket's tax on the shipping charge, cached with the rate. It must not be summed into Order.taxAmount — see tax-engine.md.
  • fulfillmentStatus never auto-completes. Creating a Shipment sets partial; no code path sets fulfilled when ShipmentStatus=delivered. Dashboards that pivot on fulfillmentStatus will under-count completed orders.
  • Hardcoded HSN 441122. Every Shiprocket line item ships with HSN 441122 (shipping.ts:L518). Fine for testing, wrong for real B2B invoicing.
  • Checkout has no rate lookup. Order.shippingAmount is whatever the seller entered; customers cannot pick a courier. Don't assume shippingAmount reflects the Shiprocket quote.
  • No ready-to-ship state. created in our enum means "Shiprocket order exists". There is no intermediate "packed" state — implement one before adding a warehouse workflow.
  • Shadow courierId on AWB generation. /shipments/:id/generate-awb requires body.courierId but the Shipment row 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, update Shipment.trackingEvents and status. Removes the polling gap.
  • Add a new carrier. Add a case in ShippingCarrier (already present), build a service class matching ShiprocketService's surface (rates, createOrder, assignAwb, label, track, cancel), branch in /api/shipping/rates + /shipments on provider.carrier. Consolidate helpers in backend/lib/ before doing this — do not fork the inline route helpers.
  • Auto-advance tracking. Add a scheduler that picks Shipment rows where status NOT IN (delivered, cancelled, returned) and lastTrackedAt < now - 6h, batch-calls /courier/track/awb/:awb.
  • Encrypt provider credentials. Wrap reads/writes of ShippingProvider.credentials with a KMS/crypto envelope; migrate existing rows.
  • Expose returns + cancellation. The Shiprocket wrapper already implements both; only routes are missing.

11. Related docs