feat(ADM-018): completed feature
This commit is contained in:
84
work/artifacts/F-016/architect.md
Normal file
84
work/artifacts/F-016/architect.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 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.
|
||||
34
work/artifacts/F-016/documenter.md
Normal file
34
work/artifacts/F-016/documenter.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Documenter — F-016 Inventory module
|
||||
|
||||
## Summary
|
||||
Documented F-016 behavior in this evidence artifact. No project README update was applied because the repository guard allows product/project file edits only during build; the document stage is limited to artifacts/docs outside `project/` in the active guard policy.
|
||||
|
||||
## Public API notes
|
||||
|
||||
Inventory owns sellability state for product variants. Catalog treats inventory as a separate module: variant ids are opaque UUID references, and catalog must not import inventory internals or query `inventory_*` tables directly.
|
||||
|
||||
| Route | Access | Result |
|
||||
| ------------------------------------------------- | ---------- | ------------------------------------------------- |
|
||||
| GET /inventory/:variantId/availability?quantity=1 | public | `200` + `{ available, availableQuantity }` |
|
||||
| PUT /inventory/:variantId/stock | admin only | `200` stock item with updated available quantity |
|
||||
| POST /inventory/:variantId/reservations | admin only | `201` reserved stock, `409` if unavailable |
|
||||
| POST /inventory/:variantId/reservations/release | admin only | `200` released stock, `409` if reserved too small |
|
||||
| POST /inventory/:variantId/reservations/confirm | admin only | `200` sold stock, `409` if reserved too small |
|
||||
|
||||
## Error codes
|
||||
- `INSUFFICIENT_STOCK` — HTTP 409
|
||||
- `INSUFFICIENT_RESERVED_STOCK` — HTTP 409
|
||||
- `INVALID_STOCK_QUANTITY` — HTTP 422
|
||||
|
||||
## Operational notes
|
||||
- Stock counters are `available`, `reserved`, `sold`, and `incoming`; every counter is constrained non-negative in PostgreSQL.
|
||||
- Reserve, release, and confirm are atomic predicate `UPDATE` statements, not application-level read-before-write checks.
|
||||
- `InventoryService` is exported as the public contract for future checkout integration.
|
||||
- Inventory mutations write `inventory_movements` audit rows.
|
||||
|
||||
## Evidence
|
||||
- `work/artifacts/F-016/architect.md`
|
||||
- `work/artifacts/F-016/implementer.md`
|
||||
- `work/artifacts/F-016/reviewer.json`
|
||||
- `work/artifacts/F-016/security.json`
|
||||
- `work/artifacts/F-016/qa.json`
|
||||
38
work/artifacts/F-016/implementer.md
Normal file
38
work/artifacts/F-016/implementer.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Implementer — F-016 Inventory module
|
||||
|
||||
## Summary
|
||||
Implemented an isolated inventory module with atomic stock counters, public `InventoryService`, PostgreSQL persistence, API routes, migration, and regression tests.
|
||||
|
||||
## Files changed
|
||||
- `project/migrations/011_inventory.js`
|
||||
- `project/src/app/build-app.ts`
|
||||
- `project/src/app/tests/inventory.itest.ts`
|
||||
- `project/src/modules/inventory/index.ts`
|
||||
- `project/src/modules/inventory/api/inventory.routes.ts`
|
||||
- `project/src/modules/inventory/application/inventory-service.ts`
|
||||
- `project/src/modules/inventory/domain/errors.ts`
|
||||
- `project/src/modules/inventory/domain/ports.ts`
|
||||
- `project/src/modules/inventory/domain/stock.ts`
|
||||
- `project/src/modules/inventory/infrastructure/pg-inventory-repository.ts`
|
||||
- `project/src/modules/inventory/tests/boundary.test.ts`
|
||||
- `project/src/modules/inventory/tests/inventory-service.test.ts`
|
||||
|
||||
## Acceptance evidence
|
||||
- AC1 concurrent last-unit reservation: `project/src/app/tests/inventory.itest.ts` covers 10 concurrent reservations; exactly one succeeds when DB integration tests are enabled.
|
||||
- AC2 zero-stock reservation: `project/src/app/tests/inventory.itest.ts` covers rejection and non-negative counters; DB CHECK constraints also enforce non-negative stock.
|
||||
- AC3 catalog isolation: `project/src/modules/inventory/tests/boundary.test.ts` scans catalog source for inventory table/import coupling.
|
||||
- AC4 checkout service dependency: `project/src/modules/inventory/index.ts` exports the public `InventoryServicePort` and `InventoryService` for future checkout injection.
|
||||
- `verify.sh`: passed.
|
||||
|
||||
## Commands run
|
||||
- `cd project && npm run typecheck` — passed
|
||||
- `cd project && npm test` — passed; DB integration tests skipped because `TEST_DATABASE_URL` is not set
|
||||
- `cd project && npm run build` — passed
|
||||
- `cd project && npx prettier --write ...` — formatted inventory files
|
||||
- `cd project && npm run lint` — passed
|
||||
- `./scripts/verify.sh` — passed
|
||||
|
||||
## Notes
|
||||
- No new runtime dependency was added.
|
||||
- Reservation correctness uses a single predicate `UPDATE ... WHERE available >= quantity`, not application read-before-write.
|
||||
- Product/variant ids are opaque UUIDs to keep inventory independent from catalog internals.
|
||||
15
work/artifacts/F-016/leader-close.json
Normal file
15
work/artifacts/F-016/leader-close.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"feature_id": "F-016",
|
||||
"agent": "leader",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "F-016 closed with reviewer, security and QA gates approved. Final verify.sh passed.",
|
||||
"evidence": [
|
||||
"reviewer.json verdict APPROVED",
|
||||
"security.json verdict APPROVED",
|
||||
"qa.json verdict APPROVED",
|
||||
"./scripts/verify.sh passed during close",
|
||||
"backlog/features.json updated: F-016 status done and gates true",
|
||||
"work/current.md updated: no active feature, next suggested F-017"
|
||||
],
|
||||
"timestamp": "2026-08-15T16:17:23Z"
|
||||
}
|
||||
54
work/artifacts/F-016/qa.json
Normal file
54
work/artifacts/F-016/qa.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"feature_id": "F-016",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "QA approved. All F-016 acceptance criteria are traced to implementation and executable evidence. The PostgreSQL integration suite passed with TEST_DATABASE_URL, including the concurrent last-unit reservation scenario and migration up/down coverage.",
|
||||
"evidence": [
|
||||
"Concurrent reservation AC covered and executed in project/src/app/tests/inventory.itest.ts",
|
||||
"Zero-stock reservation AC covered and executed in project/src/app/tests/inventory.itest.ts",
|
||||
"Catalog isolation AC covered by project/src/modules/inventory/tests/boundary.test.ts",
|
||||
"InventoryService public interface exported by project/src/modules/inventory/index.ts for future checkout injection",
|
||||
"cd project && TEST_DATABASE_URL='postgres://mdv:mdv_dev_only@localhost:5432/mdv_test' npm run test:integration -- inventory.itest migrations.itest passed: 8 integration files, 36 tests",
|
||||
"cd project && npm run lint passed",
|
||||
"cd project && npm run typecheck passed",
|
||||
"cd project && npm run build passed",
|
||||
"cd project && npm test passed: 19 passed, 8 skipped without TEST_DATABASE_URL",
|
||||
"./scripts/verify.sh passed"
|
||||
],
|
||||
"acceptance": [
|
||||
{
|
||||
"criterion": "Given 10 concurrent reservations for the last unit When all settle Then exactly 1 succeeds and 9 get unavailable",
|
||||
"status": "PASS",
|
||||
"evidence": "inventory.itest.ts executes 10 Promise.allSettled reservations against one available unit; integration run passed and final counters are available=0, reserved=1"
|
||||
},
|
||||
{
|
||||
"criterion": "Given zero stock When reservation requested Then rejected and stock never negative",
|
||||
"status": "PASS",
|
||||
"evidence": "inventory.itest.ts sets available=0, reserve rejects with InsufficientStockError, and DB row remains all non-negative counters"
|
||||
},
|
||||
{
|
||||
"criterion": "Catalog module contains zero references to inventory tables",
|
||||
"status": "PASS",
|
||||
"evidence": "boundary.test.ts scans catalog source for inventory_ table references and inventory internal imports"
|
||||
},
|
||||
{
|
||||
"criterion": "Checkout checks stock only through InventoryService",
|
||||
"status": "PASS",
|
||||
"evidence": "Checkout is not implemented yet; F-016 exports InventoryServicePort/InventoryService as the only public inventory contract for future checkout injection"
|
||||
},
|
||||
{
|
||||
"criterion": "verify.sh green",
|
||||
"status": "PASS",
|
||||
"evidence": "./scripts/verify.sh PASS"
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
"cd project && npm run lint: PASS",
|
||||
"cd project && npm run typecheck: PASS",
|
||||
"cd project && npm run build: PASS",
|
||||
"cd project && npm test: PASS; 19 passed, 8 skipped when TEST_DATABASE_URL absent",
|
||||
"cd project && TEST_DATABASE_URL='postgres://mdv:mdv_dev_only@localhost:5432/mdv_test' npm run test:integration -- inventory.itest migrations.itest: PASS; 8 files, 36 tests",
|
||||
"./scripts/verify.sh: PASS"
|
||||
],
|
||||
"timestamp": "2026-08-15T16:16:08Z"
|
||||
}
|
||||
21
work/artifacts/F-016/reviewer.json
Normal file
21
work/artifacts/F-016/reviewer.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"feature_id": "F-016",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "F-016 review approved. The inventory module is isolated, exposes a public InventoryService contract, uses PostgreSQL CHECK constraints plus atomic predicate updates for reserve/release/confirm, and includes unit, boundary, and DB integration coverage for the required stock scenarios.",
|
||||
"evidence": [
|
||||
"Read work/current.md, architect.md and implementer.md",
|
||||
"Inspected inventory domain/application/infrastructure/API files: public API exported from index.ts; internals stay inside modules/inventory",
|
||||
"Inspected PgInventoryRepository: reserve/release/confirm are single predicate UPDATE statements guarded by available/reserved counters, with movement logging in transactions",
|
||||
"Inspected migration 011_inventory.js: inventory_stock owns counters with non-negative CHECK constraints and inventory_movements records operations",
|
||||
"Inspected build-app.ts wiring: inventory registered only at composition root with injected pool/authenticate",
|
||||
"Inspected boundary test: catalog source scan blocks inventory table references and inventory internal imports",
|
||||
"gentle-ai review mode status: receipt-driven development off globally, ordinary Orquestra gate used",
|
||||
"cd project && npm run lint passed",
|
||||
"cd project && npm run typecheck passed",
|
||||
"cd project && npm run build passed",
|
||||
"cd project && npm test passed; DB integration tests skipped without TEST_DATABASE_URL",
|
||||
"./scripts/verify.sh passed"
|
||||
],
|
||||
"timestamp": "2026-08-15T16:15:17Z"
|
||||
}
|
||||
16
work/artifacts/F-016/security.json
Normal file
16
work/artifacts/F-016/security.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"feature_id": "F-016",
|
||||
"agent": "security",
|
||||
"verdict": "APPROVED",
|
||||
"summary": "Security approved. No new dependencies or secrets were introduced. Inventory mutations require admin authentication, inputs are validated with zod, SQL uses parameterized queries, and database constraints prevent negative stock even under persistence-layer corruption attempts.",
|
||||
"evidence": [
|
||||
"cd project && npm audit --audit-level=high --omit=dev: found 0 vulnerabilities",
|
||||
"Secret scan over project/src/modules/inventory, project/migrations/011_inventory.js and project/src/app/tests/inventory.itest.ts found no credentials",
|
||||
"Reviewed inventory routes: stock mutations require authenticated admin role; availability read exposes only stock availability counters",
|
||||
"Reviewed validation: variantId is z.uuid(); quantities are coerced/validated as positive or non-negative integers before use",
|
||||
"Reviewed repository SQL: all dynamic values are parameterized; no user input is interpolated into SQL text",
|
||||
"Reviewed migration: inventory_stock CHECK constraints enforce non-negative counters at database level",
|
||||
"No new runtime dependency added"
|
||||
],
|
||||
"timestamp": "2026-08-15T16:15:35Z"
|
||||
}
|
||||
Reference in New Issue
Block a user