Cash on Delivery (COD)
COD is a pending-collection gateway — no redirect, no webhook, and no paid/completed status until admin records cash.
Cash on Delivery (COD)
Audience: Backend developers, admin operators Scope: COD checkout flow, pending collection status, admin cash collection endpoint
Overview
| Property | Value |
|---|---|
| Gateway name | cod |
| Initiation type | cod |
| Redirect required | No |
| Webhook required | No |
| Payment finalization | Admin cash collection completes the existing pending payment row |
| Checkout order status | payment_pending |
| Checkout payment status | cod_pending |
| Physical collection | Admin endpoint: PUT /api/admin/orders/:id/cod-collect |
COD is available for physical orders only. Digital and care-package orders cannot use COD.
Warranty note: COD checkout does not activate warranty eligibility. Warranty remains blocked while orders.paymentStatus = "cod_pending" and becomes eligible only after admin collection sets paymentStatus = "cod_collected".
Payment Flow
Checkout Request
{
"gateway": "cod",
"returnUrl": "https://example.com/orders/101/confirmation"
}Checkout Response (200)
{
"orderId": 101,
"status": "payment_pending",
"paymentId": 9001,
"gatewayTransactionId": "cod_9001",
"initiationType": "cod",
"redirectUrl": "https://example.com/orders/101/confirmation",
"gatewayPayload": {}
}Note: redirectUrl echoes the returnUrl sent in the request. Frontend navigates there directly after receiving the cod response — no gateway redirect occurs.
The created order is confirmed for fulfillment after reservation succeeds, but it is not paid yet:
| Field | Checkout value | Cash collected value |
|---|---|---|
orders.orderStatus | payment_pending | unchanged by collection |
orders.paymentStatus | cod_pending | cod_collected |
payment.status | pending | completed |
orders.paymentReference | cod_<paymentId> | unchanged |
COD checkout does not emit payment.completed, does not enqueue order.payment_success, and does not create a paid status-history entry. It records COD pending collection history using payment_pending.
During that pending-collection window, warranty claim creation and warranty-status access treat the order as unpaid.
COD Restrictions
| Cart / Product Type | COD Allowed? | Error Code |
|---|---|---|
| Physical only | ✅ Yes | — |
| Mixed (physical + digital) | ✅ Yes | — |
| Digital only | ❌ No | ORDER_COD_PHYSICAL_ONLY |
| Care package only | ❌ No | ORDER_COD_PHYSICAL_ONLY |
| Buy-now digital product | ❌ No | ORDER_COD_PHYSICAL_ONLY |
Admin Cash Collection
After the order is delivered, an admin records physical cash collection:
PUT /api/admin/orders/:id/cod-collect
Authorization: Bearer <admin-token>Request body:
{
"remarks": "Collected by rider #12 at 10:30 AM — cash NPR 3500"
}Response (200):
{
"message": "COD cash collected successfully",
"data": {
"orderId": 101,
"codCashCollectedAt": "2026-06-22T04:45:00.000Z",
"paymentStatus": "cod_collected"
}
}Idempotency: If cash is already recorded, the endpoint returns the existing codCashCollectedAt without error.
On first collection, the backend finds the existing pending COD payment row for referenceType = "order", referenceId = orderId, gateway = "cod", and status = "pending". It marks that payment row completed, stores or reuses gatewayTransactionId = "cod_<paymentId>", then updates the order to paymentStatus = "cod_collected".
That cod_collected transition is also the point where warranty becomes eligible, assuming the order is delivered and not later cancelled or refund-approved.
Required permission: Orders_UPDATE
Remarks rules: Non-blank string, max 500 characters. Whitespace-only is rejected (400).
Schema Columns Added (SP1)
| Column | Type | Description |
|---|---|---|
orders.payment_method | order_payment_method enum | "online" or "cod" |
orders.cod_cash_collected_at | timestamp | When admin recorded physical cash collection |
orders.cod_collection_remarks | varchar(500) | Free-text remarks from admin at collection |
Error Codes
| Code | HTTP | Trigger |
|---|---|---|
ORDER_COD_PHYSICAL_ONLY | 400 | COD used with digital-only or care-package-only cart/product |
ORDER_NOT_COD_METHOD | 400 | Admin cash collection on a non-COD order |
ORDER_PAYMENT_FAILED | 400 | Admin collection cannot find the pending COD payment row |
Implementation Files
apps/api/src/modules/payment/gateways/cod.gateway.tsapps/api/src/modules/order/customer/order-customer.service.ts(COD derivation, early return)apps/api/src/modules/order/order.service.ts(COD restriction in createFromCart / createBuyNow)apps/api/src/modules/order/admin/order-admin.service.ts(collectCodPayment)apps/api/src/modules/order/admin/order-admin.controller.ts(PUT /:id/cod-collect)packages/db/src/schema/payment.ts(PAYMENT_GATEWAYS enum)packages/db/src/schema/order/orders.ts(cod columns)packages/db/src/migrations/0007_cod_payment.sql