# Design

## Context

See proposal.md for motivation. Existing source follows routes/controllers/services/Prisma with cookie authentication, trusted-origin checks, explicit DTOs, central errors, and pagination. Inventory and kiosk pages use local fixtures. StockMovement already has approval, signed deltas, an idempotency key, a reversal link, and optional WO/case references. StockBalance has a version. Part has no photo field or version. Existing tests demonstrate isolated MySQL fixtures and review locking in reports.mysql.test.ts.

The durable spec inventory is empty; core architecture requirements remain in establish-core-api-architecture. Existing Pilot Plant work must remain compatible. No consumer, application code, schema, or migration is changed by this planning workflow.

## Goals / Non-Goals

**Goals:** One reusable posting boundary, accurate single-room stock, accountable checkout approval, useful API read models, and explicit compatibility checkpoints.

**Non-Goals:** Full Maintenance execution, reservations, financial calculations, external notification delivery, room/rack management, transfer operations, unattended anonymous checkout, or permission to rewrite consumers.

## Decisions

Implementation update: the user approved all three concrete persistence changes in persistence-review.md via "ya lanjutkan" after the quarantine explanation. That approved review supersedes the tentative AuditLog-only replay option and mixed-state DTO option below: use InventoryRequest, Part.photoKey/version, and the CLOSED_MIXED enum. Backend implementation and isolated migration tests are authorized; consumer edits and application-database deployment are not. The compatibility contract is delivered in src/services/inventory.md.

### 1. Preserve layers and public contracts

Use routes/inventory.routes.ts and checkout.routes.ts, matching controllers/validators, and focused services for parts, stock movements, quarantine, and checkout. Services do not import HTTP types. Shared DTOs are explicitly mapped; never expose generated records, prices, tokens used for authentication, or arbitrary persistence errors. Keep existing envelope, status codes, UTC instants, facility date boundaries, and deterministic pagination (default 20, max 100).

Checkout and future Maintenance call the stock service. The orchestration service owns one transaction and passes its client to all participants. Separate stock engines in checkout and WO are rejected because they would bypass shared safeguards.

### 2. Endpoint contract

All paths below are under /api/v1. Static routes precede /:id routes.

| Method/path | Responsibility |
| --- | --- |
| GET /parts; GET /parts/:id | Active catalogue/detail; q, groupId, kindId, stockStatus, paging |
| POST /parts; PATCH /parts/:id | Supervisor/admin metadata and optional initial receipt on creation |
| GET /part-groups; GET /part-kinds | Read master names/IDs |
| GET /inventory/summary | Global active SKU, low/empty SKU, held quantity summary |
| GET /stock-movements; GET /stock-movements/:id | Posted history; q, type, month, partId, workOrderId, paging |
| GET /stock-movements/export | Filtered non-financial XLSX download, explicit export size limit |
| POST /stock-movements | Supervisor/admin receipt, return, or reasoned adjustment |
| POST /stock-movements/:id/reverse | Supervisor/admin full reversal |
| GET /quarantine-cases; GET /quarantine-cases/:id | Case quantities and related history |
| POST /quarantine-cases | Immediate attributed damage hold |
| POST /quarantine-cases/:id/release; /scrap | Supervisor/admin dispositions |
| GET /checkout/groups; GET /checkout/parts; GET /checkout/parts/:id | Group counts, search and current quantities |
| GET /checkout/resolve?kind=productId\|qrToken\|partNumber&value=... | Explicit identifier resolution, no arbitrary URL fetching |
| POST /checkout/requests | Request or immediately post ISSUE according to current role |
| GET /checkout/requests; GET /checkout/requests/:id | Own requests; privileged users can filter all and pending |
| POST /checkout/requests/:id/approve; /reject | Privileged review |

Request ID is the underlying movement ID, not a second independently mutable request entity. Add photo endpoints only after the persistence/consumer checkpoint below is confirmed. QR image printing stays a consumer concern; API resolves identities. Existing product-ID labels remain resolvable. qrToken is a durable lookup identifier, not authorization. Prefer qrToken for future labels only after consumer confirmation; explicit legacy part-number resolution must reject ambiguity. Unknown/archived checkout targets return not-found/unavailable without side effects.

