Happy House - Ecommerce Docs
Developer ResourcesShipping

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

AreaFiles InspectedWhat Was Verified
Controllersapps/api/src/modules/shipping/admin/shipping-admin.controller.ts, customer/shipping-quote-customer.controller.tsRoutes, methods, guards, permissions, status codes
DTOsdto/*.tsValidation, defaults
Servicesshipping-admin.service.ts, shipping-admin-bulk.service.ts, shipping-quote-customer.service.tsBehavior, side effects, errors
Schemapackages/db/src/schema/shipping/shipping-rate.tsUnique district, fee type
Error registryapps/api/src/common/types/error-codes.ts (// SHIPPING)SHIPPING_* codes

2. Module Summary

FieldValue
Module nameshipping
Module slugshipping
Primary actorsadmin, customer (signed in)
API surfacesadmin, mobile
Base route prefixes/api/admin/shipping, /api/mobile/shipping
Auth modelJwtAuthGuard + RoleGuard (admin); JwtAuthGuard (quote)
PersistencePostgreSQL (shipping_rate), Redis (shipping cache domain)
Runtime source of truthshipping_rate rows
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
feeDelivery fee in integer minor units (NPR 120.00 = 12000); 0 = freeschemaAll rate payloads
districtIdThe district a quote/rate is keyed on — never an address idcontrollersQuote, rates
configuredA rate row exists for the districtserviceCoverage list
activeis_active flag — pause keeps the feeserviceCoverage list, quote
serviceableconfigured && activeserviceCoverage, quote, address status
unconfiguredNo rate rowserviceCoverage filter

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
AdminGET/api/admin/shipping/districtsAdminJWT+RoleShipping_READShippingAdminControllerCoverage over all 77 districts
AdminGET/api/admin/shipping/districts/:districtId/rateAdminJWT+RoleShipping_READsameOne district's rate
AdminPUT/api/admin/shipping/districts/:districtId/rateAdminJWT+RoleShipping_UPDATEsameUpsert rate
AdminDELETE/api/admin/shipping/districts/:districtId/rateAdminJWT+RoleShipping_DELETEsameDiscard rate (200)
AdminPOST/api/admin/shipping/rates/bulkAdminJWT+RoleShipping_UPDATEsameBulk price (200)
MobileGET/api/mobile/shipping/quoteCustomerJWTShippingQuoteCustomerControllerQuote 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

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
AdminJwtAuthGuard, RoleGuard, IpThrottlerGuardreq.userShipping_READ / Shipping_UPDATE / Shipping_DELETENoSuperadmin bypasses
QuoteJwtAuthGuard, IpThrottlerGuardreq.userNoCUSTOMER_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)

FieldTypeRequiredDefaultValidationExampleSource
feenumberYesN/A@IsInt, >= 0minor units12000upsert-shipping-rate.dto.ts
isActivebooleanYesN/A@IsBooleantrue

6.2 BulkShippingRateDto

FieldTypeRequiredValidationNotes
scope.districtIdsstring[]one scope key requiredUUID v7 eachExplicit districts
scope.provinceIdsstring[]UUID v7 eachWhole provinces
scope.allDistrictsbooleanAll of Nepal
feenumberone of fee/isActive required>= 0 minor unitsOmitting = status-only
isActiveboolean

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

PartRequiredDetails
QueryNoprovinceId, 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. Upsertdistrict_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

HTTPCodeCondition
404SHIPPING_DISTRICT_NOT_FOUNDUnknown district
400SHIPPING_FEE_NEGATIVEfee < 0
403Role 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

HTTPCodeCondition
404SHIPPING_RATE_NOT_FOUNDNo 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

HTTPCodeCondition
400SHIPPING_BULK_SCOPE_EMPTYScope resolved to zero districts
400SHIPPING_BULK_NO_CHANGE_REQUESTEDNeither fee nor isActive sent
409SHIPPING_BULK_LIMIT_EXCEEDEDMore than 77 distinct districts (cannot happen from a correct client)
400/404district/province not foundWhole 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

HTTPCodeCondition
404SHIPPING_DISTRICT_NOT_FOUNDdistrictId names nothing

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence (quote)

9.3 Error Branch (bulk)

EndpointPagination TypeDefault SizeMax SizeSort FieldsFiltersResult Cap
GET /admin/shipping/districtsoffset (opt-in)20100district name (fixed)provinceId, status, pricing, search77
GET /mobile/shipping/quotenonedistrictId1

11. Caching, Jobs, and External Integrations

IntegrationUsed?Details
Redis cacheYesshipping cache domain — serviceability behind one key; invalidated on every admin write
BullMQNo
External APINo

13. Mandatory Deep API Documentation Pack

13.1 Route-by-Route Completeness Matrix

RouteController MethodDTOsService MethodGuardsPermissionsCacheJobsDB TouchesErrorsDocumented?
GET /admin/shipping/districtslistDistrictsListShippingDistrictsQueryDtoShippingAdminService.listDistrictsJWT+Role+IpThrottleShipping_READdistrict, rateYes
GET /districts/:districtId/rategetRateShippingDistrictParamsDto…getDistrictRatesameShipping_READdistrict, rate404Yes
PUT /districts/:districtId/rateupsertRateUpsertShippingRateDto…upsertRatesameShipping_UPDATEinvalidateshipping_rate400/404Yes
DELETE /districts/:districtId/ratedeleteRateShippingDistrictParamsDto…deleteRatesameShipping_DELETEinvalidateshipping_rate404Yes
POST /rates/bulkbulkUpsertRatesBulkShippingRateDtoShippingAdminBulkService.bulkUpsertRatessameShipping_UPDATEinvalidatemany rate rows400/409Yes
GET /mobile/shipping/quotegetQuoteShippingQuoteQueryDtoShippingQuoteCustomerService.getQuoteJWT+IpThrottleshippingdistrict, rate404Yes

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

ConsumerRequired KnowledgeFailure HandlingContract Stability
Admin panelMinor-unit fee input, three availability flags, skipped list on bulk403 only if the seed has not run; 409/400 on bulk misuseStable
StorefrontQuote takes districtId (from the address), unserviceable = 200Branch on availability.serviceable, never statusStable
Mobile appAccount-keyed CUSTOMER_READ 60/min429 → back offStable
Orders (future)Snapshot the quoted fee — never reference the live rateRepricing never changes an orderStable
QAUpsert idempotency, pause-vs-delete, all-or-nothing bulkReproduce via exact codesStable

13.5 API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
District-keyed upsertPUT replacesPOST+PUT pairNo create-vs-edit; unique districtStricter permission neededShipping_UPDATE only
Unserviceable = 200Info, not error404Coverage is a valid answerClients branch on statusDocumented
Bulk all-or-nothingOne transactionPartial applyNo half-applied price changeBig blast radius5/min + report
Status-only bulk = plain UPDATENever an upsertUpsert alwaysCannot write stale fee over concurrent price changeService contract
Bulk 5/minLow budgetHigherOne request reprices NepalSlower bulkUI submits once

13.6 API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
Repricing a districtExisting ordersNoneNoneNoOrders snapshot the fee
Granting Shipping_*Admin panelNoneNoneNoDeployment 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