# Public errors and logging

User-facing sentences are Indonesian; codes, JSON keys, enum values and diagnostic event names are English. Branch on codes/status, never message text. Preserve user content.

| Code | Status | Meaning |
| --- | --- | --- |
| INVALID_JSON | 400 | Malformed JSON |
| VALIDATION_ERROR | 422 | Input constraints; optional safe field details |
| PAYLOAD_TOO_LARGE | 413 | Parser limit exceeded |
| INVALID_CREDENTIALS | 401 | Invalid login or inactive account |
| UNAUTHENTICATED | 401 | Missing/invalid session |
| FORBIDDEN | 403 | Permission denied |
| ORIGIN_NOT_ALLOWED | 403 | Browser mutation origin rejected |
| RESOURCE_NOT_FOUND | 404 | Requested target missing |
| ROUTE_NOT_FOUND | 404 | Unknown route |
| PRODUCT_ID_EXISTS | 409 | Known identifier conflict |
| INSUFFICIENT_STOCK | 409 | Stock unavailable (future feature) |
| STATE_CONFLICT | 409 | Business state conflict |
| REPORT_STATE_CONFLICT | 409 | Incompatible review decision, changed retry input or cancelled linked WO |
| RATE_LIMIT_EXCEEDED | 429 | Retains Retry-After |
| SERVICE_UNAVAILABLE | 503 | Recognized temporary dependency outage |
| INTERNAL_ERROR | 500 | Unknown/programming failure |

Example (checked against ApiFailure in tests):

```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":"example"}}
```

Typed domain/HTTP errors use the fixed catalog. Never trust thrown status/message. Zod normalization selects paths/codes and replaces arbitrary messages. Domain details must be application-owned safe text; never interpolate submitted values, database diagnostics or credentials.

Translate P2002 only where the operation knows the identifier. P2025 requires target versus related-record context; related failures use the documented validation/conflict code. Prisma programming/validation errors and unknown adapters remain 500. Positively identified connection/pool timeouts become 503. These mappings do not implement stock/product endpoints.

Generate a UUID before parsing; return it in X-Request-Id (exposed via CORS) and meta.requestId. Ignore incoming IDs. Request logs contain event, requestId, method, registered route template (or unmatched), status and durationMs. Failure logs contain event, requestId, method, route template, public code and headersSent.

Use an allowlist: omit raw errors, stacks, database messages, bodies, headers, cookies, tokens, query strings and user-supplied URL segments in every environment. Route templates provide safe path context. Diagnose using correlation ID, route, error classification and timing. After headers are sent, log and delegate a sanitized error to Express without appending JSON. The delegated message/stack contain only the public code and correlation metadata; Express may log them again outside test mode, so the raw exception must never be forwarded.
