Customer Returns Module
Overview
The customer-returns module handles product returns linked to original sales. It supports three return types with a multi-status lifecycle and integrates with inventory, cash register, and loyalty systems via event-driven architecture.
Domain Concepts
Return Types
| Type | Description |
|---|---|
RETURN | Full refund — items returned, money refunded |
REPLACEMENT | Defective item exchanged for a new one |
REPAIR | Item sent for repair and returned to customer |
Status Lifecycle
PENDING → APPROVED → PROCESSED → DELIVERED
↘ REJECTED
ACTIVE → INACTIVE (for immediate returns without approval)
- PENDING — awaiting staff review (REPLACEMENT, REPAIR)
- ACTIVE — immediate return processed (RETURN type)
- APPROVED — staff approved with an
approvalCondition - REJECTED — staff denied the return
- PROCESSED — return physically processed (inventory updated)
- DELIVERED — replacement/repaired item delivered back
- INACTIVE — cancelled/deactivated return
Approval Conditions
When approving a return, staff must specify a condition:
| Condition | Meaning |
|---|---|
REPAIR | Item needs repair |
DAMAGED | Item is damaged, cannot be resold |
RENEWAL | Item needs refurbishment |
APPROVED_NO_ACTION | Approved without physical action |
Architecture
customer-returns/
├── customer-returns.module.ts
├── domain/
│ └── customer-returns-repository.domain.ts # Port interface + sortable keys + enum re-exports
├── application/
│ ├── customer-returns.service.ts # Use cases
│ ├── events/
│ │ ├── on-create-customer-return.event.ts # Creation event payload
│ │ └── on-update-customer-return.event.ts # Update event payload
│ └── __tests__/
│ └── customer-returns.session-linking.spec.ts
├── infrastructure/
│ └── customer-returns.repository.ts # Kysely adapter
└── interfaces/
├── customer-returns.controller.ts # HTTP endpoints
├── dtos/
│ ├── create-customer-return.dto.ts
│ └── update-customer-return.dto.ts
└── query/
└── paginate-customer-returns.query.ts
Layer Dependencies
- Domain — framework-agnostic: repository interface, sortable keys, enum re-exports
- Application — orchestrates: session validation, sale lookup, variant enrichment, cost calculation, event emission
- Infrastructure — Kysely queries implementing
ICustomerReturnsRepository - Interfaces — thin controller mapping HTTP to service calls
Cross-Module Integration
| Module | Relationship |
|---|---|
| Sales | Reads original sale to validate and extract cost/variant data |
| Inventories | Fetches current inventory costs; event handler adjusts stock on return |
| Cash Register | Auto-links returns to open sessions; validates explicit session IDs |
| Loyalty | Event handler reverses loyalty points proportionally on refund |
Event-Driven Side Effects
The service emits events after create/update. External handlers react:
-
customerReturn.create— consumed by:InventoryReturnHandler— adjusts inventory quantitiesOnSaleRefundedHandler— reverses loyalty points (if applicable)
-
customerReturn.update— consumed by:InventoryReturnHandler— handles approval-triggered inventory changes
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /customer-returns | Create a customer return |
GET | /customer-returns | List returns (paginated, searchable) |
GET | /customer-returns/:id | Get a return by ID |
PATCH | /customer-returns/:id | Update a return (status transitions) |
DELETE | /customer-returns/:id | Delete a return |
POST /customer-returns
Creates a return linked to an original sale.
Key behaviors:
- If
sessionIdis omitted, auto-links to the cashier's open cash register session - If
sessionIdis provided, validates: session exists, is OPEN, matches location and cashier - Enriches return items with
variantId/variantLabelfrom the original sale - Calculates cost snapshots (document cost + inventory cost)
- Emits
customerReturn.createevent
Request body:
{
"businessId": "uuid",
"status": "PENDING",
"createdBy": "employee-uuid",
"returnDate": "2026-03-26",
"customerId": "uuid",
"taxId": "17195594",
"taxName": "CUSTOMER NAME",
"taxAddress": "ADDRESS",
"locationId": "uuid",
"locationName": "Main Store",
"totalAmount": 24.00,
"exchangeRate": 7.90,
"totalBaseAmount": 21.43,
"currencyId": "uuid",
"saleId": "uuid",
"returnType": "RETURN",
"reason": "Defective item",
"sessionId": "uuid (optional)",
"detail": {
"items": [
{
"id": "uuid",
"productId": "uuid",
"productName": "COCA COLA",
"quantity": 1.00,
"unitPrice": 12.00,
"total": 12.00
}
]
}
}
GET /customer-returns
Query parameters:
| Param | Type | Description |
|---|---|---|
size | number | Page size (0 = no limit) |
page | number | Page number (1-based) |
orderBy | string | Sort field: taxId, taxName, taxAddress, locationName |
order | string | Sort direction: asc or desc |
search | string | Search across taxId, taxName, taxAddress, locationName |
PATCH /customer-returns/:id
Used for status transitions. The approvalCondition field is optional and only required when approving.
Deactivate a return:
{
"status": "INACTIVE",
"updatedBy": "employee-uuid"
}
Approve with condition:
{
"status": "APPROVED",
"approvalCondition": "DAMAGED",
"updatedBy": "employee-uuid"
}
Design Decisions
-
Cost snapshots are immutable at creation time —
costDetailcaptures document costs, inventory costs, and origin costs from the sale. This ensures financial accuracy even if inventory costs change later. -
Variant enrichment from sale — Return items inherit
variantId/variantLabelfrom the original sale items to maintain product variant traceability without requiring the client to provide them. -
Optional cash register session — Returns can be processed outside of a cash register session (e.g., back-office processing), but when a session is available, the return is linked for shift reconciliation.
-
Event-driven side effects — Inventory adjustments and loyalty point reversals are decoupled via events, keeping the returns module focused on return lifecycle management.