Files
mercadodevida/work/artifacts/F-016/architect.md
2026-08-17 22:23:10 +02:00

5.6 KiB

Architect — F-016 Inventory module

Feature

F-016 adds an isolated inventory module that owns stock truth, exposes availability through a public service interface, and performs atomic reserve/release/confirm operations without catalog table coupling.

Design

Module boundaries

Create project/src/modules/inventory/ with the same screaming architecture used by existing modules:

  • domain/stock.ts for stock states, stock item shape, movement/reservation concepts and command inputs.
  • domain/ports.ts for InventoryRepository and InventoryService contracts.
  • domain/errors.ts for unavailable stock and illegal stock operation errors.
  • application/inventory-service.ts implementing InventoryService use cases.
  • infrastructure/pg-inventory-repository.ts for PostgreSQL persistence and transactional atomic operations.
  • api/inventory.routes.ts for minimal admin/testable HTTP endpoints.
  • index.ts exports only the module public API.

Catalog must not import inventory internals. Variant/product ids are stored as opaque UUID references; inventory does not join catalog tables. The composition root is the only place that wires the module.

Data model

Add migration 011_inventory.js:

  • inventory_stock
    • id uuid primary key default gen_random_uuid()
    • variant_id uuid not null unique
    • available integer not null default 0
    • reserved integer not null default 0
    • sold integer not null default 0
    • incoming integer not null default 0
    • timestamps
    • CHECK constraints: every quantity >= 0
  • Optional append-only audit table inventory_movements
    • id, variant_id, operation, quantity, created_at
    • operation in reserve, release, confirm, adjust_available, adjust_incoming

This maps the requested stock states to explicit counters. Negative stock is blocked twice: domain validation rejects invalid commands and PostgreSQL CHECK constraints prevent persistence corruption.

Atomic stock operations

Implement repository operations as single SQL statements guarded by predicates:

  • Reserve: UPDATE inventory_stock SET available = available - $qty, reserved = reserved + $qty WHERE variant_id = $variantId AND available >= $qty RETURNING *.
  • Release: UPDATE inventory_stock SET reserved = reserved - $qty, available = available + $qty WHERE variant_id = $variantId AND reserved >= $qty RETURNING *.
  • Confirm: UPDATE inventory_stock SET reserved = reserved - $qty, sold = sold + $qty WHERE variant_id = $variantId AND reserved >= $qty RETURNING *.

The predicate is the concurrency control. Do not read-then-write in application code; that creates a race. For multi-step operations with movement logging, use one DB transaction in the repository.

Public service interface

Expose InventoryService from the module index:

  • checkAvailability(variantId, quantity): Promise<{ available: boolean; availableQuantity: number }>
  • reserve(input): Promise<StockItem>
  • release(input): Promise<StockItem>
  • confirm(input): Promise<StockItem>
  • setAvailable(input): Promise<StockItem> for admin seeding/adjustment only

Checkout later must receive this interface by dependency injection. For this feature, no checkout module exists yet, so acceptance is satisfied by exporting the interface and documenting that consumers must depend only on InventoryService.

API slice

Add inventory routes behind admin authentication where mutation is business-sensitive:

  • GET /inventory/:variantId/availability?quantity=1 public or authenticated-safe read.
  • PUT /inventory/:variantId/stock admin-only stock adjustment for setup.
  • POST /inventory/:variantId/reservations admin/testable reserve endpoint.
  • POST /inventory/:variantId/reservations/release admin/testable release endpoint.
  • POST /inventory/:variantId/reservations/confirm admin/testable confirm endpoint.

Use zod + parseJson for params/query/body, and map unavailable/invalid operations to stable HTTP errors (409 for unavailable stock, 422 for invalid stock operation when relevant).

Tests

Add focused tests before/with implementation:

  • Unit/application tests for rejecting non-positive quantities and mapping unavailable stock.
  • Integration test with PostgreSQL for 10 concurrent reservations against one available unit: exactly one succeeds, nine fail with unavailable stock, final stock is available=0, reserved=1.
  • Integration test for zero stock reservation: rejected and counters remain non-negative.
  • Boundary test that scans catalog source and fails if it references inventory_ tables or imports from modules/inventory internals.
  • Migration test remains covered by the existing migration suite.

Acceptance trace

  • Concurrent last-unit reservation: predicate update plus integration test proves one success and nine conflicts.
  • Zero stock: domain validation + SQL predicate + DB CHECK constraints keep stock non-negative.
  • Catalog isolation: inventory owns inventory_* tables; catalog has no inventory imports or table references; tested by source scan.
  • Checkout dependency: public InventoryService interface is exported for future checkout injection; no checkout implementation is in scope yet.
  • verify.sh: must pass after gates.

Risks / constraints

  • Do not add new runtime dependencies.
  • Do not model stock as one row per unit; counters with atomic predicate updates are simpler and sufficient for this slice.
  • Do not use application-level locks or read-before-write availability checks for reservation correctness.
  • Product/variant existence validation is intentionally outside inventory; variant ids are opaque references to preserve module isolation.