Keep bounty payout payment details in-app#252
Conversation
Greptile SummaryThis PR shifts bounty CoinPay payouts from hosted-checkout redirects to in-app payment details (address, amount, QR code), and adds a migration path so old "invoiced" rows that lack the new address metadata can generate a fresh in-app payment instead of leaving creators stuck.
Confidence Score: 3/5The route change is safe for the new in-app flow, but old paid submissions that predate the address-metadata convention could have their payout status silently downgraded from paid to invoiced if the endpoint is reached for them. The core in-app payment path and the idempotent early-return for already-invoiced rows both look correct. The risk is concentrated in the migration path: any submission in paid state that lacks a payment_address in its metadata (a realistic scenario for webhook-confirmed hosted-checkout payments) will fall through to a fresh createPayment call and overwrite payout_status with invoiced, reversing a completed payout. The UI does not expose a button for paid submissions, but there is no server-side guard blocking the API call. route.ts — specifically the migration fall-through logic around lines 56–71 and the absence of a payout_status === paid check before the createPayment call. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[POST /pay] --> B{coinpay_invoice_id set?}
B -- No --> E[Call createPayment]
B -- Yes --> C{metadata.payment_address set?}
C -- Yes --> D[Return existing in-app details\npay_url: null]
C -- No: old hosted-checkout row --> E
E --> F{Payment result valid?}
F -- No --> G[502 Bad Gateway]
F -- Yes --> H[Update DB\npayout_status: invoiced\npay_url: null\nmetadata: address/amount/currency/expires]
H --> I[Return in-app payment details]
style C fill:#ffe0b2
style D fill:#c8e6c9
style I fill:#c8e6c9
style G fill:#ffcdd2
|
| if (submission.coinpay_invoice_id) { | ||
| const metadata = (submission.metadata || {}) as Record<string, unknown>; | ||
| return NextResponse.json({ | ||
| data: { | ||
| submission_id: sid, | ||
| coinpay_invoice_id: submission.coinpay_invoice_id, | ||
| pay_url: submission.pay_url, | ||
| payment_address: metadata.payment_address || null, | ||
| payment_currency: metadata.payment_currency || null, | ||
| amount_crypto: metadata.amount_crypto || null, | ||
| expires_at: metadata.expires_at || null, | ||
| }, | ||
| }); | ||
| if (metadata.payment_address) { | ||
| return NextResponse.json({ | ||
| data: { | ||
| submission_id: sid, | ||
| coinpay_invoice_id: submission.coinpay_invoice_id, | ||
| pay_url: null, | ||
| payment_address: metadata.payment_address || null, | ||
| payment_currency: metadata.payment_currency || null, | ||
| amount_crypto: metadata.amount_crypto || null, | ||
| expires_at: metadata.expires_at || null, | ||
| }, | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
No guard against re-invoicing an already-paid submission
The early-return now only fires when metadata.payment_address is present. Any submission where payout_status = "paid" but metadata.payment_address is absent (i.e., an old hosted-checkout row whose payment was confirmed by webhook before this migration) will fall through to createPayment() and then update the row with payout_status: "invoiced", downgrading it from "paid". A creator visiting such a bounty and clicking the button — or calling the endpoint directly — would silently reset a completed payout to invoiced.
Adding a guard before the coinpay_invoice_id check would prevent this:
if (submission.payout_status === "paid") {
return NextResponse.json({ error: "Submission has already been paid" }, { status: 400 });
}
Summary
pay_urlnull for new bounty paymentsQA
Notes
This supersedes the earlier conflicting PRs (#241/#244). Branch is based before the new workflow commit to avoid the contributor token workflow-scope push restriction; local merge test against current
origin/masterwas clean.