# Inventory and checkout API Public contracts: shared/inventory.ts. Routes stay in existing layers, with no consumer edits. Operations are technical only: no price input, monetary DTO, valuation, or maintenance cost report. Existing monetary database fields keep storage defaults; existing values are never rewritten. ## Authentication and stock policy Inventory endpoints require authentication. Checkout catalogue, QR, submission and token-protected single-request status are public; the review queue and decisions require ADMIN/SUPERVISOR authentication. Browser writes retain trusted-origin checks. ADMIN/SUPERVISOR manage metadata, corrections, returns, reversals and dispositions. Authenticated technicians can report damaged goods. Anyone can submit kiosk checkout with a required requester name; that self-reported name is separate from a verified user account. All new checkout requests are PENDING, including requests from a signed-in ADMIN/SUPERVISOR. Only a separate authenticated review can approve and post them. Pending does not reserve stock. Collect goods only after posted success. Checkout is independent of Work Orders. Approval rechecks stock and active part (plus account activity for historical authenticated requests); no WO status or priority is consulted. Insufficient stock yields 409 and leaves the request pending. Rejection reason/reviewer/time live in audit and appear in the request DTO. An immediate quarantine hold uses the distinct IMMEDIATE_DAMAGE_HOLD policy: the reporting actor is retained, no supervisor identity is invented, and a technician hold is AUTO_APPROVED as a safety action. This is not checkout approval. Release/scrap is privileged. Net release/scrap counts are ledger-derived; mixed terminal outcomes use CLOSED_MIXED. Reversal restores counts and reopens the case, respecting active repair work. ## Inputs and replay Authenticated Inventory mutations supply `Idempotency-Key` (8-100 ASCII alphanumeric, colon, underscore or hyphen characters). Reuse it with the same validated body/target/actor after an uncertain response. InventoryRequest claims a globally unique key and records a SHA-256 payload fingerprint and business response in the business transaction. Another payload/actor/operation receives 409. A rollback leaves no claim; a committed retry returns the stored result with a fresh HTTP requestId and no new effects. Retain request rows indefinitely in MVP. Stale responses from replay intentionally describe the original result; GET retrieves current state. Part create takes productId, name, group, kind, manufacturer, unit, minimumStock, optional partNumber and initialStock. Names of groups/kinds and product IDs are normalized to uppercase; part numbers may repeat. Database length limits are enforced. Positive initial stock produces a receipt; zero creates no movement. PATCH sends complete metadata and the expected part version. It never accepts physical/quarantine counts. Changing unit after posting is a conflict. Public checkout takes partId, positive integer quantity, required requesterName (max 191) and required notes. It uses a UUID Idempotency-Key, hashed into StockMovement.idempotencyKey; matching retries return the original transaction with current state, without extra effects. GET /checkout/requests/:id/status requires the same UUID in Checkout-Token. No token is returned in DTOs or stored in plain text. workOrderId is rejected in checkout bodies and query filters. Checkout issues have no WO allocation; WO links remain available for maintenance and quarantine operations. Stock movement create accepts RECEIPT, RETURN or ADJUSTMENT and a required note. RETURN requires WO and cannot exceed net issued units. ADJUSTMENT also requires IN/OUT direction and the current balance version; stock is changed by delta, never overwritten. Terminal WO returns are allowed, but new issues to terminal WO are not. Reversals require a reason and negate both original deltas; only one full reversal is permitted and a reversal cannot itself be reversed. ## Reads, QR and export Parts support q/groupId/kindId/stockStatus and standard pagination. Summary is independent of paging. Availability equals physical minus quarantined. The first release uses aggregate mapping before pagination to correctly include legacy balances; large catalogues can replace that read strategy without changing the DTO. A write refuses legacy balances outside the internal room rather than silently moving them. GET /checkout/resolve accepts explicit kind=productId, qrToken or partNumber and value. Ambiguous part numbers conflict; tokens do not grant authorization. GET never posts stock. Existing printed product IDs continue to resolve. Room QR navigation/group UI and authenticated return-to-item handling await separately authorized frontend integration. GET /checkout/requests is restricted to ADMIN/SUPERVISOR; approval=PENDING provides the queue. Posted stock history is separate, with explicit type and signed physical/quarantine deltas. Stock movement list and Excel export accept dateFrom/dateTo as valid YYYY-MM-DD dates (years 1970–9998), filtering postedAt in SystemSetting.timezone (Asia/Jakarta fallback). Both ends are inclusive calendar dates: the end boundary is exclusive midnight of the next local day, including across DST changes. Either bound can be omitted; no bounds means all posted history. Reversed ranges and combining date bounds with the legacy month filter return validation errors. The frontend inventory service forwards these parameters unchanged to both endpoints. Legacy month filters remain supported; checkout request month filters use occurredAt. Order ties use ID. API errors follow existing Indonesian messages and stable codes. GET /stock-movements/export returns XLSX, without an envelope, for all matching posted rows (maximum 10,000; 422 details code EXPORT_LIMIT beyond it). It does not export only the current page, money, or a misleading sum of unlike units. Text stays literal spreadsheet text, including strings beginning with '='. ## Photos and Maintenance Photo GET returns private noncached WebP. POST multipart has file and version fields, plus Idempotency-Key header. DELETE uses version query and the same header. Upload on user Save only; reading a preview does not save anything. PNG/JPEG/WebP up to 2 MiB and 20 megapixels are decoded/re-encoded by existing storage. PhotoKey stays private. Version rejects stale writes. Files are staged outside transactions, then unreferenced files retire through the existing 24-hour cleanup, which now checks Part references. Uncertain commits never trigger eager deletion. Services participating in Maintenance must receive its transaction client, hold the part lock, validate WO eligibility and call the same ledger helpers. `applyInventoryDelta` is the shared balance guard used by checkout and warehouse posting; it is not a public HTTP balance-edit API. `netInventoryUsage` derives posted ISSUE/RETURN signed effects, including reversals. It neither writes nor treats pending as usage. No WO execution endpoints or automatic returns on cancellation are introduced. ## Deployment and validation Run the Inventory preflight before enabling writers, investigate legacy locations and unexplained balances manually. The internal room is created lazily and transactionally under the stable INVENTORY_ROOM code; users never enter places/racks. Apply the additive 20261013000200 migration through the normal reviewed deployment process. This implementation does not apply it to the configured application database. `npm run test:inventory` creates a random isolated MySQL database, applies all migrations with a preserved pre-migration Part, runs HTTP/concurrency tests, and drops only its own database. `node --env-file=.env prisma/verify-inventory-api.cjs --clean` verifies clean migration plus the same behavior. Both require CREATE/DROP DATABASE privileges. `npm run prisma:preflight:inventory` is SELECT-only. Typecheck/build, contract tests and compiled Swagger checks supplement real database tests. Rollback application code compatibly; preserve ledger, cases, balances, request claims, and audit. MySQL DDL is not an atomic multi-statement rollback: inspect partial deployment and repair forward. Consumer files remain unchanged; API availability is not completed UI integration. The approval queue, request states, full movement labels, correction form, and QR catalogue require separate consumer authorization. Public checkout and Stock Movement UI are integrated. Pending/rejected requests and posted movements share StockMovement; the UI separates the approval queue from posted stock history. Requester names appear in search, detail and XLSX export. No application database migration was automatically applied. # Maintenance coordination Quarantine release/scrap and case movement reversal are blocked while its WO is IN_PROGRESS. All paths acquire the same Part lock as repair start, so concurrent decisions cannot leave active repair without remaining stock. OPEN work can coexist with disposition, but start rechecks remaining quantity. Completing/cancelling work does not release goods or restore consumed stock automatically. WO net usage includes posted returns/reversals and is refreshed from the ledger; terminal corrections retain actor, reason and history. Public checkout stays independent. See maintenance.md for the complete contract.