feat: add skip_proration flag to first-payment endpoint and checkout route #459
No reviewers
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!459
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "458-skip-proration-flag"
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?
Summary
Adds a
skip_prorationflag so custom-deal players (e.g. Jaxon Gerber $50/mo flat) get charged their exact monthly fee instead of the auto-prorated amount. Also adds theGET /checkout/first-paymentroute that was missing from this branch.Changes
src/basketball_api/services/email.py—send_first_payment_email()acceptsskip_prorationkwarg; when true, uses flatmonthly_fee, says "April fee" instead of "Prorated April fee", and appends&prorate=falseto checkout URLsrc/basketball_api/routes/admin.py—POST /admin/email/first-paymentacceptsskip_prorationquery param, forwards to send functionsrc/basketball_api/routes/checkout.py— NewGET /first-paymentendpoint: looks up player bycontract_token, creates Stripe Checkout Session withsetup_future_usagefor card saving; respectsproratequery param (default true)tests/test_first_payment_email.py— 2 new tests: skip_proration=True uses flat fee, skip_proration=False still proratestests/test_first_payment_blast.py— 2 new blast tests (flag forwarding) + 3 new checkout route tests (prorated, non-prorated, invalid token)Test Plan
Manual verification:
POST /admin/email/first-payment?test_email=X&skip_proration=true— email shows flat fee, checkout link hasprorate=falseGET /checkout/first-payment?token=X&prorate=false— Stripe session chargesmonthly_fee * 100centsReview Checklist
Related Notes
369-first-payment-email-blastCloses #458
QA Review — PR #459
Summary
Adds
skip_prorationflag to the first-payment blast endpoint and email function, plus a newGET /checkout/first-paymentroute. 5 files changed, 363 additions, 11 deletions. All changes are well-scoped to the issue spec.Findings
No blockers found.
Minor observations (not blocking):
Private function import (
_calculate_prorated_fee): Imported fromservices/email.pyintoroutes/checkout.pywith a leading underscore. Functionally correct — it's internal to the package — but could be renamed to drop the underscore if it's now part of the module's public interface. Not blocking since this is an existing pattern.Checkout route is unauthenticated:
GET /first-paymentuses token-based auth viacontract_tokenquery param (no JWT/session). This matches the email-link flow pattern used elsewhere in the codebase (contract signing). The token is a UUID generated at contract creation, so it provides adequate security for this use case.Test coverage is solid: 8 new tests cover skip_proration on the email function (both true/false), blast endpoint flag forwarding (both true/false), checkout route with proration, without proration, and invalid token. All 15 tests pass.
Acceptance criteria verification:
skip_proration=trueon blast → email shows flat feeskip_proration=true→ checkout URL includes&prorate=falseprorate=falseon checkout → Stripe session usesmonthly_fee * 100VERDICT: APPROVED
PR #459 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Stripe / Gmail SDK
Architecture: Clean feature addition. The
skip_prorationflag threads correctly from admin route -> email service -> checkout URL query param -> checkout route. The checkout route follows the existing token-based public endpoint pattern (no auth dependency, token as identifier), consistent withcreate_checkout_session.Stripe integration:
setup_future_usage="off_session"for card saving is correct for the use case. Customer create-or-reuse pattern is sound. Thedb.commit()after savingstripe_customer_idis appropriate.Email service: The conditional HTML/plain-text generation is readable. The f-string ternary pattern for toggling proration explanation paragraphs works, though it produces trailing whitespace in the
skip_prorationcase (empty string concatenated). Not a blocker.BLOCKERS
None found. Test coverage is solid (7 tests across both files covering happy path, edge cases, flag forwarding, and 404 handling).
NITS
Hardcoded month name "April" --
fee_labelin bothemail.pyandcheckout.pyhardcodes "April fee" / "Prorated April fee". This works for the immediate use case but will require a code change for May onward. Consider extracting to a parameter or usingdatetime.now().strftime("%B"). Low priority since this is clearly a one-shot first-payment flow for the current cohort._calculate_prorated_feecalled with rawplayer.monthly_fee(nullable)In
checkout.pyline ~312 (diff context):The prorated path passes
player.monthly_fee(which could beNone) rather than the already-defaultedmonthly_feelocal. The email service (email.py) has the same pattern. This is presumably safe if_calculate_prorated_feehandlesNoneinternally (it existed before this PR), but it is inconsistent with the non-prorated path which uses the defaulted local. Worth a consistency pass in a follow-up.charge_centstype --_calculate_prorated_feereturns a dollar amount (int), and* 100converts to cents. If_calculate_prorated_feeever returns a float (e.g., for partial-day proration),charge_centswould be a float passed to Stripe'sunit_amount, which expects an int. Stripe would reject it. A defensiveint(...)wrap would be safer:charge_cents = int(_calculate_prorated_fee(player.monthly_fee) * 100).Plain text double newline -- When
skip_proration=True,full_fee_lineis""and the plain body getsf"{full_fee_line}\n"which produces a bare\n(extra blank line). Cosmetic only.asyncmismatch -- The existingcreate_checkout_sessionis declaredasync def(line 104 in main). The newfirst_payment_checkoutisdef(sync). Both callstripe.*synchronously. Not a bug (FastAPI handles sync endpoints in a threadpool), but inconsistent. Match the existing pattern for consistency.SOP COMPLIANCE
458-skip-proration-flagreferences issue #458PROCESS OBSERVATIONS
369-first-payment-email-blast(notmain), which is appropriate for a stacked feature branch. The merge path is 458 -> 369 -> main._make_unauthenticated_clienthelper in tests is well-structured for testing public endpoints without auth overhead.VERDICT: APPROVED