POS Module Backend Documentation
Backend architecture, services, data model, payment event flow, idempotency, and inter-module integration for the In-Store Sales (POS) module.
POS Module - Backend Documentation
1. Backend Scope and Boundaries
POS backend owns:
- Walk-in customer creation and lookup
- POS order creation (delegates to
OrderService.createFromPosItems()) - POS payment recording (delegates to
PaymentService, emitsEventEmitter2event) - POS fulfillment fast-track (pickup-confirm, delegates to
OrderAdminService) - POS order list, export, and analytics queries
- EMI inquiry on behalf of walk-in customer (delegates to
EmiCustomerService)
POS does NOT own:
- Order state-machine transitions (owned by
OrderService/OrderAdminService) - Payment finalization downstream logic (owned by
OrderPaymentListener) - BullMQ queue processing (POS has no BullMQ processors)
- Customer authentication (customers receive a password-reset email; POS never authenticates customers)
2. Module Composition
PosModule
├── imports: [OrderCoreModule, OrderAdminModule, AuthModule, UsersModule, PaymentModule, EmiCustomerModule]
├── controllers: [PosController]
├── providers: [PosCustomerService, PosOrderService, PosPaymentService]
└── exports: [PosCustomerService, PosOrderService, PosPaymentService]PosModule is registered in AppModule after PublicInvoiceModule.
Dependency chain:
PosCustomerService→UsersService,AuthService,DatabasePosOrderService→OrderService(fromOrderCoreModule),OrderAdminService(fromOrderAdminModule),DatabasePosPaymentService→PaymentService,OrderAdminService,EventEmitter2,DatabasePosController→ all 3 services +EmiCustomerService
3. Data Model
POS does not add new tables. It uses columns added to existing tables.
orders table additions:
order_sourcepgEnum: new value"pos_sale"(added via Drizzle migration in SP1).- New:
"pos_sale"— identifies this as a POS terminal order.
- New:
order_payment_methodpgEnum: new value"pos"(added via Drizzle migration in SP1).- Existing values:
"online","cod". - New:
"pos"— identifies in-store payment (not a redirect gateway).
- Existing values:
Schema source of truth:
packages/db/src/schema/order/enums.ts—orderSourceEnum,orderPaymentMethodEnumpackages/db/src/schema/order/orders.ts—orderstable
No new tables. No new FK constraints. Migration type: ALTER TYPE ... ADD VALUE IF NOT EXISTS (non-transactional on Postgres versions earlier than 14 — SP1 handled this correctly).
async_requests table usage:
- Scope
"pos_create_order"— created inPosOrderService.createPosOrder(). - Scope
"pos_record_payment"— created inPosPaymentService.recordPayment(). - Unique index on
(actor_type, actor_id, scope, idempotency_key)— prevents duplicate processing. onConflictDoNothingon insert prevents race-condition duplicate inserts.
4. Service Reference
4.1 PosCustomerService
| Method | Description | Returns |
|---|---|---|
lookupByEmail(email) | Case-insensitive email lookup. Returns null if not found. | PosCustomerLookupResponseDto | null |
createWalkInCustomer(dto, ctx) | Creates user account (emailVerified=false, random password) + triggers password-reset email. Throws 409 on duplicate email/phone. | PosCustomerLookupResponseDto |
checkDuplicatePhone(phone) | Internal helper — checks phone uniqueness. | boolean |
4.2 PosOrderService
| Method | Description | Returns |
|---|---|---|
createPosOrder(adminId, dto, idempotencyKey) | Idempotency check → customer validate → product validate → price calc → orderService.createFromPosItems() → asyncRequests insert. | AsyncRequestResponseDto |
pickupConfirm(orderId, adminId) | Validates POS orderSource → reads fulfillmentStatus → fast-tracks through "ready" → "picked_up" via orderAdminService.updateFulfillmentStatus(). Idempotent on already-picked-up. | AsyncRequestResponseDto |
getPosOrders(query) | Paginated list of orderSource = "pos_sale" orders with customer join. Max limit: 100. | { items, total, page, limit } |
getPosAnalytics(query) | Aggregates revenue/count for completed POS orders. Includes per-gateway breakdown via SQL GROUP BY and per-district breakdown via GROUP BY shippingDistrict. Supports optional district filter param. | PosAnalyticsDto |
getPosExport(query) | Date-ranged flat export. Validates 90-day max window. | PosOrderListItemDto[] |
Private helpers:
validateAndFetchItems(items)— batch-fetches products, validatesstatus = "published"+isSellable = true, returns typed item array withunitMrp/unitSp.resolveOrderType(items)— resolves"physical"/"digital"/"mixed"from item product types.
4.3 PosPaymentService
| Method | Description | Returns |
|---|---|---|
recordPayment(orderId, adminId, dto, idempotencyKey) | Full idempotent payment recording flow (see diagram below). | PosPaymentResponseDto |
recordPaymentFailure(orderId, adminId, reason) | Validates POS order → adds admin note "[POS Payment Failure] <reason>". No status change. | void |
5. POS Payment Flow (Detailed)
Key contracts:
PaymentService.createPaymentRecord(referenceId, referenceType, userId, amountNpr, gateway)—referenceIdisString(orderId)(stringified),referenceType = "order".PaymentService.markPaymentCompleted(paymentId, transactionRef, {})— setsgatewayTransactionId = transactionRefon the payment record.PaymentCompletedEventfields:{ paymentId, referenceId: String(orderId), referenceType: "order", userId: order.customerId, amountNpr, gateway, gatewayTransactionId: transactionRef }.PAYMENT_EVENTS.COMPLETEDis anEventEmitter2event (NestJS EventEmitter), NOT a BullMQ job.
6. IpThrottlerGuard Extension
Walk-in customer creation uses IpThrottlerGuard with the "user" keyStrategy (added in SP3). The throttler reads request.user?.sub as the rate-limit key instead of the client IP. This is required because all admin staff in the office share the same IP address.
@UseGuards(IpThrottlerGuard)
@IpThrottle({ limit: 10, windowSeconds: 3600, keyStrategy: "user" })Guard location: apps/api/src/common/guards/ip-throttler.guard.ts
7. Error Codes
All POS error codes are defined in apps/api/src/common/types/error-codes.ts:
| Code | HTTP Exception | Location triggered |
|---|---|---|
POS_DUPLICATE_EMAIL | ConflictException | PosCustomerService.createWalkInCustomer() |
POS_DUPLICATE_PHONE | ConflictException | PosCustomerService.createWalkInCustomer() |
POS_CUSTOMER_NOT_FOUND | NotFoundException | PosOrderService.createPosOrder() |
POS_ORDER_NOT_POS_SALE | BadRequestException | PosPaymentService.recordPayment(), PosPaymentService.recordPaymentFailure(), PosOrderService.pickupConfirm() |
POS_ORDER_NOT_PAYMENT_PENDING | BadRequestException | PosPaymentService.recordPayment() |
POS_INVALID_GATEWAY | BadRequestException | PosPaymentService.recordPayment() |
POS_DUPLICATE_PAYMENT_REFERENCE | BadRequestException | PosPaymentService.recordPayment() (Postgres 23505 catch) |
POS_PICKUP_NOT_IN_STORE | BadRequestException | PosOrderService.pickupConfirm() |
8. File Map
apps/api/src/modules/pos/
├── pos.module.ts — Module registration
├── pos.controller.ts — 10 admin endpoints at /admin/pos/*
├── pos-customer.service.ts — Customer lookup + walk-in creation
├── pos-order.service.ts — Order creation, pickup, list, analytics, export
├── pos-payment.service.ts — Payment recording + failure recording
└── dto/
├── create-walk-in-customer.dto.ts — CreateWalkInCustomerDto
├── create-pos-order.dto.ts — CreatePosOrderDto, PosOrderLineItemDto
├── pos-customer-lookup-response.dto.ts — PosCustomerLookupResponseDto
├── pos-order-list-item.dto.ts — PosOrderListItemDto, PosOrderCustomerSummaryDto
├── pos-order-list-query.dto.ts — PosOrderListQueryDto
├── pos-analytics.dto.ts — PosAnalyticsDto, PosAnalyticsQueryDto, PosGatewayBreakdownDto
├── pos-export-query.dto.ts — PosExportQueryDto
├── record-pos-payment.dto.ts — RecordPosPaymentDto, POS_GATEWAYS const, PosGateway type
├── pos-payment-failure.dto.ts — PosPaymentFailureDto
└── pos-payment-response.dto.ts — PosPaymentResponseDto
packages/db/src/schema/order/enums.ts — orderSourceEnum (pos_sale), orderPaymentMethodEnum (pos)
packages/db/src/seed/seed-auth.ts — Add "Pos" module + Pos_RECORD_PAYMENT, Pos_PROCESS_REFUNDSpecs:
apps/api/src/modules/pos/pos-customer.service.spec.ts — 5 tests
apps/api/src/modules/pos/pos-order.service.spec.ts — 22 tests
apps/api/src/modules/pos/pos-payment.service.spec.ts — 11 testsSee Also
- API Reference: See POS - API Reference for all 10 endpoint contracts.
- Module Overview: See POS - Overview for user flows and permissions.
- Payment Architecture: See Payment Module Architecture "POS Direct Payment Consumer" section.
- Order Backend: See Order - Backend Documentation for
orderSourceandpaymentMethodenum definitions.