Inventory Transfers
Overview
The Inventory Transfers module manages the movement of inventory between business locations. It sits at the center of the transfer lifecycle and coordinates with Transfer Requests, Dispatch Notes, and Goods Receipts to provide end-to-end stock movement tracking.
Each transfer tracks:
- Origin and destination locations
- Items being transferred (products, quantities)
- Cost detail calculated from current inventory costs
- Dispatch summary tracking what has been shipped
- Receipt summary tracking what has been received
- Document number auto-generated for reference
Architecture
inventory-transfers/
├── inventory-transfers.module.ts # NestJS module
├── domain/
│ └── inventory-transfers-repository.domain.ts # Port interface + injection token
├── application/
│ ├── inventory-transfers.service.ts # Use cases
│ ├── handlers/
│ │ └── inventory-transfer-lifecycle.handler.ts # Event handlers (SRP)
│ └── events/
│ ├── on-create-inventory-transfer.event.ts
│ └── on-update-inventory-transfer.event.ts
├── infrastructure/
│ └── inventory-transfers.repository.ts # Kysely adapter
└── interfaces/
├── inventory-transfers.controller.ts # REST controller
├── dtos/
│ ├── create-inventory-transfer.dto.ts
│ └── update-inventory-transfer.dto.ts
└── query/
└── paginate-inventory-transfers.query.ts
Domain Concepts
Transfer Statuses
| Status | Description |
|---|---|
draft | Transfer created but not yet submitted |
pending | Transfer submitted for approval |
approved | Transfer approved — triggers inventory updates |
rejected | Transfer rejected |
Dispatch & Receipt Tracking
Each transfer maintains two summary objects:
- Dispatch Summary: tracks
orderedQuantityvsdispatchedQuantityper item - Receipt Summary: tracks
orderedQuantityvsreceivedQuantityper item
Status progression: pending → partial → dispatched/received
Transfer Lifecycle
Transfer Request (approved)
│
▼
Inventory Transfer (auto-created)
│
├── Dispatch Note (created/approved)
│ └── Updates dispatchSummary + transferDispatchNoteStatus
│
└── Goods Receipt (created/approved)
└── Updates receiptSummary + transferGoodsReceiptStatus
- A Transfer Request is approved → automatically creates an Inventory Transfer
- The transfer status changes to
APPROVED→ triggers inventory quantity updates - Dispatch Notes are created as items ship → dispatch summary is updated
- Goods Receipts are created as items arrive → receipt summary is updated
- When all items are dispatched/received, status becomes
DISPATCHED/RECEIVED
Event Integration
Events Published
inventoryTransfer.create— after creating a transferinventoryTransfer.update— after updating a transfer
Events Consumed (via InventoryTransferLifecycleHandler)
transferRequest.create— auto-creates transfer if request is approvedtransferRequest.update— auto-creates transfer when request status changes to approvedtransferDispatchNote.create/update— updates dispatch summarytransferGoodsReceipt.create/update— updates receipt summary
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /inventory-transfers | Create inventory transfer |
GET | /inventory-transfers | List with pagination |
GET | /inventory-transfers/search | Search with advanced filters |
GET | /inventory-transfers/:id | Get single transfer |
GET | /inventory-transfers/:id/pdf | Download PDF document |
GET | /inventory-transfers/:id/print | HTML print view |
PATCH | /inventory-transfers/:id | Update transfer |
DELETE | /inventory-transfers/:id | Delete transfer |
Search Filters
| Parameter | Type | Description |
|---|---|---|
businessId | UUID | Filter by business |
status | string | Filter by transfer status |
originLocationId | UUID | Filter by origin location |
destinationLocationId | UUID | Filter by destination location |
createdAtFrom/To | ISO date | Filter by creation date range |
transferDateFrom/To | ISO date | Filter by transfer date range |
query | string | Free-text search on document number and location names |
Example: Create Transfer
POST /inventory-transfers
{
"businessId": "uuid",
"status": "draft",
"createdBy": "uuid",
"transferDate": "2026-03-25",
"originLocationId": "uuid",
"originLocationName": "Main Warehouse",
"destinationLocationId": "uuid",
"destinationLocationName": "Retail Store #1",
"detail": {
"items": [
{
"id": "uuid",
"businessId": "uuid",
"locationId": "uuid",
"locationName": "Main Warehouse",
"productId": "uuid",
"productName": "Blue T-Shirt M",
"goodOrService": "B",
"quantity": 10
}
]
},
"note": "Seasonal stock rebalance"
}
Example: Update Transfer Status
PATCH /inventory-transfers/:id
{
"status": "approved",
"updatedBy": "uuid"
}
Design Decisions
-
Event-driven lifecycle: Transfer Request → Inventory Transfer → Dispatch Note / Goods Receipt chain is coordinated via domain events, keeping modules loosely coupled.
-
Cost detail calculation: Cost is computed from current inventory WAC (weighted average cost) at creation time and recalculated if items change on update.
-
Hexagonal injection: Repository is injected via
INVENTORY_TRANSFERS_REPOSITORYsymbol token, allowing easy test mocking and adapter swapping. -
Handler separation: Event handlers live in
handlers/inventory-transfer-lifecycle.handler.tsto keep the service focused on use cases (SRP).