Saltar al contenido principal

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 typesgoods_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 in product_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.

ParameterTypeRequiredDescription
businessIdUUIDYesBusiness scope
productIdUUIDYesProduct to query
variantIdUUIDNoFilter by variant
sourceTypeenumNoFilter by source type
fromDateISO 8601NoEntries on or after this date
toDateISO 8601NoEntries on or before this date
limitintNoPage size (1–200, default 50)
offsetintNoSkip 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

  1. Append-only — entries are never updated or deleted; provides a complete audit trail.
  2. Numeric precision — costs stored as numeric(20,6) to avoid floating-point drift.
  3. changePercentage computed at query time — not stored, keeping the table insert-only and avoiding stale calculations.
  4. Symbol-based injectionPRODUCT_COST_HISTORY_REPOSITORY Symbol token enables clean dependency inversion in the module.