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
Lot selection (inventory_detail)
Transfer lines can carry a planned origin lot in detail.items[].inventoryDetails (typically one lot per line, FEFO default). This is a plan only:
- Transfer approve still reserves aggregate stock. It does not decrement or lock
inventory_detailrows. - Dispatch approve consumes the planned lots at the origin (
decreaseQuantityInventoryDetailsFromItems, by lot id when present). - Receipt approve recreates or merges lots at the destination from batch/serial identity. Origin row ids are not reused. Missing destination
inventoryandinventory_detailrows are created (or merged by batch/serial) at the destination location.
Partial dispatches scale the inherited lot quantity down to the remaining amount. Extra origin lots can be added on the dispatch note. Dispatch notes keep one line per transfer line (duplicate SKUs keep each line's planned quantity). Receipts inherit lot identity (batch/serial/dates/qty) without the origin id, one receipt line per dispatch line (duplicate SKUs keep each line's dispatched quantity).
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).