Shop It Docs
Developer ResourcesPOS (In-Store Sales)

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, emits EventEmitter2 event)
  • 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:

  • PosCustomerServiceUsersService, AuthService, Database
  • PosOrderServiceOrderService (from OrderCoreModule), OrderAdminService (from OrderAdminModule), Database
  • PosPaymentServicePaymentService, OrderAdminService, EventEmitter2, Database
  • PosController → all 3 services + EmiCustomerService

3. Data Model

POS does not add new tables. It uses columns added to existing tables.

orders table additions:

  • order_source pgEnum: new value "pos_sale" (added via Drizzle migration in SP1).
    • New: "pos_sale" — identifies this as a POS terminal order.
  • order_payment_method pgEnum: new value "pos" (added via Drizzle migration in SP1).
    • Existing values: "online", "cod".
    • New: "pos" — identifies in-store payment (not a redirect gateway).

Schema source of truth:

  • packages/db/src/schema/order/enums.tsorderSourceEnum, orderPaymentMethodEnum
  • packages/db/src/schema/order/orders.tsorders table

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 in PosOrderService.createPosOrder().
  • Scope "pos_record_payment" — created in PosPaymentService.recordPayment().
  • Unique index on (actor_type, actor_id, scope, idempotency_key) — prevents duplicate processing.
  • onConflictDoNothing on insert prevents race-condition duplicate inserts.

4. Service Reference

4.1 PosCustomerService

MethodDescriptionReturns
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

MethodDescriptionReturns
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, validates status = "published" + isSellable = true, returns typed item array with unitMrp/unitSp.
  • resolveOrderType(items) — resolves "physical" / "digital" / "mixed" from item product types.

4.3 PosPaymentService

MethodDescriptionReturns
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)referenceId is String(orderId) (stringified), referenceType = "order".
  • PaymentService.markPaymentCompleted(paymentId, transactionRef, {}) — sets gatewayTransactionId = transactionRef on the payment record.
  • PaymentCompletedEvent fields: { paymentId, referenceId: String(orderId), referenceType: "order", userId: order.customerId, amountNpr, gateway, gatewayTransactionId: transactionRef }.
  • PAYMENT_EVENTS.COMPLETED is an EventEmitter2 event (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:

CodeHTTP ExceptionLocation triggered
POS_DUPLICATE_EMAILConflictExceptionPosCustomerService.createWalkInCustomer()
POS_DUPLICATE_PHONEConflictExceptionPosCustomerService.createWalkInCustomer()
POS_CUSTOMER_NOT_FOUNDNotFoundExceptionPosOrderService.createPosOrder()
POS_ORDER_NOT_POS_SALEBadRequestExceptionPosPaymentService.recordPayment(), PosPaymentService.recordPaymentFailure(), PosOrderService.pickupConfirm()
POS_ORDER_NOT_PAYMENT_PENDINGBadRequestExceptionPosPaymentService.recordPayment()
POS_INVALID_GATEWAYBadRequestExceptionPosPaymentService.recordPayment()
POS_DUPLICATE_PAYMENT_REFERENCEBadRequestExceptionPosPaymentService.recordPayment() (Postgres 23505 catch)
POS_PICKUP_NOT_IN_STOREBadRequestExceptionPosOrderService.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_REFUND

Specs:

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 tests

See Also