Skip to content

Architecture

1. System Context (C4 L1)

2. Container View (C4 L2)

3. Component View (C4 L3) — Internal Layering

LayerResponsibility
Routes/inventory-stocks, /purchase-orders, … (15 in RestPaths)
ControllersAuth gate (JWT / BASIC), permission check, DTO mapping
ServicesBusiness logic, transactions, event emission, idempotency guards
WorkersInventoryWorkerService, MaterialWorkerService — stateless Kafka handlers
RepositoriesDrizzle queries, soft-delete, atomic adjusters (adjustStock)
ComponentsKafka producer/consumer, WebSocket, Redis cache

4. State Machines Index

EntityStatesDiagram
PurchaseOrderDRAFT, PROCESSING, RECEIVED, COMPLETED, CLOSED, CANCELLED
MaterialRecipeDRAFT, ACTIVATED, DEACTIVATED
Vendor / VendorItemACTIVATED, DEACTIVATED, ARCHIVED
InventoryLocationNEW, ACTIVATED, DEACTIVATED, ARCHIVED
InventoryTicketDRAFT, SUBMITTED, APPROVED, IN_PROGRESS, COMPLETED, CANCELLED

PurchaseOrder

FromEventToGuards
DRAFTconfirmPROCESSINGitems length ≥ 1
PROCESSINGreceive(items)RECEIVEDpartial fill
PROCESSINGreceive(items)COMPLETEDall items receivedQuantity ≥ quantity
RECEIVEDreceive(items)RECEIVED / COMPLETEDre-receive remaining
RECEIVED / COMPLETEDcompleteCOMPLETEDidempotent
RECEIVED / COMPLETEDcloseCLOSEDterminal
PROCESSINGrevertDRAFTno items received yet
any non-terminalcancelCANCELLEDterminal

MaterialRecipe

Vendor / VendorItem

InventoryLocation

InventoryTicket

5. Runtime Scenarios

5.1 Sale Payment → Stock Deduct

5.2 Kitchen Ticket Item READY → Material Consume

5.3 PO Confirm → Receive → Complete

5.4 Merchant CDC → Default Location Seed

5.5 ProductVariant CDC seed — COMBO is auto-skipped

InventoryWorkerService.handleProductVariantCDC only seeds InventoryItem for stockable variant types. ProductVariantTypes.COMBO is not in STOCKABLE_SET, so the existing guard

ts
if (!ProductVariantTypes.isStockable(productVariant.type)) return;

already excludes combo variants — no category lookup, no extra branch. A combo variant is virtual; its components carry stock.

Combo deduction flows through the existing per-item path at payment-success because the cart-add layer (@nx/sale) already inserts the leaf components as SaleOrderItem children with leadItemId. The worker sees those as ordinary PRODUCT_VARIANT items and deducts them normally.

ADR: ./decisions/0006-combo-explosion-at-cart-add.

6. Crosscutting Concerns

ConcernHow this service handles it
AuthNJWT (ES256, JWKS pulled from identity); falls back to BASIC for service-to-service
AuthZCasbin via PolicyDefinitionService (cached in Redis) — <Resource>.<action> codes from controllers/permissions.ts
i18njsonb columns with { en, vi } shape; locale resolved from Accept-Language
LoggingIGNIS structured logger; key: %s format; topic/partition/offset on Kafka
IdempotencyPer (referenceType, referenceId, inventoryStockId) lookup in InventoryTracking before write
AtomicityInventoryStockRepository.adjustStock — single SQL UPDATE with optional forceNonNegative
Soft-deleteSoftDeletableRepository (deletedAt); audit trail (InventoryTracking) NOT soft-deletable
IDsSnowflake via IdGenerator, worker 5
TransactionsService methods accept optional transaction param; aggregate ops scoped via _withTransaction helper
Merchant scopingMerchantScopedService injects merchantId filter into all reads/writes for non-system roles

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.