### 3. One room without place entry

Select a configured internal StockLocation record automatically, using a stable code such as INVENTORY_ROOM. One StockBalance per part is selected by that room. Do not remove existing location foreign keys or add rackLabel. Preflight existing balances: if multiple populated locations exist, stop for explicit reconciliation; never sum and overwrite or silently move them.

Consumer optional location text is not an operational selector. Preserve the file as it is. Its future mapping, omission, or read-only treatment is a consumer compatibility checkpoint, not permission to create place-entry functionality. Creating a part with zero stock creates no zero-quantity movement; positive initial quantity creates a posted receipt in the same transaction. Group/kind text can be resolved to normalized unique names by authorized master creation; choose no new management UI. Respect database field lengths rather than the UI's blanket 120-character limit.

### 4. Approval and identity

Confirmed rule: TECHNICIAN ISSUE is PENDING; ADMIN/SUPERVISOR ISSUE is APPROVED immediately with self as approvedBy and approvedAt. A reviewer approves/rejects only PENDING requests. The existing URGENT_AND_HIGH setting is ignored for this checkout policy; do not label privileged actions AUTO_APPROVED or SYSTEM. Persist rejection reason and reviewer/time in append-only audit (the model has no rejectedBy/rejectedAt columns); do not overwrite request notes or misuse approvedAt for rejection.

Proposed least-privilege default: VIEWER reads only. Derive actor from authentication; kiosk selecting someone else's name is not delegated identity. Delegated checkout is excluded. Checkout is independent of WO: purpose is always required, WO input/filter is rejected, and no WO eligibility is checked at submission or approval. WO relationships remain in Maintenance and quarantine workflows. Preserve original actor if another user approves. Reject inactive requesters at posting until a supervisor resolves the request. No priority flag changes these permissions.

No reservation: pending requests leave availability unchanged. Lock and recheck at approval; insufficient stock yields 409 and leaves pending intact. Identical committed decisions replay; conflicting decisions fail. Rejection creates audit without stock effects. Initial receipts and corrections are privileged direct approvals. Immediate quarantine holds are authorized safety operations, not checkout requests; record the reporting user and an explicit policy reason in audit, using AUTO_APPROVED under this documented hold policy rather than pretending a technician is supervisor. This extends hold policy only, not checkout auto-approval.

### 5. Transaction and retry boundary

Lock the movement/case and balance in a consistent order; use balance version comparison or row locks to guard quantities. Derive deltas server-side. Commit movement, balance/version, case state, and audit together. Keep filesystem/network operations outside the database transaction. Retry only recognized transient conflicts with a bounded policy validated against the installed adapter and real MySQL.

All logical write operations, including initial stock, holds, reviews, and reversal, need durable retry identity. Movement.idempotencyKey is globally unique; scope payload matching to actor and operation. Store a normalized request fingerprint and outcome pointer in a durable record. Proposed option: an append-only AuditLog entry within the same commit with a recognizable action and payload fingerprint, using the movement unique key for posting deduplication. For zero-stock creation and non-movement writes, use a unique business identity plus an explicit concurrency guard; if that cannot satisfy replay safely, present an idempotency-table migration for confirmation before implementation. Never silently settle for process memory or a fresh server key per retry. Do not leak another actor's resource on key collision.

Posted rows are immutable. Reverse only once, with same balance/WO and opposite signed deltas; validate stock and restore case effects. Returns are bounded by net issued quantity per part/WO under the same lock protocol. Corrections must not inflate stock through repeated returns/reversals. No automatic return on WO cancellation. A future coordinated WO operation rolls back with its stock writes.

### 6. Read models and technical-only scope

Expose raw movement types and both deltas. Keep pending/rejected request listing separate from posted history. Use postedAt for movement month filters and occurredAt as supplemental event time; convert facility-local month bounds to UTC. Summary uses all active parts, independent of table paging. Report net quantities grouped by unit; do not imply that Pcs and Set are interchangeable.

