Skip to main content

Retail Return Module

Feature: 014-exchange-return | Backend module: apps/backend/src/retail-return/

Overview​

The Retail Return module handles return and exchange operations for retail sales in FlowPOS. It manages the full lifecycle from draft creation through inventory reconciliation.

  • Returns: Customer returns items from a previous sale → refund is issued
  • Exchanges: Customer returns items and purchases new ones → net settlement

Both transaction types are stored as rows in the sale table with type = 'return' or type = 'exchange', linked to the original sale via parent_sale_id.


Architecture​

retail-return/
├── domain/ # Ports & type definitions
│ ├── return-transaction-repository.domain.ts
│ ├── return-policy-repository.domain.ts
│ ├── return-reason-repository.domain.ts
│ ├── approval-audit-log-repository.domain.ts
│ ├── return-transaction.types.ts
│ └── exchange-transaction.types.ts
├── application/ # Business logic (use cases)
│ ├── return-transaction.service.ts # Return flow orchestration
│ ├── exchange-transaction.service.ts # Exchange flow orchestration
│ ├── return-line-builder.service.ts # Shared: line creation & validation
│ ├── sale-validation.service.ts # Shared: sale lookup & policy checks
│ ├── return-inventory.service.ts # Inventory ledger & stock updates
│ ├── return-policy.service.ts # Policy CRUD & resolution
│ ├── return-reason.service.ts # Reason CRUD & uniqueness
│ └── approval.service.ts # Manager PIN verification & audit
├── infrastructure/ # Kysely repository implementations
│ ├── return-transaction.repository.ts
│ ├── return-policy.repository.ts
│ ├── return-reason.repository.ts
│ └── approval-audit-log.repository.ts
└── interfaces/ # HTTP controllers, DTOs, queries
├── return-transaction.controller.ts
├── exchange-transaction.controller.ts
├── return-reason.controller.ts
├── return-policy.controller.ts
├── dtos/ (10 DTOs)
└── query/ (2 query parameter classes)

Shared Services​

Two services extract logic that was previously duplicated between ReturnTransactionService and ExchangeTransactionService:

ServiceResponsibility
ReturnLineBuilderServiceValidates return quantities, looks up reasons, determines restock action, calculates refund amounts, builds SaleLineItem objects, recalculates totals
SaleValidationServiceLooks up original sales by receipt/ID, validates business ownership and sale status, checks return windows, validates no-receipt policy

Domain Concepts​

Transaction Types​

Typesale.typeDescription
ReturnreturnItems returned → refund issued
ExchangeexchangeItems returned + new items purchased → net settlement

Transaction Lifecycle​

draft → completed   (committed successfully)
draft → cancelled (cancelled by user or timeout)

Item Condition & Restock Action​

ConditionReason affectsRestockingRestock ActionStock Bucket
resellableanyrestockquantity
damagedanyquarantinequarantined
openedtruerestockquantity
openedfalsescrap(no stock update)

Policy Resolution​

Policies are resolved with location-specific precedence:

  1. Active policy matching businessId + locationId (if provided)
  2. Active business-wide policy (locationId IS NULL)
  3. Most recently created if multiple match

API Endpoints​

Returns (/returns)​

MethodPathDescription
POST/returnsInitiate a draft return
POST/returns/:id/linesAdd a return line item
GET/returns/:id/refund-calculationCalculate refund breakdown
GET/returns/:id/requires-approvalCheck approval requirement
POST/returns/:id/commitFinalize return (atomic)
GET/returns/:idGet return details
DELETE/returns/:idCancel draft return
GET/returns/sales/:receiptNumber/lookupLook up original sale

Exchanges (/exchanges)​

MethodPathDescription
POST/exchangesInitiate a draft exchange
POST/exchanges/:id/return-linesAdd return line (item out)
POST/exchanges/:id/sale-linesAdd sale line (item in)
GET/exchanges/:id/settlementCalculate net settlement
GET/exchanges/:id/requires-approvalCheck approval requirement
POST/exchanges/:id/commitFinalize exchange (atomic)
GET/exchanges/:idGet exchange details
DELETE/exchanges/:idCancel draft exchange

Return Reasons (/return-reasons)​

MethodPathDescription
POST/return-reasonsCreate reason (code unique per business)
GET/return-reasonsList with pagination/search
GET/return-reasons/:idGet by ID
PATCH/return-reasons/:idUpdate
DELETE/return-reasons/:idSoft delete

Return Policies (/return-policies)​

MethodPathDescription
POST/return-policiesCreate policy
GET/return-policiesList with pagination
GET/return-policies/applicableResolve applicable policy
GET/return-policies/:idGet by ID
PATCH/return-policies/:idUpdate
DELETE/return-policies/:idSoft delete

Concurrency & Data Integrity​

The commit flow uses several mechanisms to prevent data corruption:

  1. Pre-validation outside the transaction (fast-fail, no locks held)
  2. Status re-check inside the transaction (prevents double-commit)
  3. FOR UPDATE row lock on the original sale (serializes concurrent returns)
  4. Quantity re-validation under lock (prevents over-returning)
  5. Idempotency keys on inventory ledger entries (prevents duplicate stock updates)

Store Credit Events​

Store credit issuance is emitted as an event after the DB transaction commits, ensuring the credit is only created if the return succeeds.


Key Design Decisions​

  1. JSON-based line items: Return/exchange lines are stored in sale.sale_detail JSON alongside original sale lines, discriminated by lineType: 'return' | 'sale'. This avoids schema proliferation while maintaining auditability.

  2. Reusing the sale table: Returns and exchanges are stored as sale rows with type = 'return' | 'exchange' and a parent_sale_id FK. This simplifies reporting and cash register session reconciliation.

  3. Policy-driven behaviour: Return windows, approval thresholds, refund method rules, and cross-location behaviour are all configurable per business/location via return_policy.

  4. Soft deletes: Return reasons and policies use active = false rather than physical deletion to preserve referential integrity.