Replacement Issue Notes Module
Overview
The replacement-issue-notes module manages replacement issue notes — documents created when a business issues replacement goods to a customer following an approved customer return. It tracks the lifecycle from creation through inventory reservation to completion, ensuring sufficient stock exists before committing replacements.
Domain Concepts
Status Lifecycle
PENDING → RESERVE_STOCK → COMPLETED
- PENDING — note created, replacement items listed but not yet reserved
- RESERVE_STOCK — inventory validated and reserved; cost snapshot captured
- COMPLETED — replacement goods issued to customer
Inventory Validation
When transitioning from PENDING to RESERVE_STOCK, the service:
- Extracts items from the note's
detailJSON - Groups items by location and product
- Queries current inventory quantities and costs
- Rejects the transition if any item has insufficient inventory (HTTP 422)
- Captures an immutable cost snapshot in
costDetail
Cost Snapshot
The costDetail field stores a point-in-time snapshot of:
- documentCost — aggregated cost per grouped item (with WAC unit costs)
- inventoryCosts — raw inventory rows at the time of reservation
This ensures accurate cost attribution regardless of future price changes.
Architecture
replacement-issue-notes/
├── replacement-issue-notes.module.ts
├── domain/
│ └── replacement-issue-notes-repository.domain.ts # Port interface + sortable keys + enum re-exports
├── application/
│ ├── replacement-issue-notes.service.ts # Use cases
│ └── events/
│ └── on-update-replacement-issue-note.event.ts # Update event payload
├── infrastructure/
│ └── replacement-issue-notes.repository.ts # Kysely adapter
└── interfaces/
├── replacement-issue-notes.controller.ts # HTTP endpoints
├── dtos/
│ ├── create-replacement-issue-note.dto.ts
│ └── update-replacement-issue-note.dto.ts
└── query/
└── paginate-replacement-issue-notes.query.ts
Layer Dependencies
- Domain — framework-agnostic: repository interface, sortable keys, enum re-exports
- Application — orchestrates: inventory validation, cost calculation, event emission
- Infrastructure — Kysely queries implementing
IReplacementIssueNotesRepository - Interfaces — thin controller mapping HTTP to service calls
Cross-Module Integration
| Module | Relationship |
|---|---|
| Customer Returns | Each note links to a customerReturnId |
| Inventories | Queries inventory for availability and cost during RESERVE_STOCK transition |
| Event Listeners | Emits replacementIssueNote.update for downstream side effects (e.g., inventory adjustments) |
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /replacement-issue-notes | Create a replacement issue note |
GET | /replacement-issue-notes | List with pagination, search, sort |
GET | /replacement-issue-notes/:id | Get by ID |
PATCH | /replacement-issue-notes/:id | Update (status transitions) |
DELETE | /replacement-issue-notes/:id | Delete a note |
Query Parameters (GET list)
| Param | Type | Description |
|---|---|---|
size | number | Page size (0 = no limit) |
page | number | Page number (1-based) |
orderBy | string | Sort field: status, taxId, taxName, taxAddress, locationName |
order | string | Sort direction: asc or desc |
search | string | Search across status, taxId, taxName, taxAddress, locationName |
Status Transition via PATCH
To reserve stock:
{
"status": "RESERVE_STOCK",
"updatedBy": "<employee-uuid>"
}
To mark completed:
{
"status": "COMPLETED",
"updatedBy": "<employee-uuid>"
}
Error Responses
| Status | Condition |
|---|---|
| 400 | Validation error (missing/invalid fields) |
| 401 | Unauthorized (missing or invalid Firebase token) |
| 404 | Replacement issue note not found |
| 422 | Insufficient inventory for one or more items (RESERVE_STOCK transition) |
Design Decisions
-
Event-driven updates — Every update emits
replacementIssueNote.updatewith both previous and new state, enabling downstream listeners (e.g., inventory adjustment, audit logging) without coupling. -
Cost snapshot immutability — Cost details are captured at RESERVE_STOCK time and stored as JSON. This prevents cost drift if inventory prices change after reservation.
-
Detail as JSON — The
detailcolumn stores the full line-item structure as JSONB, matching the pattern used across other document modules (sales, customer returns, purchase orders).