Skip to content

Keep bounty payout payment details in-app#252

Open
BogdanMaryniuk wants to merge 1 commit into
profullstack:masterfrom
BogdanMaryniuk:fix/bounty-inapp-payment-details-clean
Open

Keep bounty payout payment details in-app#252
BogdanMaryniuk wants to merge 1 commit into
profullstack:masterfrom
BogdanMaryniuk:fix/bounty-inapp-payment-details-clean

Conversation

@BogdanMaryniuk
Copy link
Copy Markdown

Summary

  • keeps bounty CoinPay payment details in-app instead of opening hosted checkout URLs
  • persists payment address/amount/currency/expires metadata and keeps pay_url null for new bounty payments
  • lets pre-migration invoiced rows without in-app address details create a fresh in-app payment request instead of getting stuck

QA

  • Route Vitest: exit 0
  • TypeScript: exit 0
  • Targeted ESLint: exit 0

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/master was clean.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 24, 2026

Greptile Summary

This 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.

  • route.ts now stores pay_url: null and persists the payment address/amount/currency/expiry in metadata; the existing-invoice early-return only fires when metadata.payment_address is present, letting old hosted-checkout rows fall through to a new createPayment call.
  • ReviewPanel.tsx exposes the Pay button for invoiced submissions with no payment_address, labels it "Create new payment request", and renders CryptoPaymentBox with the address once available.
  • A new Vitest case covers the old-invoice fresh-payment path; all three QA checks (route tests, TypeScript, ESLint) report exit 0.

Confidence Score: 3/5

The 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

Filename Overview
src/app/api/bounties/[id]/submissions/[sid]/pay/route.ts Changes the existing-invoice early-return to only short-circuit when metadata.payment_address is present, and sets pay_url to null on new payments. Missing a payout_status === "paid" guard means old paid-but-address-less rows could be downgraded back to "invoiced".
src/app/bounties/[id]/ReviewPanel.tsx Extends the Pay button condition to also show for invoiced submissions without a payment address, enabling creators to create fresh in-app payments for old rows. checkout_url in local paymentDetails state is always null (mapped from pay_url), causing minor inconsistency with the post-refresh metadata path.
src/app/api/bounties/[id]/submissions/[sid]/pay/route.test.ts Adds a well-structured test for the old-invoice fresh-payment path and updates the pay_url assertion to null. No test covers the paid-submission downgrade edge case.

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
Loading

Comments Outside Diff (1)

  1. src/app/bounties/[id]/ReviewPanel.tsx, line 139-150 (link)

    P2 checkout_url in paymentDetails is always null after payment creation

    The client maps json.data.pay_url (always null now) to checkout_url in local state. Immediately after pay(), CryptoPaymentBox renders without the "Open" external-link button. Once the user navigates away and back (clearing paymentDetails state), details falls back to s.metadata, which does contain checkout_url from the DB. The "Open" button then reappears — meaning its presence is tied to React state lifecycle rather than a deterministic design decision. If retaining the checkout link is intentional, mapping json.data.checkout_url (added to the route response) would give consistent behavior; if the intent is to suppress it entirely, checkout_url should be omitted from the metadata write.

Reviews (1): Last reviewed commit: "fix(bounties): keep payments in-app on c..." | Re-trigger Greptile

Comment on lines 56 to 71
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,
},
});
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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 });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant