Product Cost History
Append-only audit trail that records every cost change for products and product variants.
Domain Concepts
- Cost entry — An immutable record capturing the previous cost, new cost, source of change, and context (reference, reason, quantity).
- Source types —
goods_received_note,manual_adjustment,inventory_adjustment,supplier_return,opening_balance,purchase,purchase_order. - Dual snapshot pattern — Current cost lives on
product.cost/productVariant.avgCost; the full history is inproduct_cost_history.
Architecture
product-cost-history/
├── product-cost-history.module.ts
├── domain/
│ └── product-cost-history-repository.domain.ts # Port interface + query params
├── application/
│ └── product-cost-history.service.ts # Use cases (create, manual update, query)
├── infrastructure/
│ └── product-cost-history.repository.ts # Kysely adapter
└── interfaces/
├── product-cost-history.controller.ts # HTTP routes
└── dtos/
├── create-manual-cost-update.dto.ts
└── query-cost-history.dto.ts
Follows hexagonal architecture: the service depends on the IProductCostHistoryRepository interface (injected via Symbol token), not the concrete repository.
API Endpoints
GET /product-cost-history
Returns paginated cost history for a product or variant.
| Parameter | Type | Required | Description |
|---|---|---|---|
| businessId | UUID | Yes | Business scope |
| productId | UUID | Yes | Product to query |
| variantId | UUID | No | Filter by variant |
| sourceType | enum | No | Filter by source type |
| fromDate | ISO 8601 | No | Entries on or after this date |
| toDate | ISO 8601 | No | Entries on or before this date |
| limit | int | No | Page size (1–200, default 50) |
| offset | int | No | Skip N entries (default 0) |
Response:
{
"data": [
{
"id": "uuid",
"businessId": "uuid",
"productId": "uuid",
"variantId": null,
"previousCostAmount": "40.000000",
"costAmount": "45.500000",
"sourceType": "manual_adjustment",
"reason": "Supplier price increase",
"createdBy": "uuid",
"createdByName": "John Doe",
"createdAt": "2026-03-24T12:00:00.000Z",
"changePercentage": 13.75
}
],
"total": 1,
"limit": 50,
"offset": 0
}
POST /product-cost-history/manual-update
Atomically updates product/variant cost and creates a history entry.
Request:
{
"businessId": "uuid",
"productId": "uuid",
"variantId": "uuid (optional)",
"newCost": 45.50,
"reason": "Supplier price increase effective Q2",
"employeeId": "uuid"
}
Response: 201 Created — returns the created ProductCostHistory entry.
Authorization
- Protected by
AuthGuard(global) +RolesGuard - Resource:
ProductCostHistory - Actions:
Read(GET),Create(POST manual-update)
Integration
The ProductCostHistoryService.createEntry() method is used by InventoriesService to record cost changes during:
- Goods received note processing
- Direct purchases
- Supplier returns
These callers pass an active transaction (trx) to ensure the history entry is committed atomically with the inventory operation.
Design Decisions
- Append-only — entries are never updated or deleted; provides a complete audit trail.
- Numeric precision — costs stored as
numeric(20,6)to avoid floating-point drift. changePercentagecomputed at query time — not stored, keeping the table insert-only and avoiding stale calculations.- Symbol-based injection —
PRODUCT_COST_HISTORY_REPOSITORYSymbol token enables clean dependency inversion in the module.