# Design

## Context

See proposal.md for motivation and scope. The API uses Express 5, TypeScript, Prisma 7.2 with the MariaDB driver adapter and a MySQL datasource. Prisma models are split under `prisma/models`; preserve that configuration and the generated-client path.

Before this change, requests follow routes -> controllers -> services -> Prisma. Authentication is implemented; Inventory and Maintenance models exist without their business endpoints. `app.ts` mounts `/api/v1`, disables the existing not-found middleware, and registers a final error handler. That handler infers status from the response and authentication returns several direct response shapes. Shared `AuthResponse` remains the payload `{ user }`; the frontend reads `data.user` from Axios's response body and top-level `message` on errors. Swagger scans the current routes directory. Existing auth tests cover cookies, current account checks, origin protection, rate limits, and error secrecy.

## Goals / Non-Goals

**Goals:** Establish consistent public contracts, keep business rules independent of HTTP, and make cross-domain consistency explicit without replacing the existing stack. A design artifact is necessary because this change crosses routing, errors, contracts, tests, and client compatibility.

**Non-Goals:** Restructure the existing folders, introduce modules/http/domain/infrastructure directories, relocate auth or config/prisma.ts, implement Inventory/Maintenance workflows, introduce repository abstractions for every model, change the data model, replace authentication, add translation infrastructure or background workers, or claim future domain requirements have runtime coverage in this baseline.

## Decisions

### 1. Preserve the existing layered architecture

Keep one deployment and the shared database client at config/prisma.ts. Follow the user's existing routes -> controllers -> services -> Prisma design. Logical feature boundaries describe ownership and service contracts, not new directories. Feature colocation was rejected because the existing folder structure must remain intact.

```text
src/
  app.ts
  index.ts
  config/                 env, auth, Swagger, prisma.ts, error-catalog.ts
  routes/                 auth.routes.ts and existing general routes
  controllers/            auth.controller.ts and response.ts helpers
  services/               auth/password/JWT services, serialization, Prisma error translation
  validators/             auth.schema.ts and pagination.schema.ts
  middlewares/            auth, request correlation, error normalization and handling
  interfaces/             internal contracts and HTTP-independent typed domain errors
  generated/              Prisma output, never manually edited
```

Keep auth.routes.ts, auth.controller.ts, auth.service.ts, auth.schema.ts and auth.middleware.ts in their existing layer folders. Keep password.service.ts and jwt.service.ts in services. DTO mapping stays explicit in the owning service (the existing publicUser mapper is sufficient); do not add a mapper directory or generic repository.

Routes declare URLs and attach middleware. Controllers validate HTTP input, call services and send responses. Services own business rules, database queries, DTO mapping and transactions and must not import Express Request/Response, controllers, routes or middleware. Validators define input constraints. Middleware owns request correlation and HTTP error normalization; config/error-catalog.ts supplies the fixed public status/message catalog. interfaces/domain-error.ts defines domain errors without HTTP dependencies. services/prisma-error.service.ts translates persistence errors using operation context. General response helpers live in controllers/response.ts; pagination validation lives in validators/pagination.schema.ts and serialization in services/serialization.service.ts.

Inventory owns parts, stock locations, balances, movements and quarantine. Maintenance owns plans, work orders, transitions, comments and calibration. Assets owns plant units and the asset hierarchy. Reports reads data without mutating it. Future feature files follow routes/<feature>.routes.ts, controllers/<feature>.controller.ts, services/<feature>.service.ts and validators/<feature>.schema.ts. One feature service may call another feature's public service contract but must not bypass its mutation rules or import controllers. Shared config must not import feature services.

Do not add modules/, http/, domain/ or infrastructure/ directories. Restore any relocation performed under the superseded plan and place the already implemented helpers inside the existing layer folders. This is a placement correction, not a change to the response/error behavior.

### 2. Explicit JSON envelopes and shared public contracts

Use the shapes and status rules in `api-response-contract`. The `success` literal supports a discriminated TypeScript union; keeping separate success/error shapes avoids nullable data/error combinations. Plain payloads would keep clients coupled to per-endpoint conventions. A separate problem-details format was considered but a single application envelope gives this frontend one decoding path.

Keep `AuthResponse` as the auth payload `{ user }` and introduce shared generic `ApiSuccess<T>`, `ApiFailure`, field-detail, and pagination types. HTTP auth consumers use `ApiSuccess<AuthResponse>` and read the body envelope's `data.user` (Axios response `data.data.user`). Generated Prisma types do not become frontend contracts.

Example success:

```json
{"success":true,"data":{"id":"part_123","name":"Katup pengatur tekanan","estimatedUnitCost":"125000.00"},"meta":{"requestId":"req_abc"}}
```

Example failure:

```json
{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Data yang dikirim tidak valid.","details":[{"field":"quantity","code":"MIN_VALUE","message":"Jumlah harus lebih dari 0."}]},"meta":{"requestId":"req_abc"}}
```

No envelope for 204, HEAD, downloads, or Swagger assets. Keep `/` outside the versioned contract. Paginated domain collections use page 1, pageSize 20, maximum 100, filtered counts, and a stable ID sort tie-breaker. Reject invalid pagination rather than silently clamping it. General demo arrays need envelopes but are not paginated domain collections. Offset pagination fits initial table screens; live concurrent edits can move records between pages, and future high-volume feeds can define a separate cursor contract.

### 3. Centralized normalization and safe logging

Generate a request ID before JSON parsing; attach it to response headers, response metadata, and structured request/failure logs. Use a server-generated value rather than trusting arbitrary client strings. Expose `X-Request-Id` through CORS for the configured frontend. Redact credentials and tokens from diagnostic messages, never log whole bodies or cookies, and log path without query secrets. Retain Morgan only if it fits this correlation/redaction policy; Node logging suffices without adding a logger dependency.

