Skip to main content

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​

TypeDescription
RETURNFull refund — items returned, money refunded
REPLACEMENTDefective item exchanged for a new one
REPAIRItem 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:

ConditionMeaning
REPAIRItem needs repair
DAMAGEDItem is damaged, cannot be resold
RENEWALItem needs refurbishment
APPROVED_NO_ACTIONApproved 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​

ModuleRelationship
SalesReads original sale to validate and extract cost/variant data
InventoriesFetches current inventory costs; event handler adjusts stock on return
Cash RegisterAuto-links returns to open sessions; validates explicit session IDs
LoyaltyEvent handler reverses loyalty points proportionally on refund

Event-Driven Side Effects​

The service emits events after create/update. External handlers react:

  1. customerReturn.create — consumed by:

    • InventoryReturnHandler — adjusts inventory quantities
    • OnSaleRefundedHandler — reverses loyalty points (if applicable)
  2. customerReturn.update — consumed by:

    • InventoryReturnHandler — handles approval-triggered inventory changes

API Endpoints​

MethodPathDescription
POST/customer-returnsCreate a customer return
GET/customer-returnsList returns (paginated, searchable)
GET/customer-returns/:idGet a return by ID
PATCH/customer-returns/:idUpdate a return (status transitions)
DELETE/customer-returns/:idDelete a return

POST /customer-returns​

Creates a return linked to an original sale.

Key behaviors:

  • If sessionId is omitted, auto-links to the cashier's open cash register session
  • If sessionId is provided, validates: session exists, is OPEN, matches location and cashier
  • Enriches return items with variantId/variantLabel from the original sale
  • Calculates cost snapshots (document cost + inventory cost)
  • Emits customerReturn.create event

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:

ParamTypeDescription
sizenumberPage size (0 = no limit)
pagenumberPage number (1-based)
orderBystringSort field: taxId, taxName, taxAddress, locationName
orderstringSort direction: asc or desc
searchstringSearch 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​

  1. Cost snapshots are immutable at creation time — costDetail captures document costs, inventory costs, and origin costs from the sale. This ensures financial accuracy even if inventory costs change later.

  2. Variant enrichment from sale — Return items inherit variantId/variantLabel from the original sale items to maintain product variant traceability without requiring the client to provide them.

  3. 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.

  4. Event-driven side effects — Inventory adjustments and loyalty point reversals are decoupled via events, keeping the returns module focused on return lifecycle management.