Serviceability Module — Admin
Admin surface for the delivery deny-list — the pincodes the store cannot ship to, whether each one warns or blocks at checkout, and the message the shopper sees. Bulk add, bulk edit, bulk remove.
Admin surface for the delivery deny-list — the pincodes the store cannot ship to, whether each one warns or blocks at checkout, and the message the shopper sees. Everything is bulk-capable: one endpoint adds a single pincode or a pasted batch.
Source:
api-modules/serviceability/src/controllers/admin-serviceability.controller.ts.This is an optional plugin — removing
ServiceabilityModule.forRoot()fromapp.module.tsremoves these routes along with the storefront check, drops the cart advisory, and makes every pincode deliverable again. The customer-facing side is documented in Serviceability — Storefront.
Model
The table is a deny-list. A pincode with no row is deliverable; there is no allow-list to maintain and an empty table means the store ships anywhere. Each row carries a mode:
| Mode | Checkout behaviour |
|---|---|
inform | The message renders as a warning. The shopper may still place the order. |
block | The message renders as an error and order placement is refused with PINCODE_NOT_SERVICEABLE. |
pincode is unique. Submitting a pincode that already has a rule overwrites its mode and message rather than erroring — re-pasting an overlapping batch is the normal way an operator corrects one.
Conventions
Authentication
All endpoints: BetterAuthGuard + PermissionsGuard.
| Endpoint | Permission |
|---|---|
GET /admin/serviceability/pincodes | serviceability: read |
GET /admin/serviceability/pincodes/:id | serviceability: read |
POST /admin/serviceability/pincodes | serviceability: create |
PATCH /admin/serviceability/pincodes/:id | serviceability: update |
PATCH /admin/serviceability/pincodes/bulk | serviceability: update |
DELETE /admin/serviceability/pincodes/:id | serviceability: delete |
POST /admin/serviceability/pincodes/bulk-delete | serviceability: delete |
Admin UI
Storefront → Serviceability (@sc/admin-serviceability), gated on serviceability: read. The Restrict pincodes button is gated on serviceability: create, the row-level and bulk edit actions on serviceability: update, and both remove actions on serviceability: delete. Removing the module's line from apps/admin/modules/registry.ts drops the nav entry and its route.
It is a single list route — both the batch-add form and the single-row editor open in a dialog over it. Clicking a row opens the editor pre-filled from the row already in the table, so editing costs no extra request. Selecting rows reveals a bulk bar with Edit restriction (mode and/or message) and Remove.
Response envelope
{ "data": <payload>, "message": "Success", "statusCode": <code> }The list uses the standard metadata: { total, limit, offset, hasMore } shape.
Endpoints
GET /admin/serviceability/pincodes — List restricted pincodes
Standard limit / offset pagination, newest first, plus:
| Query | Type | Notes |
|---|---|---|
mode | "inform" | "block" | Filter by restriction |
pincode | string | Prefix match — 56 narrows to 56xxxx |
Response 200 — paginated rows:
type ServiceabilityPincode = {
id: string;
pincode: string;
mode: "inform" | "block";
message: string;
createdAt: string; // ISO
updatedAt: string; // ISO
};GET /admin/serviceability/pincodes/:id — Read one
Response 200 — one ServiceabilityPincode. 404 when the id is unknown.
POST /admin/serviceability/pincodes — Add or overwrite
The single and bulk path are the same endpoint. pincodes accepts either a delimited string or an array, so the admin form needs one text box either way.
type UpsertServiceabilityPincodes = {
pincodes: string | string[]; // comma / space / semicolon / newline separated
mode: "inform" | "block";
message?: string; // up to 500 chars; blank or omitted → default
};message is optional. Omitting it — or sending a blank/whitespace-only string — stores the default copy, "We don't deliver to this pincode.", so a shopper always sees a reason and the operator only writes copy when the generic line isn't good enough.
Entries are trimmed, de-duplicated, and each must match ^[1-9]\d{5}$. At most 5000 pincodes per request; a single invalid entry fails the whole request with VALIDATION_ERROR rather than silently dropping it.
Response 201:
type UpsertServiceabilityResult = {
created: number; // pincodes that had no rule before
updated: number; // pincodes whose rule was overwritten
affected: number; // created + updated
pincodes: string[]; // the normalised, de-duplicated batch
};PATCH /admin/serviceability/pincodes/:id — Edit one
type UpdateServiceabilityPincode = {
pincode?: string;
mode?: "inform" | "block";
message?: string;
};At least one field is required. Changing pincode to one that already has a rule is 409. Omitting message leaves it alone; sending it blank resets it to the default. Response 200 — the updated ServiceabilityPincode.
PATCH /admin/serviceability/pincodes/bulk — Edit many
type BulkUpdateServiceabilityPincodes = {
ids: string[]; // 1–1000, de-duplicated
mode?: "inform" | "block";
message?: string;
};At least one of mode / message is required. pincode is deliberately not bulk-editable — it identifies the row. A blank message resets the selection to the default copy. Unknown ids are ignored, so affected may be lower than ids.length.
Response 200 — { created: 0, updated, affected }.
DELETE /admin/serviceability/pincodes/:id — Lift one restriction
Response 204. 404 when the id is unknown. The pincode becomes deliverable immediately.
POST /admin/serviceability/pincodes/bulk-delete — Lift many
POST rather than DELETE because the id list travels in the body, which DELETE does not carry portably across proxies.
type BulkDeleteServiceabilityPincodes = { ids: string[] }; // 1–1000Response 200 — { created: 0, updated: 0, affected }.
Enforcement
The deny-list is consulted through the neutral SERVICEABILITY_PORT (@sc/core/ports), never by importing this module, so every consumer degrades cleanly when the plugin is unmounted:
| Consumer | Behaviour |
|---|---|
Cart reads (GET /store/cart, POST /store/cart/prepare-checkout) | Attach a serviceability advisory for the picked delivery address. Advisory only — a cart read never fails. |
Order placement (POST /store/orders) | Re-checks the resolved shipping address and rejects block with 400 PINCODE_NOT_SERVICEABLE. inform passes. |
| Razorpay Magic serviceability callback | Reports serviceable: false for a blocked pincode, so Magic stops the shopper before payment is captured. |
Placement re-checks rather than trusting the cart advisory: the storefront may ignore it, and the operator may have added the pincode between the shopper reading the cart and pressing pay.
For orders that arrive already paid (the externally-paid webhook path), a blocked pincode is logged as a warning and accepted — refusing would strand captured money. The provider's own pre-payment callback is the gate there.
Search Module — Admin
HTTP surface for the platform-admin operational control of the Typesense product search index: trigger a bulk reindex, poll its status, and recreate the collection — surfaced as action buttons on the Product Search settings page.
Settings Module — Admin
HTTP surface for the platform-wide settings store (admin + store scopes) and the platform-admin override surface for vendor-scoped settings (admin + store sub-scopes per vendor).…