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