Inventory API Reference
Complete API contracts for the Inventory module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Inventory - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers.
Scope: Admin stock, configuration, movements, bulk adjust, and import/export job APIs owned by the Inventory module. There is no customer-facing inventory endpoint — stock reaches the storefront through the product response's inventory group.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | admin/stock/inventory-stock-admin.controller.ts, inventory-stock-admin-bulk.controller.ts, admin/job/inventory-job-admin.controller.ts | Routes, methods, guards, permissions, status codes |
| DTOs | dto/*.ts | Validation, defaults, enums |
| Services | inventory-stock-admin.service.ts, inventory-job-admin.service.ts, shared services | Behavior, side effects, errors |
| Schema | packages/db/src/schema/inventory/*.ts | Tables, GENERATED columns, constraints |
| Jobs/cache | packages/jobs/src/index.ts, cache-invalidation.tags.ts | Queue names, payloads, domains |
| Error registry | apps/api/src/common/types/error-codes.ts | INVENTORY_* codes |
2. Module Summary
| Field | Value |
|---|---|
| Module name | inventory |
| Module slug | inventory |
| Primary actors | admin, worker, internal system |
| API surfaces | admin only |
| Base route prefixes | /api/admin/inventory, /api/admin/inventory/jobs |
| Auth model | JwtAuthGuard + RoleGuard |
| Persistence | PostgreSQL (4 tables), Redis (cache), BullMQ (INVENTORY queue via outbox), MongoDB (activity projections) |
| Runtime source of truth | PostgreSQL tables |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
productPublicId | The product a row belongs to. Stock is NOT keyed on it — see variantPublicId | controllers | All stock routes |
variantPublicId | The row's real identity. inventory's primary key is variant_id, so a product with three variants owns three stock rows. Every response names its variant, and the variant-scoped routes are addressed by this | inventory-stock-admin.service.ts | All stock responses |
variantName, variantSku, isDefaultVariant | Variant identity carried on every stock response, off the innerJoin(product_variant) the query already needs to resolve the row. variantName is null when the variant IS the product — render the product's name, never a literal like "Default" | same | All stock responses |
productName, productSku | Product identity carried on every stock response, off the innerJoin(products) the query already performs. Added so a list row is readable without a per-row lookup — that N+1 exceeds the 30/min ADMIN_READ budget on the second page view and degrades a list to raw uuids | same | All stock responses |
availableQuantity | total - reserved, GENERATED — read-only | schema | All responses |
stockStatus | GENERATED enum (not_tracked, in_stock, low_stock, out_of_stock, overselling) | schema | Responses |
trackInventory | Whether the row's counters are authoritative | schema | Configuration |
allowOversell | Permits negative total_quantity (units owed) | schema | Configuration, guards |
version | Optimistic lock | schema | Configuration |
reservationKey | Globally unique idempotency key for reservations | reservation service | Reservations (internal) |
cursor | Opaque keyset token | cursor util | List/movements |
4. API Surface Map
| Surface | Method | Path | Actor | Auth/Guard | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|---|
| Admin | GET | /api/admin/inventory | Admin | JWT+Role | Inventory_READ | InventoryStockAdminController | Cursor-paginated list |
| Admin | GET | /api/admin/inventory/products/:productPublicId | Admin | JWT+Role | Inventory_READ | same | The product's default variant's row |
| Admin | PATCH | /api/admin/inventory/products/:productPublicId/configuration | Admin | JWT+Role | Inventory_UPDATE | same | Tracking/oversell/threshold, default variant |
| Admin | POST | /api/admin/inventory/products/:productPublicId/adjust | Admin | JWT+Role | Inventory_UPDATE | same | Manual adjustment, default variant |
| Admin | GET | /api/admin/inventory/products/:productPublicId/movements | Admin | JWT+Role | Inventory_READ | same | Movement ledger, all variants |
| Admin | GET | /api/admin/inventory/products/:productPublicId/variants | Admin | JWT+Role | Inventory_READ | same | Every variant's row, unpaginated |
| Admin | GET | /api/admin/inventory/products/:productPublicId/variants/:variantPublicId | Admin | JWT+Role | Inventory_READ | same | One variant's row |
| Admin | PATCH | /api/admin/inventory/products/:productPublicId/variants/:variantPublicId/configuration | Admin | JWT+Role | Inventory_UPDATE | same | One variant's configuration |
| Admin | POST | /api/admin/inventory/products/:productPublicId/variants/:variantPublicId/adjust | Admin | JWT+Role | Inventory_UPDATE | same | One variant's adjustment |
| Admin | GET | /api/admin/inventory/products/:productPublicId/variants/:variantPublicId/movements | Admin | JWT+Role | Inventory_READ | same | One variant's ledger |
| Admin | POST | /api/admin/inventory/bulk/adjust | Admin | JWT+Role | Inventory_UPDATE | InventoryStockAdminBulkController | Bulk adjust |
| Admin | POST | /api/admin/inventory/jobs/import | Admin | JWT+Role | Inventory_UPDATE | InventoryJobAdminController | CSV import job |
| Admin | POST | /api/admin/inventory/jobs/export | Admin | JWT+Role | Inventory_READ | same | Export job |
| Admin | POST | /api/admin/inventory/jobs/:publicId/cancel | Admin | JWT+Role | Inventory_UPDATE | same | Cancel job |
| Admin | GET | /api/admin/inventory/jobs | Admin | JWT+Role | Inventory_READ | same | Job list |
| Admin | GET | /api/admin/inventory/jobs/:publicId | Admin | JWT+Role | Inventory_READ | same | Job detail |
The /products/ segment is deliberate, so a future /admin/inventory/reservations leaf cannot collide with a bare :productPublicId route.
There are two address forms over the same rows, and the distinction is not cosmetic. inventory's primary key has been variant_id since migration 0021, so "the stock of product X" is a complete address only while X has one variant.
/products/{id}/…resolves the product's default variant, on reads and writes alike./products/{id}/variants/{id}/…names the row explicitly. This is what a screen offering a variant picker uses. The product stays in the path so the variant is resolved within it — a variant of another product addressed here is a404, not a write whose movement-ledger entry and cache invalidation both name the wrong product.
The product-addressed detail read used to be … WHERE products.public_id = $1 LIMIT 1 with no ORDER BY, returning an arbitrary one of the N rows, while the writes resolved the default. Read and write could name different rows: the screen showed 12 units, an adjustment of +5 was accepted, and a different variant became 17. Nothing in the response named either row, so nothing on screen could reveal it, and it stayed invisible because every product in the database had exactly one variant.
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Admin | JwtAuthGuard, RoleGuard, IpThrottlerGuard | req.user | Inventory_READ / Inventory_UPDATE | No | superadmin bypasses |
Rate limits: ADMIN_READ 30/min (list/detail/movements/jobs), ADMIN_WRITE 10/min (configuration/adjust/cancel), ADMIN_BULK_WRITE 5/min (bulk adjust), ADMIN_ASYNC_JOB_SUBMIT 10/hour (import/export). Idempotency: Idempotency-Key required on job submits (scopes inventory-job-import / inventory-job-export); missing → 400 IDEMPOTENCY_KEY_REQUIRED, replay-mismatch → 409, in-flight → 409.
6. DTO and Model Reference
6.1 AdjustInventoryAdminDto
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
delta | number | Yes | N/A | @IsInt, @NotEquals(0); may be negative | -2 | adjust-inventory-admin.dto.ts |
reason | enum | Yes | N/A | @IsIn(inventoryMovementReasonEnum) | "damage" | |
note | string | No | NULL | max 1000 | "Two units damaged in transit" |
The field is delta, a signed change and not a new total, and zero is rejected outright — a movement that moves nothing is a ledger row that explains nothing.
6.2 UpdateInventoryConfigurationAdminDto
| Field | Type | Required | Default | Validation | Example |
|---|---|---|---|---|---|
trackInventory | boolean | No | — | @IsBoolean | true |
allowOversell | boolean | No | — | @IsBoolean | false |
lowStockThreshold | number | No | — | @IsInt, >= 0 | 5 |
version | number | Yes | — | @IsInt, >= 1 | 3 |
6.3 Bulk adjust
{ items: [{ productPublicId, variantPublicId?, delta }], reason, note? } — reason and note apply to the whole batch; a caller adjusting for two reasons sends two requests. Max 100 items (409 INVENTORY_BULK_LIMIT_EXCEEDED).
variantPublicId is optional and means the product's default variant when omitted — correct for a product sold in one configuration, and an arbitrary choice for anything else. A multi-variant product should always name one.
Result:
{
"succeeded": [{ "productPublicId": "018f…", "variantPublicId": "019b…" }],
"failures": [
{ "productPublicId": "018f…", "variantPublicId": "019b…", "errorCode": "INVENTORY_ADJUSTMENT_WOULD_GO_NEGATIVE" }
]
}variantPublicId on a success row is the variant the item resolved to, not the one it asked for, so an item that named none still reports the counter that moved. It is null on a failure row only when the failure is why no variant could be resolved — an unknown product, or one with no default variant.
Dedup is by (product, variant), not by product, so two items naming two variants of one product are two counters and both apply. A second dedupe runs after resolution, keyed on the resolved variant_id: the pre-resolution key cannot tell that {product} and {product, its default variant} are the same row, and applying both would double a delta on a duplicate the caller could not see was one.
6.4 Query DTOs
FetchInventoryAdminDto: cursor, limit (1..100, default 20), plus filters. FetchInventoryMovementsAdminDto: cursor, limit. FetchInventoryJobDto: kind, entity, status, page/size.
7. Enum Reference
| Enum | Value | Meaning | Runtime Effect | Source |
|---|---|---|---|---|
inventory_stock_status | not_tracked | Tracking off | Generated; product projection skipped | enums.ts |
inventory_stock_status | in_stock / low_stock / out_of_stock / overselling | Availability | Generated from counters + threshold | |
inventory_reservation_status | active / released / finalized / expired | Reservation lifecycle | Guarded transitions | |
inventory_movement_kind | adjustment, reservation_created, reservation_released, reservation_finalized, reservation_expired, correction | Ledger entry type | Written same-tx | |
inventory_job_status | queued / processing / completed / failed / cancelled | Job lifecycle | Lease claim |
8. Endpoint Reference
8.1 POST /api/admin/inventory/products/:productPublicId/adjust
Purpose
Manually adjust a product's stock — restock, write-off, physical-count fix. The only way to change counters (besides import and the reconciler).
Auth and Permissions
JwtAuthGuard, RoleGuard, IpThrottlerGuard; Inventory_UPDATE; ADMIN_WRITE 10/min; no idempotency header.
Request
Body per §6.1.
Response
200 — inventory envelope:
{
"message": "Inventory adjusted successfully",
"data": {
"productPublicId": "018f4e2a-…",
"productName": "Gaming Laptop",
"productSku": "HH0001",
"variantPublicId": "019b0b5f-…",
"variantName": "32GB / Black",
"variantSku": "HH0001-32-BLK",
"isDefaultVariant": true,
"publicId": "019fcc90-…",
"totalQuantity": 18,
"reservedQuantity": 2,
"availableQuantity": 16,
"stockStatus": "in_stock",
"trackInventory": true,
"allowOversell": false,
"lowStockThreshold": 5,
"version": 3
},
"errorCode": null
}availableQuantity and stockStatus are GENERATED — read-only. publicId is the inventory row's own id; variantPublicId is what the variant-scoped routes are addressed by. The three ids are distinct and conflating them is a 404.
Side Effects
Guarded UPDATE + inventory_movement row in the same transaction; product.stock_status projection; outbox → Mongo activity projection; cache invalidation.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | INVENTORY_QUANTITY_INVALID | Malformed delta |
| 404 | INVENTORY_NOT_FOUND | No row (create is implicit on first adjust) |
| 409 | INVENTORY_ADJUSTMENT_WOULD_GO_NEGATIVE | Would go negative without oversell |
| 409 | INVENTORY_INSUFFICIENT_STOCK | Guarded operation lacks stock |
Edge Cases
First adjustment on an untracked row auto-enables tracking. Oversell rows may go negative (units owed). A zero delta is rejected by the DTO (@NotEquals(0)).
Without variantPublicId this moves the product's default variant. On a multi-variant product that is a real choice being made silently, so a screen that lets an operator see one variant's count must adjust through the variant-scoped route, or it will show one row and move another.
8.2 PATCH /api/admin/inventory/products/:productPublicId/configuration
Purpose
Enable/disable tracking, allow/disallow oversell, set the low-stock threshold.
Request
Body per §6.2.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 409 | INVENTORY_VERSION_CONFLICT | Stale version |
| 409 | INVENTORY_OVERSELL_DISABLE_BLOCKED | Disabling oversell while units are owed |
| 409 | re-enable tracking on a row in debt | Refused until debt resolved |
8.3 The read surface
GET /api/admin/inventory — cursor-paginated list, one row per variant. GET /products/:productPublicId — the default variant's row, 404 INVENTORY_NOT_FOUND when there is none. GET /products/:productPublicId/movements — the ledger, newest first, cursor-paginated. All Inventory_READ / ADMIN_READ 30/min.
8.3a The variant-scoped read surface
GET /products/:productPublicId/variants returns every variant's row for one product, unpaginated — the row count is the product's variant count, which is bounded far below a page, and this is what a variant picker reads. Ordered by the variant's own position, so it matches the order the product screen lists its variants in; an operator comparing the two screens is comparing the same list.
GET /products/:productPublicId/variants/:variantPublicId is the detail read, and …/movements is that variant's ledger alone. The product-scoped ledger answers "what happened to this product"; the variant-scoped one answers "where did these units go", which a combined ledger cannot.
Every variant-scoped route resolves the variant within the product, so a variant belonging to another product is 404 INVENTORY_NOT_FOUND rather than a cross-product read.
8.4 POST /api/admin/inventory/bulk/adjust
200 with a per-item result; up to 100 items; one failure never rolls back the batch, including an unresolvable product or variant — that used to throw inside the transaction and discard the items that had already applied.
Writes are ordered by variant_id ascending, not product_id. inventory's row lock is taken on its primary key, which is variant_id; ordering by product leaves the lock order between two variants of one product unconstrained, which is all a deadlock (SQLSTATE 40P01) needs.
Resolution runs as a separate pass over the whole batch before anything is written — the variant ids are not known until every item is resolved, and the apply pass has to run in their order.
8.5 POST /api/admin/inventory/jobs/import
multipart/form-data (file + entity). CSV only (unlike catalog/products), 25 MB, 50,000 rows; required columns productPublicId (UUID v7) + quantityDelta (integer), optional note. Idempotency-Key required; ADMIN_ASYNC_JOB_SUBMIT 10/hour. Returns 200 with the queued job.
8.6 POST /api/admin/inventory/jobs/export
JSON body; Idempotency-Key required; > 50,000 matching rows fails the job (INVENTORY_EXPORT_TOO_LARGE); zero-row export succeeds with a header-only file.
8.7 POST /api/admin/inventory/jobs/:publicId/cancel
200 with the job row; 404 INVENTORY_JOB_NOT_FOUND; 409 INVENTORY_JOB_NOT_CANCELLABLE from a terminal state. Cooperative — a processing import aborts and rolls back.
8.8 GET /api/admin/inventory/jobs(/:publicId)
List (paginated, filters kind/entity/status) and detail (adds startedAt, isStalled, retained errors).
8.9 GET /api/admin/inventory/jobs/import-template
Downloads the column template for an import file — header row only, no example row, because a downloadable import file is eventually uploaded unmodified and a data row would create junk. Zero rows is a no-op.
entity is inventory — required even though it is the only value, so the three template routes read alike.
Permission Inventory_READ — the file carries no data, only column names. Rate limit ADMIN_READ 30/min. Returns 200 as text/csv; charset=utf-8 with Content-Disposition: attachment. An unknown entity fails DTO validation with 400.
Columns: productPublicId, quantityDelta, note. quantityDelta is a signed change, not a new total.
The list is generated from the same constant the row builder's fields are typed against, so a column cannot silently diverge from what the parser reads: satisfies rejects a column that is not a row field, an Exclude assertion rejects a row field with no column, and a round-trip spec parses the emitted template back through the real builder.
Route ordering is load-bearing — this literal segment is declared above @Get(":publicId") in the controller. Declared after it, the wildcard swallows the path and the request fails as "publicId must be a UUID".
9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence (adjust)
9.3 Error Branch (adjust)
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
GET /api/admin/inventory | keyset cursor | 20 | 100 | (sortKey, variantId) | stock status, tracking | per page |
GET /api/admin/inventory/products/:id/movements | keyset cursor | 20 | 100 | newest first | kind | per page |
GET /api/admin/inventory/jobs | offset page/size | 20 | 100 | createdAt | kind/entity/status | offset cap |
No search endpoint exists; the inventory list supports stock-status and tracking filters only.
The list's cursor tiebreaker is variant_id, and a cursor carrying anything else is rejected with 400 INVENTORY_PAGINATION_CURSOR_INVALID. A keyset cursor is correct only when its tiebreaker is unique. product_id was unique while it was the primary key; after migration 0021 two variants of one product share it, and a page boundary between them silently skips a row or repeats one. Rejecting rather than ignoring matters too: an unmappable cursor column drops the WHERE clause, which re-serves page 1 with a fresh page-1 cursor — a client that follows nextCursor loops forever. Every sort index in inventory.ts is (sort_column, variant_id) for the same reason.
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details | Source |
|---|---|---|---|
| Redis cache | Yes | inventory cache domain; invalidated on every write | cache-invalidation.tags.ts |
| BullMQ | Yes | INVENTORY queue: 5 job names; one worker; import/export via outbox; cron sweep + reconcile | packages/jobs/src/index.ts |
| MongoDB | Yes | Activity/analytics projections via outbox — never a ledger | inventory-projection.service.ts |
| External API | No | — | — |
13. Mandatory Deep API Documentation Pack
13.1 Route-by-Route Completeness Matrix
| Route | Controller Method | DTOs | Service Method | Guards | Permissions | Cache | Jobs | DB Touches | Errors | Documented? |
|---|---|---|---|---|---|---|---|---|---|---|
GET /api/admin/inventory | findAll | FetchInventoryAdminDto | InventoryStockAdminService.findAll | JWT+Role+IpThrottle | Inventory_READ | inventory | — | inventory | — | Yes |
GET /products/:productPublicId | findOne | InventoryProductParamsDto | …findOne | same | Inventory_READ | inventory | — | inventory | 404 | Yes |
PATCH /products/:productPublicId/configuration | updateConfiguration | UpdateInventoryConfigurationAdminDto | …updateConfiguration | same | Inventory_UPDATE | invalidate | projection | inventory | 409 | Yes |
POST /products/:productPublicId/adjust | adjust | AdjustInventoryAdminDto | …adjust | same | Inventory_UPDATE | invalidate | projection | inventory, movement | 400/404/409 | Yes |
GET /products/:productPublicId/movements | findMovements | FetchInventoryMovementsAdminDto | InventoryStockMovementsAdminService.findMovements | same | Inventory_READ | — | — | movement | 404 | Yes |
GET /products/:productPublicId/variants | findAllForProduct | InventoryProductParamsDto | …findAllForProduct | same | Inventory_READ | inventory | — | inventory | 404 | Yes |
GET /products/:productPublicId/variants/:variantPublicId | findOneVariant | InventoryVariantParamsDto | …findOne | same | Inventory_READ | inventory | — | inventory | 404 | Yes |
PATCH /products/:productPublicId/variants/:variantPublicId/configuration | updateVariantConfiguration | InventoryVariantParamsDto, UpdateInventoryConfigurationAdminDto | …updateConfiguration | same | Inventory_UPDATE | invalidate | projection | inventory | 404/409 | Yes |
POST /products/:productPublicId/variants/:variantPublicId/adjust | adjustVariant | InventoryVariantParamsDto, AdjustInventoryAdminDto | …adjust | same | Inventory_UPDATE | invalidate | projection | inventory, movement | 400/404/409 | Yes |
GET /products/:productPublicId/variants/:variantPublicId/movements | findVariantMovements | InventoryVariantParamsDto, FetchInventoryMovementsAdminDto | InventoryStockMovementsAdminService.findMovements | same | Inventory_READ | — | — | movement | 404 | Yes |
POST /bulk/adjust | bulkAdjust | bulk DTO | InventoryStockAdminBulkService.bulkAdjust | same | Inventory_UPDATE | invalidate | projection | inventory, movement | 409 | Yes |
POST /jobs/import | submitImport | SubmitInventoryImportDto | InventoryJobAdminService.submitImport | JWT+Role+IpThrottle+Idempotency | Inventory_UPDATE | — | outbox → inventory.import_stock | inventory_job, outbox_events | 400/409 | Yes |
POST /jobs/export | submitExport | SubmitInventoryExportDto | …submitExport | same | Inventory_READ | — | outbox → inventory.export_stock | inventory_job, outbox_events | 400/409 | Yes |
POST /jobs/:publicId/cancel | cancel | InventoryJobParamsDto | …cancel | JWT+Role+IpThrottle | Inventory_UPDATE | — | — | inventory_job | 404/409 | Yes |
GET /jobs | findAll | FetchInventoryJobDto | …findAll | JWT+Role+IpThrottle | Inventory_READ | — | — | inventory_job | — | Yes |
GET /jobs/:publicId | findById | InventoryJobParamsDto | …findById | JWT+Role+IpThrottle | Inventory_READ | — | — | inventory_job | 404 | Yes |
13.2 Request/Response Exhaustiveness
Covered in §8: minimal adjust request (§6.1/8.1), full configuration request (§6.2/8.2), success responses (§8.1), empty-list behavior (cursor lists return data: [], nextCursor: null), validation error (400 INVENTORY_QUANTITY_INVALID), domain errors (§8 error tables), rate-limit behavior (429), permission errors (403).
13.3 API Diagram Pack
Route ownership (§9.1), sequence per endpoint family (§9.2, backend §7), activity diagrams per mutation family (§9.3, feature §6.1), error decision trees (§9.3), async/job flow (backend §7 + §9), cache flow (backend §8).
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Admin panel | Guards (oversell disable, debt re-enable), version lock, per-item bulk failures | 409 codes → specific messages; re-read on version conflict | Stable |
| Product domain (internal) | inventory response group; stock PATCH narrowing | 409 INVENTORY_STOCK_STATUS_DERIVED → adjust via inventory | Stable |
| Future cart/checkout | Reservation contract (reservation_key, TTL, release/finalize) | 409 ALREADY_SETTLED on double finalize | Stable (internal) |
| QA | GENERATED columns read-only, oversell semantics, zero-row export | Reproduce via exact codes | Stable |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
| Product-addressed routes | /products/:productPublicId/... | Inventory-id routes | Future reservation leaf cannot collide | Longer paths | Documented |
| Cursor pagination | Keyset | Offset | Stable ordering | Sort-bound cursors | Loud 400 |
| Generated columns | DB-computed availability | Service-computed | No stale-write path | 428C9 on misuse | Documented |
| No guest endpoints | Admin-only surface | Public stock reads | Stock reaches storefront via product group | — | Documented |
| Version lock | Optimistic | Last-write-wins | Config races on shared row | 409 churn | Re-read/retry |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? | Compatibility Plan |
|---|---|---|---|---|---|
inventory group populated | Web/mobile/admin | Assembler | None (was null) | No | Reserved-null contract |
PATCH /admin/products/:publicId/stock 409 | Admin panel | resolveStockStatusForWrite | None | No | Untracked unchanged |
14. Zero-Omission API Checklist
- Every controller route is documented (§4, §8, §13.1).
- Every parent route prefix and runtime URL is documented (§2, §4).
- Every DTO field, enum, default, transform and validator is documented (§6, §7).
- Every response field, nullable field, generated field and omitted raw entity field is documented (§8.1).
- Every auth, guard, permission, public decorator and guest identity branch is documented (§5).
- Every success, validation, auth, permission, not-found, conflict, rate-limit and server-error branch is documented (§8).
- Every DB read/write, cache invalidation, queue job, audit log and external call is documented (§11, backend §8/§9).
- Every route has examples for minimal request, success response and representative failures (§8).
- Every endpoint family has route, sequence, activity and error diagrams (§9, backend §7).
- Every tradeoff and compatibility risk is documented (§13.5, §13.6).
- The API doc links to backend and features/flows (§1, See Also).
15. Integration Checklist
- Every route from controllers is documented.
- Every DTO field is documented.
- Every enum value is documented.
- Every response envelope is documented.
- Every error code is documented.
- Every auth guard and permission is documented.
- Every cache key, queue job and external call is documented.
- Every diagram matches the current code.
- The API doc links to backend and features/flows.
See Also
- Backend doc: /docs/developer/inventory/backend
- Features and flows doc: /docs/developer/inventory/feature
- TDD: not yet published