Services throw typed errors with domain code and safe details. The HTTP layer owns a fixed code-to-status and Indonesian message catalog; arbitrary thrown `status` or `message` values are not trusted. Normalize parser, Zod, authentication, origin, and limiter failures through the same serializer. Register unmatched API-route handling after routers/documentation and the four-argument error middleware last. Check `headersSent` and delegate to Express rather than writing twice.

Express 5 handles rejected promises from returned/awaited async handlers, so no Express 4 async wrapper is required. Detached callbacks and future jobs remain outside that mechanism. Reference: https://expressjs.com/en/guide/error-handling (checked during exploration using Context7).

Suggested mappings:

| Condition | HTTP | Public code |
| --- | --- | --- |
| Malformed JSON | 400 | INVALID_JSON |
| Schema failure | 422 | VALIDATION_ERROR |
| Oversized body | 413 | PAYLOAD_TOO_LARGE |
| Unauthenticated / invalid credentials | 401 | UNAUTHENTICATED / INVALID_CREDENTIALS |
| Forbidden operation or origin | 403 | FORBIDDEN / ORIGIN_NOT_ALLOWED |
| Missing target / route | 404 | RESOURCE_NOT_FOUND / ROUTE_NOT_FOUND |
| Known duplicate | 409 | Domain code, e.g. PRODUCT_ID_EXISTS |
| Insufficient stock / stale state | 409 | INSUFFICIENT_STOCK / STATE_CONFLICT |
| Rate limit | 429 | RATE_LIMIT_EXCEEDED |
| Recognized temporary dependency outage | 503 | SERVICE_UNAVAILABLE |
| Unknown exception | 500 | INTERNAL_ERROR |

Map Prisma P2002 in the operation that knows the unique business identifier. Map P2025 to 404 only for a missing requested target; related-record failures need context. Do not map Prisma client programming/validation errors to 422 indiscriminately. Unknown adapter failures remain 500; only positively classified temporary outages become 503. Preserve Retry-After from the rate limiter. No client stack traces in development or production. Reference: https://www.prisma.io/docs/orm/v7/reference/error-reference.

### 4. Language and serialization at the boundary

Carry forward the proposed default of Indonesian user-facing error sentences with familiar English technical terms. Identifiers, enum values, codes, and developer logs remain English. Store and return business content as entered. Locale negotiation and automatic translation are unnecessary for the pilot.

DTO mappers serialize decimal money as exact decimal strings, quantities as integers, IDs as strings, instant timestamps in UTC, and local schedule dates without timezone conversion. Recurrence uses the configured facility timezone, not a hardcoded browser timezone. Clients own Indonesian number/currency formatting and status labels. Do not depend on incidental Prisma JSON serialization.

### 5. One transaction owner for a business operation

The orchestrating service owns the Prisma transaction and passes its transaction client to participating domain services. Inventory functions enforce approval, availability, ledger posting, and version/lock checks even when called from Maintenance. Participating services do not open independent transactions or silently use the global client during a coordinated operation.

Follow `prisma/schema-design.md`: ledger, balance/version, quarantine effects, and required audit records commit together. Use compare-and-swap or a row lock for concurrent balance updates; a transaction alone does not prevent overdraw. Work-order usage derives from posted ISSUE/RETURN/reversal records. Avoid network calls inside transactions. Future notification delivery occurs after commit, with an outbox only when reliable external delivery is introduced.

Future stock mutation endpoints accept a client-retained idempotency key generated before the first attempt; retries reuse it. The domain feature must define key scope, payload matching, and replay semantics before implementation. This refines the schema-design note about boundary-generated keys: minting a fresh server key on every retry is insufficient. Retry only recognized transient conflicts with a bounded policy and no duplicate external effects. These rules are architectural acceptance constraints for future domain changes, not stock implementation tasks in this change.

## Risks / Trade-offs

- [Breaking existing auth consumer] -> Change shared contracts, API, frontend auth decoding, and OpenAPI in one coordinated release; do not deploy the API alone.
- [Companion paths outside current workspace permissions] -> Complete frontend/shared tasks from a workspace authorized for the monorepo before release; do not hide this dependency or duplicate shared contracts locally.
- [Swagger contract drift] -> Retain discovery in routes/ and verify updated schemas in both source and compiled runtime documentation.
- [Different failure paths drift] -> Contract tests cover parser, validation, auth, origin, limiter, unknown route, and unexpected service failures.
- [Architecture drifts from the existing project] -> Preserve all layer folders and original auth/Prisma paths; keep feature ownership in service contracts and avoid new architectural directory trees or generic repositories.
- [Accidental history loss or stock races] -> Future domain changes must test the consistency scenarios in `api-module-boundaries` against real transactional storage.

## Migration Plan

1. Define the shared envelope types and backend response/error infrastructure without publishing changed endpoints independently.
2. Preserve the existing routes/controllers/services structure and config/prisma.ts. Restore files moved during the interrupted implementation, adapt helper imports to existing folders, retain routes/ Swagger discovery, and verify existing security behavior.
3. Convert versioned JSON responses and all failure paths; update the frontend auth consumer and its fallbacks to the new contract and Indonesian copy.
4. Update API and client contract tests, OpenAPI schemas/examples, and source architecture documentation. Run typechecks, builds, and existing regression suites from the appropriate workspaces.
5. Release backend and frontend together after contract checks pass. Roll back both artifacts together if needed; no database rollback is involved.
6. Sync/archive the implemented change through OpenSpec so the capability deltas become the durable core specification. Future domain proposals reference these requirements and supply their own endpoint/concurrency acceptance tests.
