Shipping API Reference
Complete API contracts for the Shipping module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Shipping - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin coverage/rate endpoints and the customer quote endpoint owned by the Shipping module.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | apps/api/src/modules/shipping/admin/shipping-admin.controller.ts, customer/shipping-quote-customer.controller.ts | Routes, methods, guards, permissions, status codes |
| DTOs | dto/*.ts | Validation, defaults |
| Services | shipping-admin.service.ts, shipping-admin-bulk.service.ts, shipping-quote-customer.service.ts | Behavior, side effects, errors |
| Schema | packages/db/src/schema/shipping/shipping-rate.ts | Unique district, fee type |
| Error registry | apps/api/src/common/types/error-codes.ts (// SHIPPING) | SHIPPING_* codes |
2. Module Summary
| Field | Value |
|---|---|
| Module name | shipping |
| Module slug | shipping |
| Primary actors | admin, customer (signed in) |
| API surfaces | admin, mobile |
| Base route prefixes | /api/admin/shipping, /api/mobile/shipping |
| Auth model | JwtAuthGuard + RoleGuard (admin); JwtAuthGuard (quote) |
| Persistence | PostgreSQL (shipping_rate), Redis (shipping cache domain) |
| Runtime source of truth | shipping_rate rows |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
fee | Delivery fee in integer minor units (NPR 120.00 = 12000); 0 = free | schema | All rate payloads |
districtId | The district a quote/rate is keyed on — never an address id | controllers | Quote, rates |
configured | A rate row exists for the district | service | Coverage list |
active | is_active flag — pause keeps the fee | service | Coverage list, quote |
serviceable | configured && active | service | Coverage, quote, address status |
unconfigured | No rate row | service | Coverage filter |
4. API Surface Map
| Surface | Method | Path | Actor | Auth/Guard | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|---|
| Admin | GET | /api/admin/shipping/districts | Admin | JWT+Role | Shipping_READ | ShippingAdminController | Coverage over all 77 districts |
| Admin | GET | /api/admin/shipping/districts/:districtId/rate | Admin | JWT+Role | Shipping_READ | same | One district's rate |
| Admin | PUT | /api/admin/shipping/districts/:districtId/rate | Admin | JWT+Role | Shipping_UPDATE | same | Upsert rate |
| Admin | DELETE | /api/admin/shipping/districts/:districtId/rate | Admin | JWT+Role | Shipping_DELETE | same | Discard rate (200) |
| Admin | POST | /api/admin/shipping/rates/bulk | Admin | JWT+Role | Shipping_UPDATE | same | Bulk price (200) |
| Mobile | GET | /api/mobile/shipping/quote | Customer | JWT | — | ShippingQuoteCustomerController | Quote a district |
Permission note: the Shipping_* permissions derive from the shared permission catalog (packages/db/src/authorization/permission-catalog.ts); the seed grants them to admin like every non-System module, and superadmin bypasses the check.
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Admin | JwtAuthGuard, RoleGuard, IpThrottlerGuard | req.user | Shipping_READ / Shipping_UPDATE / Shipping_DELETE | No | Superadmin bypasses |
| Quote | JwtAuthGuard, IpThrottlerGuard | req.user | — | No | CUSTOMER_READ 60/min keyed on the account |
Rate limits: ADMIN_READ 30/min, ADMIN_WRITE 10/min, ADMIN_BULK_WRITE 5/min (one request can reprice all 77 districts — submit once, not per district), CUSTOMER_READ 60/min (account-keyed).
6. DTO and Model Reference
6.1 UpsertShippingRateDto (body of PUT)
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
fee | number | Yes | N/A | @IsInt, >= 0 — minor units | 12000 | upsert-shipping-rate.dto.ts |
isActive | boolean | Yes | N/A | @IsBoolean | true |
6.2 BulkShippingRateDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
scope.districtIds | string[] | one scope key required | UUID v7 each | Explicit districts |
scope.provinceIds | string[] | — | UUID v7 each | Whole provinces |
scope.allDistricts | boolean | — | — | All of Nepal |
fee | number | one of fee/isActive required | >= 0 minor units | Omitting = status-only |
isActive | boolean | — | — |
Scopes union and de-duplicate.
6.3 Query DTOs
ListShippingDistrictsQueryDto: provinceId (UUID), status (active/inactive/unconfigured), pricing (free/paid), search (name), + QueryDto base (pagination, page, size). ShippingQuoteQueryDto: districtId (UUID v7, required).
6.4 Response DTOs
ShippingRateResponseDto:
{
"district": { "id": "0198…", "code": "kathmandu", "name": "Kathmandu",
"province": { "id": "0198…", "code": "bagmati", "name": "Bagmati" } },
"pricing": { "fee": 12000, "currency": "NPR" },
"availability": { "serviceable": true, "active": true, "configured": true },
"timestamps": { "createdAt": "…", "updatedAt": "…" }
}ShippingQuoteResponseDto: same minus configured (customer does not see it). BulkShippingRateResultDto: { requested, created, updated, skipped, skippedDistricts: [{ id, code, reason }] }.
7. Enum Reference
None — district status is derived, not stored.
8. Endpoint Reference
8.1 GET /api/admin/shipping/districts
Purpose
The admin coverage screen: all 77 districts with their rate attached, whether configured or not. Lists districts, not rate rows — an unconfigured district has no rate row, and "which districts are unavailable" is the question.
Auth and Permissions
JwtAuthGuard, RoleGuard, IpThrottlerGuard; Shipping_READ; ADMIN_READ 30/min.
Request
| Part | Required | Details |
|---|---|---|
| Query | No | provinceId, status (active/inactive/unconfigured), pricing (free/paid), search, pagination, page, size |
Response
200 — array of ShippingRateResponseDto. Unconfigured districts have configured: false, active: false, serviceable: false, fee: null.
Error Cases
None (unknown provinceId → empty or 404 per service; the contract surfaces districts regardless).
8.2 GET /api/admin/shipping/districts/:districtId/rate
200 with one rate response. 404 SHIPPING_DISTRICT_NOT_FOUND for a uuid that names no district.
8.3 PUT /api/admin/shipping/districts/:districtId/rate
Purpose
Set a district's delivery fee. Upsert — district_id is unique, so a second call replaces the first (never a 409). isActive: false pauses delivery while keeping the fee.
Auth and Permissions
Shipping_UPDATE; ADMIN_WRITE 10/min.
Request
{ "fee": 12000, "isActive": true }fee is minor units: NPR 120.00 = 12000. 0 is free delivery.
Response
200 — rate response.
Side Effects
shipping_rate upsert; activity record; shipping cache domain invalidated.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | SHIPPING_DISTRICT_NOT_FOUND | Unknown district |
| 400 | SHIPPING_FEE_NEGATIVE | fee < 0 |
| 403 | — | Role without Shipping_UPDATE (permission catalog not seeded) |
8.4 DELETE /api/admin/shipping/districts/:districtId/rate
Purpose
Discard the configuration entirely — the district returns to unconfigured. To stop delivering while keeping the price, PUT with isActive: false instead.
Auth and Permissions
Shipping_DELETE; ADMIN_WRITE 10/min.
Response
200 message-only.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | SHIPPING_RATE_NOT_FOUND | No rate row for the district |
8.5 POST /api/admin/shipping/rates/bulk
Purpose
Set one fee and/or status across many districts at once: explicit districts, whole provinces, or all of Nepal (scopes union, de-duplicated). All-or-nothing — an unknown id rejects the entire request with nothing written.
Auth and Permissions
Shipping_UPDATE; ADMIN_BULK_WRITE 5/min (blast radius, not request count).
Request
{ "scope": { "districtIds": ["0198…"], "provinceIds": ["0198…"], "allDistricts": false },
"fee": 15000, "isActive": true }At least one scope key and one of fee/isActive required.
Response
200 — a report, not a bare success:
{
"message": "Shipping rates updated successfully",
"errorCode": null,
"data": { "requested": 77, "created": 60, "updated": 14, "skipped": 3,
"skippedDistricts": [ { "id": "0198…", "code": "humla",
"reason": "unconfigured_and_no_fee_supplied" } ] }
}Show the skipped list — a status-only bulk cannot create a configuration, and a bare "success" would hide that the request did not do what the admin asked.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | SHIPPING_BULK_SCOPE_EMPTY | Scope resolved to zero districts |
| 400 | SHIPPING_BULK_NO_CHANGE_REQUESTED | Neither fee nor isActive sent |
| 409 | SHIPPING_BULK_LIMIT_EXCEEDED | More than 77 distinct districts (cannot happen from a correct client) |
| 400/404 | district/province not found | Whole request rejected, nothing written |
There is deliberately no bulk delete — use isActive: false to stop delivering while keeping every configured fee.
8.6 GET /api/mobile/shipping/quote
Purpose
Delivery cost and availability for a district. Takes a districtId, not an addressId — read location.district.id off the address you already have; shipping deliberately knows nothing about the address table.
Auth and Permissions
JwtAuthGuard; CUSTOMER_READ 60/min keyed on the account.
Request
?districtId=<uuid>
Response
200 — quote response. An unserviceable district is a 200, not a 404:
{ "message": "Shipping quote fetched successfully", "errorCode": null,
"data": { "district": { "id": "0198…", "code": "kathmandu", "name": "Kathmandu",
"province": { "id": "0198…", "code": "bagmati", "name": "Bagmati" } },
"pricing": { "fee": 12000, "currency": "NPR" },
"availability": { "serviceable": true, "active": true } } }Unserved: { "serviceable": false, "fee": null } — branch on availability.serviceable, never on the status code.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | SHIPPING_DISTRICT_NOT_FOUND | districtId names nothing |
9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence (quote)
9.3 Error Branch (bulk)
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
GET /admin/shipping/districts | offset (opt-in) | 20 | 100 | district name (fixed) | provinceId, status, pricing, search | 77 |
GET /mobile/shipping/quote | none | — | — | — | districtId | 1 |
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details |
|---|---|---|
| Redis cache | Yes | shipping cache domain — serviceability behind one key; invalidated on every admin write |
| BullMQ | No | — |
| External API | No | — |
13. Mandatory Deep API Documentation Pack
13.1 Route-by-Route Completeness Matrix
| Route | Controller Method | DTOs | Service Method | Guards | Permissions | Cache | Jobs | DB Touches | Errors | Documented? |
|---|---|---|---|---|---|---|---|---|---|---|
GET /admin/shipping/districts | listDistricts | ListShippingDistrictsQueryDto | ShippingAdminService.listDistricts | JWT+Role+IpThrottle | Shipping_READ | — | — | district, rate | — | Yes |
GET /districts/:districtId/rate | getRate | ShippingDistrictParamsDto | …getDistrictRate | same | Shipping_READ | — | — | district, rate | 404 | Yes |
PUT /districts/:districtId/rate | upsertRate | UpsertShippingRateDto | …upsertRate | same | Shipping_UPDATE | invalidate | — | shipping_rate | 400/404 | Yes |
DELETE /districts/:districtId/rate | deleteRate | ShippingDistrictParamsDto | …deleteRate | same | Shipping_DELETE | invalidate | — | shipping_rate | 404 | Yes |
POST /rates/bulk | bulkUpsertRates | BulkShippingRateDto | ShippingAdminBulkService.bulkUpsertRates | same | Shipping_UPDATE | invalidate | — | many rate rows | 400/409 | Yes |
GET /mobile/shipping/quote | getQuote | ShippingQuoteQueryDto | ShippingQuoteCustomerService.getQuote | JWT+IpThrottle | — | shipping | — | district, rate | 404 | Yes |
13.2 Request/Response Exhaustiveness
Covered in §8: minimal/full request bodies (§6.1/8.3, §6.2/8.5), success responses (§8.3, §8.6), the unserviceable-but-200 response (§8.6), domain errors per endpoint (§8 error tables), rate-limit behavior (bulk 5/min → build the UI to submit once), permission errors (403).
13.3 API Diagram Pack
Route ownership (§9.1), sequence per endpoint family (§9.2, backend §7), activity/error diagrams (§9.3, feature §5.3), cache flow (backend §8).
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Admin panel | Minor-unit fee input, three availability flags, skipped list on bulk | 403 only if the seed has not run; 409/400 on bulk misuse | Stable |
| Storefront | Quote takes districtId (from the address), unserviceable = 200 | Branch on availability.serviceable, never status | Stable |
| Mobile app | Account-keyed CUSTOMER_READ 60/min | 429 → back off | Stable |
| Orders (future) | Snapshot the quoted fee — never reference the live rate | Repricing never changes an order | Stable |
| QA | Upsert idempotency, pause-vs-delete, all-or-nothing bulk | Reproduce via exact codes | Stable |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
| District-keyed upsert | PUT replaces | POST+PUT pair | No create-vs-edit; unique district | Stricter permission needed | Shipping_UPDATE only |
| Unserviceable = 200 | Info, not error | 404 | Coverage is a valid answer | Clients branch on status | Documented |
| Bulk all-or-nothing | One transaction | Partial apply | No half-applied price change | Big blast radius | 5/min + report |
| Status-only bulk = plain UPDATE | Never an upsert | Upsert always | Cannot write stale fee over concurrent price change | — | Service contract |
| Bulk 5/min | Low budget | Higher | One request reprices Nepal | Slower bulk | UI submits once |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? | Compatibility Plan |
|---|---|---|---|---|---|
| Repricing a district | Existing orders | None | None | No | Orders snapshot the fee |
Granting Shipping_* | Admin panel | None | None | No | Deployment step |
14. Zero-Omission API Checklist
- Every controller route is documented (§4, §8, §13.1).
- Every parent route prefix and runtime URL is documented (§2, §4).
- Every DTO field, enum, default, transform and validator is documented (§6).
- Every response field and nullable field is documented (§6.4, §8).
- Every auth, guard, permission and guest identity branch is documented (§5).
- Every success, validation, auth, permission, not-found, conflict, rate-limit and server-error branch is documented (§8).
- Every DB read/write, cache invalidation and external call is documented (§11, backend §8).
- Every route has examples for minimal request, success response and representative failures (§8).
- Every endpoint family has route, sequence, activity and error diagrams (§9, backend §7).
- Every tradeoff and compatibility risk is documented (§13.5, §13.6).
- The API doc links to backend and features/flows (§1, See Also).
15. Integration Checklist
- Every route from controllers is documented.
- Every DTO field is documented.
- Every response envelope is documented.
- Every error code is documented.
- Every auth guard and permission is documented.
- Every cache key, queue job and external call is documented.
- Every diagram matches the current code.
- The API doc links to backend and features/flows.
See Also
- Backend doc: /docs/developer/shipping/backend
- Features and flows doc: /docs/developer/shipping/feature
- TDD: not yet published