Skip to main content

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​

StatusDescription
draftTransfer created but not yet submitted
pendingTransfer submitted for approval
approvedTransfer approved — triggers inventory updates
rejectedTransfer rejected

Dispatch & Receipt Tracking​

Each transfer maintains two summary objects:

  • Dispatch Summary: tracks orderedQuantity vs dispatchedQuantity per item
  • Receipt Summary: tracks orderedQuantity vs receivedQuantity per 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
  1. A Transfer Request is approved → automatically creates an Inventory Transfer
  2. The transfer status changes to APPROVED → triggers inventory quantity updates
  3. Dispatch Notes are created as items ship → dispatch summary is updated
  4. Goods Receipts are created as items arrive → receipt summary is updated
  5. 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_detail rows.
  • 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 inventory and inventory_detail rows 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 transfer
  • inventoryTransfer.update — after updating a transfer

Events Consumed (via InventoryTransferLifecycleHandler)​

  • transferRequest.create — auto-creates transfer if request is approved
  • transferRequest.update — auto-creates transfer when request status changes to approved
  • transferDispatchNote.create/update — updates dispatch summary
  • transferGoodsReceipt.create/update — updates receipt summary

API Endpoints​

MethodPathDescription
POST/inventory-transfersCreate inventory transfer
GET/inventory-transfersList with pagination
GET/inventory-transfers/searchSearch with advanced filters
GET/inventory-transfers/:idGet single transfer
GET/inventory-transfers/:id/pdfDownload PDF document
GET/inventory-transfers/:id/printHTML print view
PATCH/inventory-transfers/:idUpdate transfer
DELETE/inventory-transfers/:idDelete transfer

Search Filters​

ParameterTypeDescription
businessIdUUIDFilter by business
statusstringFilter by transfer status
originLocationIdUUIDFilter by origin location
destinationLocationIdUUIDFilter by destination location
createdAtFrom/ToISO dateFilter by creation date range
transferDateFrom/ToISO dateFilter by transfer date range
querystringFree-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​

  1. Event-driven lifecycle: Transfer Request → Inventory Transfer → Dispatch Note / Goods Receipt chain is coordinated via domain events, keeping modules loosely coupled.

  2. Cost detail calculation: Cost is computed from current inventory WAC (weighted average cost) at creation time and recalculated if items change on update.

  3. Hexagonal injection: Repository is injected via INVENTORY_TRANSFERS_REPOSITORY symbol token, allowing easy test mocking and adapter swapping.

  4. Handler separation: Event handlers live in handlers/inventory-transfer-lifecycle.handler.ts to keep the service focused on use cases (SRP).