Saltar al contenido principal

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.