POS Module Overview
In-store sales terminal for admin staff — customer lookup, order creation, payment recording, and fulfillment management.
Audience: Admin/back-office developers and frontend developers building the POS terminal UI.
POS Module - Overview
1. What is POS?
The POS (Point of Sale) module is the in-store sales terminal that enables admin staff to create orders on behalf of walk-in customers.
Key facts:
- All endpoints are mounted at
GET|POST /api/admin/pos/*. - Orders created via POS have
orderSource = "pos_sale"andpaymentMethod = "pos". - Swagger tag:
POS. - Auth:
JwtAuthGuard + RoleGuardon all endpoints. - No customer-facing POS routes exist. All POS routes are admin-only.
2. Permissions Required
| Permission | Endpoints |
|---|---|
Pos_READ | GET /customers/lookup, GET /orders, GET /orders/export, GET /analytics |
Pos_CREATE | POST /customers, POST /orders, POST /emi-inquiry |
Pos_RECORD_PAYMENT | POST /orders/:orderId/payment |
Pos_UPDATE | POST /orders/:orderId/payment-failure, POST /orders/:orderId/pickup-confirm |
Seeds required:
- Add
"Pos"to themodulesconst array inpackages/db/src/seed/seed-auth.ts. - Custom permissions
Pos_RECORD_PAYMENTandPos_PROCESS_REFUNDmust be seeded separately.
3. POS Admin User Flows
3.1 Walk-in customer order (new customer)
- Staff calls
GET /api/admin/pos/customers/lookup?email=<email>→ returnsnull(not found). - Staff calls
POST /api/admin/pos/customers(body:name,email,phone?).- Creates account with a random temp password.
- Password-reset email is sent to the customer automatically.
- Returns
customerId.
- Staff calls
POST /api/admin/pos/orders(body:customerId,items[],couponDiscount?,shippingFee?,receiverName?,shippingAddress*).- Header:
idempotency-key(required, UUID). - Returns
{ requestId, status: "completed", id: orderId }.
- Header:
- Staff calls
POST /api/admin/pos/orders/:orderId/payment(body:gateway,amountNpr,transactionReference?).- Header:
idempotency-key(required, UUID — different from the order creation key). - Returns
{ paymentId, orderId }. - Payment event emitted → order status transitions to
paid→ fulfillment moves toreceived.
- Header:
- (Optional) Staff calls
POST /api/admin/pos/orders/:orderId/pickup-confirm.- If
fulfillmentStatusis"received"→ advances through"ready"→"picked_up". - If
fulfillmentStatusis"ready"→ advances to"picked_up". - If already
"picked_up"→ no-op, returns completed.
- If
3.2 Existing customer order
- Staff calls
GET /api/admin/pos/customers/lookup?email=<email>→ returns{ exists: true, customerId, name, email, phone, image }. - Proceed directly to step 3 of the walk-in flow above.
3.3 Payment failure recording
- Staff records payment failed (gateway declined, etc.).
POST /api/admin/pos/orders/:orderId/payment-failure(body:reason).- Adds
"[POS Payment Failure] <reason>"as an admin note on the order. - Does NOT change order status — the order remains in
payment_pending. - Staff can retry
POST /orders/:orderId/paymentafterwards.
- Adds
3.4 EMI inquiry on behalf of walk-in
POST /api/admin/pos/emi-inquiry(body:EmiInquirySubmitDto).- Delegates to
EmiCustomerService.submitInquiry(). - Creates an EMI inquiry record.
- Returns
EmiInquirySubmitResponseDto.
- Delegates to
3.5 Analytics and export
GET /api/admin/pos/analytics?startDate=2026-01-01&endDate=2026-01-31
→ Returns totalRevenue, orderCount, gatewayBreakdown[]
GET /api/admin/pos/orders/export?startDate=2026-01-01&endDate=2026-01-31
→ Returns flat array of all POS orders in range (max 90-day window)
→ Error: ORDER_DATE_RANGE_TOO_LARGE if range > 90 days
GET /api/admin/pos/orders?page=1&limit=20&startDate=...&endDate=...
→ Returns paginated list of POS orders with customer snapshot4. Rate Limiting
Walk-in customer creation (POST /customers) is rate-limited using IpThrottlerGuard with keyStrategy: "user" — the rate-limit key is the JWT sub (adminId), NOT the IP. This is by design because admin staff share a single office IP.
Limit: 10 requests per 3600 seconds per admin user.
5. State Model
POS orders follow the same order state lifecycle as standard orders:
created → payment_pending → paid → [fulfillment: received → ready → picked_up]POS-specific behavioral notes:
- Orders skip
notifyOrderCreated. POS callsOrderService.createFromPosItems()directly;OrderCustomerService.dispatchOrderCreatedNotification()is never called for POS. - Payment completion emits
PAYMENT_EVENTS.COMPLETEDviaEventEmitter2(not a BullMQ queue job). The existingOrderPaymentListenerhandles this event and transitions the order status topaidand triggers fulfillment toreceived. - No new BullMQ queues were added for POS. All downstream processing reuses existing order queue infrastructure.
- POS does NOT use BullMQ directly. There is no POS-specific BullMQ queue or processor.
6. Idempotency
Two endpoints use idempotency:
| Endpoint | Scope | Header |
|---|---|---|
POST /orders | pos_create_order | idempotency-key (required) |
POST /orders/:orderId/payment | pos_record_payment | idempotency-key (required) |
Both use the async_requests table with a unique index on (actorType, actorId, scope, idempotencyKey). onConflictDoNothing prevents race-condition duplicates. Replaying the same key returns the cached result without re-processing.
See Also
- API Reference: See POS - API Reference for complete request/response DTOs and endpoint contracts.
- Backend Reference: See POS - Backend Documentation for service architecture, data model, and the POS payment event flow.