Migrate monthly-fee checkout flow to Stripe Payment Links (analog of #494) #498
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/basketball-api#498
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type
Feature
Lineage
Standalone — direct analog of #494 (tournament-fee migration). Monthly-fee flow still mints Stripe Checkout Sessions which cap at 24h. Payment Links do not expire and carry metadata for webhook fulfillment. Same mechanism change, different flow.
Refinement applied after
/review-ticketpass 1 (review notereview-1032-2026-04-17): added AC forsetup_future_usagecarry-over, added AC for widening webhook deactivation gate toProductCategory.monthly, and locked in the email-URL strategy (keep the/checkout/first-payment?token=...redirect, have the endpoint return the persisted Payment Link URL).Repo
forgejo_admin/basketball-apiUser Story
As a parent who receives a monthly-fee payment email, I can click the link any time after receipt — that day, that week, or later — and reach a working Stripe Checkout page. The link does not expire out from under me.
Context
The 2026-04-17 investigation proved Stripe Checkout Sessions are the wrong primitive for any flow where the URL lives in an inbox. We migrated tournament fees to Payment Links in #494. Monthly fees are the next flow in the same pattern: parents receive a monthly-fee link, may open the email a day or two later, hit a dead URL. Same class of customer-facing breakage we just apologized for on tournaments; we will face it again on monthly unless this ships.
Monthly fees differ from tournament fees in three substantive ways that the dev agent MUST preserve:
amount_centsrather than a product-level price.setup_future_usage: "off_session"is required on the monthly flow for card-save on recurring billing. The existing Session call sets this viapayment_intent_data(checkout.py:423). #494's tournament helper does NOT set this, so a straight copy will silently break recurring charges. Regression coverage lives attests/test_first_payment.py::test_setup_future_usage_passed_to_stripeand must remain green.{base_url}/checkout/first-payment?token=...(services/email.py:1041-1043). Keep the redirect — thefirst_payment_checkoutendpoint should return the persisted Payment Link URL fromOrder.stripe_checkout_url, preserving the contract-status re-check that happens on click. Do NOT embedhttps://buy.stripe.com/...directly in the email template.File Targets
Dev agent's decision — grep live code for the monthly-fee mint path(s) and substitute the primitive. Verified single site as of 2026-04-17:
routes/checkout.py:305(first_payment_checkout). Do not touch non-monthly flows (tournament already migrated, jersey / tryout / generic / admin regen for tournaments stay as-is).Acceptance Criteria
order_idpresent in Stripe metadata so the existing webhook matcher continues to flip Order → paidwebhooks.py:246-267currently deactivates Payment Links only whenproduct.category == ProductCategory.tournament. Widen to includeProductCategory.monthlyWITHOUT removing the tournament branch — jersey/tryout/generic must continue to skip deactivation.orders.amount_centsunchanged — scholarships, prorations, custom rates all preservedsetup_future_usage: "off_session"carried over from the existing Session call. The regression testtests/test_first_payment.py::test_setup_future_usage_passed_to_stripemust continue to pass (and ideally an analogous assertion added for the Payment Link path).{base_url}/checkout/first-payment?token=...— no change to the email template. The redirect endpoint returns the persisted Payment Link URL fromOrder.stripe_checkout_url.Test Expectations
order_id→Order.status = paidon payment (mocked)setup_future_usagepresence asserted on the Payment Link path (extend or add alongsidetests/test_first_payment.py::test_setup_future_usage_passed_to_stripe)Constraints
CHECKOUT_SESSION_TTL_SECONDSor similar TTL constants must not be introduced — that was the #490 dead end/checkout/first-payment?token=...redirect so contract-status re-check on click is preservedChecklist
setup_future_usagecarry-over explicitly assertedRelated
forgejo_admin/basketball-api #494— tournament migration. Pattern to copy with three monthly-specific deltas above.forgejo_admin/basketball-api #486— tournament recovery (uses #494's helper). Dogfood analog for monthly.forgejo_admin/basketball-api #497— monthly-fee recovery ticket. Downstream consumer of this helper.forgejo_admin/basketball-api #493— revert of broken 30-day approach. Context for why integration test is required.feedback_retrieve_before_theorize.mdScope Review: NEEDS_REFINEMENT
Review note:
review-1032-2026-04-17Ticket is directionally sound and a true standalone analog of #494 (not dependent on #497 — #497 is the consumer, this is the enabler). Traceability triangle intact:
story:WS-S11,arch:dataflow-westside-basketball, issue open. Monthly-fee mint path is unambiguous and grep-able (routes/checkout.py:305first_payment_checkoutis the sole site;services/email.py:1021send_first_payment_emailembeds the URL). Fits a single agent pass (no decomposition).Three
[BODY]refinements needed before this can advance totodo:setup_future_usagepreservation. The existing monthly Session call passespayment_intent_data={"setup_future_usage": "off_session"}(checkout.py:423) so the card is saved for recurring billing. #494's tournament helper does not set this — tournament doesn't need card save. A dev agent copying #494 verbatim will silently drop card saving. The AC should name this explicitly. Regression test:tests/test_first_payment.py::test_setup_future_usage_passed_to_stripe.webhooks.py:246-267deactivates the Payment Link only whenproduct.category == ProductCategory.tournament. Monthly orders will never trigger deactivation under the existing gate. Add to Acceptance Criteria or Constraints: "Widen the deactivation gate inroutes/webhooks.pyto includeProductCategory.monthly. Do not remove the gate — jersey/tryout/generic must stay untouched."{base_url}/checkout/first-payment?token=...), not a Stripe URL. With Payment Links the dev agent must choose: (a) keep the redirect, havefirst_payment_checkoutreturn the persisted Payment Link URL onOrder.stripe_checkout_url; or (b) embed thehttps://buy.stripe.com/...URL directly. Recommend (a) — preserves the contract-status re-check on click, minimum diff to the email template. Issue body should state the choice so the dev agent doesn't have to guess.No
[LABEL],[SCOPE], or[DECOMPOSE]recommendations. Per the consolidated-spec convention, these refinements must land in the issue body itself — comments document the why, but the agent reads the body.Full review:
review-1032-2026-04-17in pal-e-docs.Refinement applied after
/review-ticketpass 1 (review notereview-1032-2026-04-17). Three body edits:Added AC + Checklist + Test Expectation for
setup_future_usage: "off_session"carry-over. The existing Session call sets this viapayment_intent_data(checkout.py:423); #494's tournament helper does not. Straight copy would silently break card-save on recurring billing. Regression testtests/test_first_payment.py::test_setup_future_usage_passed_to_stripenamed explicitly as must-stay-green.Added AC + Checklist for widening the webhook deactivation gate at
webhooks.py:246-267to includeProductCategory.monthly, without removing the tournament branch. Jersey/tryout/generic must continue to skip deactivation.Locked in email-URL strategy. Keep the
{base_url}/checkout/first-payment?token=...redirect; havefirst_payment_checkoutreturn the persisted Payment Link URL fromOrder.stripe_checkout_url. Preserves contract-status re-check on click, minimum diff to email template. Added as Context #3, AC, Constraint, and Checklist item.Also added the verified single mint site (
routes/checkout.py:305 first_payment_checkout) to File Targets, and linked #497 in Related. Re-running /review-ticket.Scope Review Pass 2: APPROVED
Review note:
review-1032-2026-04-17-pass-2Pass-1 note:
review-1032-2026-04-17(NEEDS_REFINEMENT)All three pass-1 deltas landed in the body as first-class ACs + constraints + test expectations + checklist items:
setup_future_usage: "off_session"preservation — named in Context, AC, Test Expectations, Checklist; regression testtests/test_first_payment.py::test_setup_future_usage_passed_to_stripe(line 477) cited.routes/webhooks.py:246-267, tournament branch explicitly preserved./checkout/first-payment?token=...redirect, havefirst_payment_checkoutreturn the persisted Payment Link URL fromOrder.stripe_checkout_url. No longer phrased as an open question.No scope creep — body grew only within the allowed pass-1 deltas + the mint-site citation (
routes/checkout.py:305) + the#497related-link add. Traceability triangle intact. Fits a single agent pass.Board item #1032 ready to advance from
backlog→todo.