fix: May monthly payment email + dedup + full fee #521
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
ldraney/basketball-api!521
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "monthly-payment-email-may"
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
Changes
services/email.py: Rewritesend_first_payment_email— subject, HTML body, and plain text updated for current monthly billingroutes/admin.py: AddSubscriptionStatusimport, filternotin_(active, past_due)on first-payment blast query; add outstanding balance endpoint + importsroutes/checkout.py: Replace_calculate_prorated_cents(monthly_fee)withmonthly_fee * 100models.py: AddPaymentLinkmodel,PaymentLinkStatusenum,outstanding_balanceemail typeservices/email_queries.py: Add outstanding balance email queryalembic/versions/048_add_payment_links_table.py: Migration for payment_links tablescripts/backfill_payment_links.py: Backfill existing Stripe payment links into new tableTest Plan
POST /admin/email/first-payment?test_email=draneylucas@gmail.comReview Checklist
Related Notes
westside-basketball— projectwestside-billing-status-2026-05— billing status snapshotPR #521 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Alembic / Stripe SDK
This PR bundles three concerns into one branch: (1) rewriting the first-payment email copy to remove April proration references, (2) adding subscription dedup filtering on the blast endpoint, (3) adding an entirely new
PaymentLinkmodel + migration + outstanding balance email endpoint + backfill script. The first two map directly to issue #520; the third is prior-session work from closed issue #518 / PR #519.Migration 048 (
alembic/versions/048_add_payment_links_table.py):ALTER TYPE emailtype ADD VALUE IF NOT EXISTS 'outstanding_balance'-- correct PostgreSQL syntax for extending existing enums.downgrade()drops the table and thepaymentlinkstatusenum but does NOT remove theoutstanding_balancevalue from theemailtypeenum. PostgreSQL cannot remove individual enum values without recreating the type, so this is a known limitation, not a bug. Worth a comment in the migration though.Model (
models.py):PaymentLinkmodel aligns with the migration. Relationships declared withoutback_populates, which is consistent with the rest of the codebase (other models also use barerelationship()).PaymentLinkStatusenum is clean. MatchesOrderStatusshape.Checkout (
routes/checkout.py):_calculate_prorated_cents(monthly_fee)replaced withmonthly_fee * 100. The function_calculate_prorated_centsis now dead code at line 296 of checkout.py -- still defined but never called. Same for_calculate_prorated_feeat line 1012 of email.py.first_payment_checkout(line 313) still says "prorated first monthly payment" -- stale after this change.Admin (
routes/admin.py):Player.subscription_status.notin_([active, past_due])is correct and directly addresses issue #520./email/outstanding-balanceendpoint: groups activePaymentLinkrows by parent, builds item dicts, delegates tosend_outstanding_balance_email. Auth-gated viarequire_admin. Good.DRY concern -- outstanding balance grouping logic is duplicated:
routes/admin.pylines 1376-1407: queriesPaymentLink, groups byparent_id, builds item dicts, iterates.services/email_queries.pylines 299-356: queriesPaymentLink, groups byparent_id, builds HTML rows, iterates.send_outstanding_balance_email, the other builds pre-rendered HTML for the blast registry). This is not a security/auth path so it does not meet the BLOCKER threshold, but the divergence risk is real -- if the filter criteria change (e.g., add tenant filter, exclude canceled), both must be updated.Email (
services/email.py):send_outstanding_balance_emailis well-structured: HTML table with per-item "Pay Now" links, total summary, plain-text fallback, email logging.escape()on user-supplied data (player_name,description,tenant.name) -- XSS-safe for HTML email context.stripe_payment_link_urlis rendered as anhrefwithout escaping. Stripe URLs are system-generated, not user-supplied, so this is acceptable.Backfill script (
scripts/backfill_payment_links.py):--commitflag. Good safety pattern.~/secrets/basketball-api/stripe-api-key. No hardcoded keys.db.query(Product).get(order.product_id)--.get()is deprecated in SQLAlchemy 2.0 (should bedb.get(Product, order.product_id)). Non-blocking but worth noting.BLOCKERS
1. No test coverage for new functionality.
Three pieces of new functionality have zero tests:
PaymentLinkmodel/email/outstanding-balanceadmin endpointsend_outstanding_balance_emailservice functionquery_outstanding_balancesquery functionadmin_send_first_paymentThe existing test files (
test_admin.py,test_checkout.py) do not cover any of these new paths. Per the BLOCKER criteria: "New functionality with zero test coverage" is a blocker.At minimum, the dedup filter on
admin_send_first_paymentneeds a test proving that players withsubscription_status=activeare excluded from the blast query -- this is the core fix for issue #520 and a financial safety guard.NITS
Dead code:
_calculate_prorated_centsincheckout.py(line 296) and_calculate_prorated_feeinemail.py(line 1012) are now unreferenced. Remove them or add a deprecation comment.Stale docstring:
first_payment_checkoutdocstring (checkout.py line 313) still says "prorated first monthly payment" -- should say "full monthly payment" now.DRY opportunity: The outstanding balance grouping/filtering logic in
admin.pyandemail_queries.pycould share a common helper. Not blocking, but reduces divergence risk on future filter changes.Deprecated SQLAlchemy API:
db.query(Product).get(order.product_id)inbackfill_payment_links.pyline 220 -- usedb.get(Product, order.product_id)for SQLAlchemy 2.0 compatibility.Scope bundling: The PR bundles the #520 fix (email copy + dedup + full fee) with the #518 PaymentLink feature (model + migration + outstanding balance endpoint + backfill). The PR body acknowledges this ("prior session work bundled"). This makes the PR harder to review and revert independently.
Missing
player_idon outstanding balance email log:send_outstanding_balance_emailcreates anEmailLogwithoutplayer_id(email.py line 2594-2600). This is technically correct since the email covers multiple players, but differs fromsend_first_payment_emailwhich logsplayer_id. Consider documenting this intentional difference.SOP COMPLIANCE
monthly-payment-email-may, not520-monthly-payment-email-may. Does not follow{issue-number}-{kebab-case-purpose}convention.westside-basketballproject andwestside-billing-status-2026-05note, but no plan slug (acknowledged: "No plan slug").PROCESS OBSERVATIONS
VERDICT: NOT APPROVED
Reason: New functionality (PaymentLink model, outstanding balance endpoint, dedup filter) has zero test coverage. The subscription dedup filter is a financial safety guard that must be tested. Add at minimum:
admin_send_first_paymentprovingsubscription_status=activeplayers are excluded./email/outstanding-balanceendpoint (happy path + empty state).View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.