Export uses the same authorized filters as posted history with stable ordering and a bounded result (proposed maximum 10,000 rows, explicit error if exceeded). User text is written as literal spreadsheet text. The current browser Excel export only knows the loaded array and needs separate integration approval to consume server export. DTOs omit unitCost/estimatedUnitCost. Existing monetary columns keep their defaults; do not rewrite historical nonzero values. Financial snapshot requirements in schema-design.md do not introduce price entry or cost reporting into this user-confirmed technical scope; document that scope exception during implementation.

### 7. Confirmation checkpoints, not silent consumer changes

These are explicit boundaries for dependent tasks; the proposal records candidates for review, not authorization to migrate or change UI.

| Mismatch | Concrete proposed resolution | Before proceeding |
| --- | --- | --- |
| Part photos have no persistence field | Nullable photoKey and concurrency guard, reuse media staging/cleanup; upload on Save, not preview selection | Confirm exact schema and photo consumer contract |
| Form edits physical stock without reason/version | Separate adjustment operation with reason and expected balance version | Confirm consumer interaction; never accept a blind balance overwrite |
| Approval queue and pending/rejected labels absent | Reuse existing styles for a queue and own-request status, backend API available first | Confirm affected frontend files and interaction |
| Movement UI has only three types | Map full raw types, reversal direction, and explicit deltas | Confirm UI/filter/export changes |
| Optional location input still exists | Leave unchanged until its non-operational treatment is agreed | No rack/place field additions |
| Mixed release/scrap terminal case | Prefer deriving a CLOSED_MIXED DTO state from ledger while retaining a documented internal status; alternatively add enum | Confirm representation before mixed-disposition implementation |
| StockoutEvent lacks retry identity | Defer stockout analytics; insufficient stock still returns 409 | Confirm any later event persistence/dedup design separately |

Consumer integration is not an automatic task of applying this API change. Do not claim photos or end-to-end checkout integrated while their checkpoints are unresolved. Core API work independent of these gates can proceed when apply is requested. No SQL CHECK constraints, new fields, or revised enum are authorized by schema documentation alone.

## Risks / Trade-offs

- [Pending requests can lose available stock] -> Recheck at approval and expose pending status; reservations intentionally excluded.
- [Existing UI suggests immediate checkout or arbitrary stock edits] -> Keep consumers untouched; require explicit integration approval before operational rollout.
- [Photo capability cannot be persisted today] -> Resolve the bounded photo checkpoint before that feature; do not store browser data URLs as durable images.
- [Multiple legacy locations] -> Read-only preflight and reviewed reconciliation; no destructive merge.
- [Master renaming changes history labels] -> Preserve IDs; prohibit unit changes after posting; snapshot labels only through a separately approved schema design if required.
- [Shared package outside writable root] -> Obtain workspace authorization before implementing shared DTO edits; do not duplicate public contracts locally.

## Migration Plan

1. Verify existing balances, movement consistency, case quantities, and unique identifiers read-only. Do not import demo stock as operational history.
2. Resolve applicable confirmation checkpoints before dependent changes. Prepare any approved migration with data preservation and real isolated MySQL migration tests.
3. Implement API and shared contracts, then validate isolated database behavior and OpenAPI; keep old Work Order endpoints unchanged.
4. Deploy backend only as API availability, not as completed consumer integration. Operational frontend rollout is a separately confirmed task with authorized paths.
5. Roll back application artifacts compatibly; preserve posted ledger, balances, cases, and audit. Do not delete transactions or drop populated columns to roll back UI.


## Superseding public checkout decision (2026-10-14)
The user now requires anonymous checkout with identity stored on the existing transaction, not a separate guest table. `public-checkout-review.md` supersedes all earlier authenticated-checkout and immediate-admin-checkout assumptions in this design. All new requests wait for authenticated ADMIN/SUPERVISOR approval. Stock Movement consumes the queue and ledger APIs.
