fix: admin refund actually sends funds (was calling a nonexistent method)

The admin "send funds" refund path called walletRPCService.sendPayment(),
which does not exist — guarded by a typeof check, so it silently never sent
while still reporting the refund as processed.

Now uses the real processPayout() (validates the address and transfers,
returning { success, txHash }), and refunds the amount the user actually paid
(received_amount, recorded at confirmation) rather than the expected amount,
falling back to expected for older records.
This commit is contained in:
jwinterm
2026-05-31 10:02:19 -04:00
parent ce83ec4b6c
commit 2e6f1a6335
2 changed files with 19 additions and 11 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ Key: `S` ≤ half day · `M` ~1–3 days · `L` ~1 week+ · 🔴 launch-blocker
- [x] **1.2 S — Snapshot payout config at game start.** ✅ Migration 016 adds `payout_escape_amount`/`payout_treasure_amount` (BIGINT) + multipliers to `games`. Both start paths stamp the resolved payout amounts (via `_computePayoutSnapshot`); `completeGame` pays from the snapshot (falls back to live calc only for pre-snapshot games). New `payoutSnapshot.test.js` proves a mid-game config change can't alter an in-flight payout.
- [x] **1.3 S — Close payout uniqueness holes.** ✅ `processing` covered in the unique index (migration 015) and in the in-code existing-payout guard. Migration 017 adds idempotent `NOT VALID` CHECK constraints on `payments.status`, `payments.payment_type`, and `payouts.status` so a typo'd status can't silently orphan a money row (enforces new writes, won't choke on legacy rows). _(games.status/game_mode CHECKs deferred — transient states, lower money-impact.)_
- [x] **1.4 S — Fix retry stat guard + non-atomic tx_hash write.** ✅ The "already on-chain" path used a `NOT EXISTS` guard against the row it had just marked completed (always skipped → never counted); replaced with a conditional `UPDATE … WHERE status <> 'completed' RETURNING` so stats count exactly once. Successful retries now store `tx_hash` + mark completed + count stats in ONE transaction (no stranded-tx_hash window). New `payoutRetry.test.js` covers count-once, no-double-count, and atomic completion.
- [ ] **1.5 S — Remove dead refund send-path** (`src/index.js:795`).
- [x] **1.5 S — Fix dead refund send-path.** ✅ The admin refund called a nonexistent `walletRPCService.sendPayment()` (guarded by a `typeof` check), so refunds were silently never sent while still reporting success. Now uses the real `processPayout()` (validates address + transfers, returns `{success, txHash}`) and refunds the **actually received** amount (`received_amount`, falling back to expected).
- [ ] **1.6 S — `credits` → BIGINT; make migration 010 idempotent** (`src/migrations/010_security_constraints.sql`); per-migration transactions in runner (`src/db/databaseManager.js:146`).
## Phase 2 — Identity, abuse & concurrency (🟠)
+18 -10
View File
@@ -811,19 +811,27 @@ app.post('/api/admin/refund/payment', adminAuth, asyncHandler(async (req, res) =
await client.query('COMMIT');
// Optionally send funds back via wallet RPC
// Optionally send funds back via wallet RPC. Refund what the user ACTUALLY paid
// (received_amount, recorded at confirmation) when available, falling back to the
// expected amount for older records.
let fundsSent = false;
let txHash = null;
if (sendFunds && payment.payout_address && payment.expected_amount > 0) {
const refundAmount = (payment.received_amount != null && Number(payment.received_amount) > 0)
? payment.received_amount
: payment.expected_amount;
if (sendFunds && payment.payout_address && Number(refundAmount) > 0) {
try {
if (walletRPCService && typeof walletRPCService.sendPayment === 'function') {
const result = await walletRPCService.sendPayment(
payment.payout_address,
payment.expected_amount
);
fundsSent = true;
txHash = result?.tx_hash || null;
}
// processPayout validates the address and performs the transfer; returns
// { success, txHash, fee }. (The previous code called a nonexistent sendPayment(),
// so refunds were silently never sent while still reporting success.)
const result = await walletRPCService.processPayout({
address: payment.payout_address,
amount: refundAmount,
userId: payment.user_id,
description: `Refund for payment ${paymentId}`
});
fundsSent = !!(result && result.success);
txHash = (result && result.txHash) || null;
} catch (err) {
console.error('Failed to send refund funds:', err.message);
// Don't fail the request - refund is recorded, funds can be sent manually