feat: add skip_proration flag to first-payment endpoint and checkout #472

Open
forgejo_admin wants to merge 2 commits from 458-skip-proration-v2 into main

Summary

Add skip_proration to first-payment blast and prorate param to checkout route. Same logic as QA-approved #470, rebased cleanly on current main.

Changes

  • src/basketball_api/routes/checkout.pyprorate query param on GET /first-payment
  • src/basketball_api/routes/admin.pyskip_proration query param on POST /email/first-payment
  • src/basketball_api/services/email.pyskip_proration kwarg, dynamic fee label + conditional proration text

Test Plan

Review Checklist

  • 19 insertions, 3 files
  • Syntax verified
  • Same logic QA-approved on #470
  • sop-contract-offer
  • project-westside-basketball

Closes #458

## Summary Add `skip_proration` to first-payment blast and `prorate` param to checkout route. Same logic as QA-approved #470, rebased cleanly on current main. ## Changes - `src/basketball_api/routes/checkout.py` — `prorate` query param on `GET /first-payment` - `src/basketball_api/routes/admin.py` — `skip_proration` query param on `POST /email/first-payment` - `src/basketball_api/services/email.py` — `skip_proration` kwarg, dynamic fee label + conditional proration text ## Test Plan - [ ] Deploy, send test to draneylucas@gmail.com with skip_proration=true - [ ] Verify checkout charges $50 flat ## Review Checklist - [x] 19 insertions, 3 files - [x] Syntax verified - [x] Same logic QA-approved on #470 ## Related Notes - `sop-contract-offer` - `project-westside-basketball` Closes #458
feat: add skip_proration flag to first-payment endpoint and checkout
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
a2abd867e8
When skip_proration=true on POST /admin/email/first-payment, the email
shows the flat monthly fee. The checkout URL includes &prorate=false so
GET /checkout/first-payment charges monthly_fee * 100 cents directly.

Closes #458

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

PR #472 Review

DOMAIN REVIEW

Stack: Python / FastAPI / SQLAlchemy / Pydantic

Small, focused change: 19 additions across 3 files. Adds skip_proration flag to the admin first-payment blast endpoint and prorate query param to the checkout route. Logic is straightforward -- when skipping proration, charge the full monthly fee instead of the prorated amount.

Correctness observations:

  1. Plaintext email label bug -- In services/email.py, the HTML body correctly uses the dynamic {fee_label} variable ("April fee" vs "Prorated April fee"), but the plaintext fallback still has the hardcoded string "Prorated April fee: ${charge_fee}". When skip_proration=True, the plaintext email will incorrectly say "Prorated April fee: $200" instead of "April fee: $200". The fix is to use {fee_label} in the plaintext body too.

  2. "Covers remaining days" copy is misleading when skipping proration -- Both the HTML body ("Since you're joining partway through April, your first month's fee covers the remaining days in the month.") and the plaintext body have this same proration-specific copy that renders regardless of the skip_proration flag. When charging the full $200 flat fee, telling parents the fee "covers the remaining days" is inaccurate. This is a nit since the primary use case is a one-time admin override, but worth noting.

  3. FastAPI parameter patterns are correct -- skip_proration: bool = False as a query param on POST and prorate: bool = Query(True) on GET are both clean. The keyword-only * separator on send_first_payment_email is good practice.

  4. Checkout URL construction -- checkout_url += "&prorate=false" is safe because the base URL already contains ?token=..., so the ampersand is correct.

BLOCKERS

  1. No test coverage for the new skip_proration / prorate=False paths. There are existing tests for the prorated path (test_first_payment.py, test_first_payment_email.py) but zero tests exercise skip_proration=True on the email service, prorate=False on the checkout route, or skip_proration=True on the admin blast endpoint. New functionality with zero test coverage is a blocker per review criteria. Needed tests:

    • test_send_first_payment_email with skip_proration=True -- verifies charge_fee == monthly_fee, checkout URL contains &prorate=false, and email label is "April fee" (not "Prorated April fee")
    • test_first_payment_checkout with prorate=false -- verifies amount_cents == monthly_fee * 100 (no proration calc)
  2. Plaintext email label inconsistency (item 1 above) -- The HTML body uses {fee_label} but the plaintext body hardcodes "Prorated April fee". This means skip_proration=True produces an email where HTML says "April fee" but plaintext says "Prorated April fee". This is a functional bug affecting email clients that render plaintext.

NITS

  • The "Since you're joining partway through April, your first month's fee covers the remaining days" copy should ideally be conditional on skip_proration in both HTML and plaintext bodies. Not blocking since this is an admin override path, but it reads incorrectly when charging full price.
  • The prorate param on the checkout route is unauthenticated -- any user with a valid token could append &prorate=false to pay the full fee instead of the prorated amount. Since prorate=False charges MORE (not less), this is not a financial risk, but worth documenting the intent.

SOP COMPLIANCE

  • Branch named after issue (458-skip-proration-v2)
  • PR body follows template (Summary, Changes, Test Plan, Related)
  • Related references plan slugs (sop-contract-offer, project-westside-basketball)
  • No secrets committed
  • Tests exist -- no tests for new functionality

PROCESS OBSERVATIONS

This is noted as the same logic from QA-approved PR #470, rebased on current main. The prior approval does not carry forward -- the plaintext label bug (item 1) either existed in #470 and was missed, or was introduced in the rebase. Either way it needs fixing in this PR. The change failure risk is low (small surface area, admin-controlled flag), but the missing tests and plaintext inconsistency must be addressed before merge.

VERDICT: NOT APPROVED

Two blockers: (1) zero test coverage for the new skip_proration/prorate paths, (2) plaintext email label bug where {fee_label} is not used in the plaintext body. Fix both and re-request review.

## PR #472 Review ### DOMAIN REVIEW **Stack**: Python / FastAPI / SQLAlchemy / Pydantic Small, focused change: 19 additions across 3 files. Adds `skip_proration` flag to the admin first-payment blast endpoint and `prorate` query param to the checkout route. Logic is straightforward -- when skipping proration, charge the full monthly fee instead of the prorated amount. **Correctness observations:** 1. **Plaintext email label bug** -- In `services/email.py`, the HTML body correctly uses the dynamic `{fee_label}` variable ("April fee" vs "Prorated April fee"), but the **plaintext fallback** still has the hardcoded string `"Prorated April fee: ${charge_fee}"`. When `skip_proration=True`, the plaintext email will incorrectly say "Prorated April fee: $200" instead of "April fee: $200". The fix is to use `{fee_label}` in the plaintext body too. 2. **"Covers remaining days" copy is misleading when skipping proration** -- Both the HTML body ("Since you're joining partway through April, your first month's fee covers the remaining days in the month.") and the plaintext body have this same proration-specific copy that renders regardless of the `skip_proration` flag. When charging the full $200 flat fee, telling parents the fee "covers the remaining days" is inaccurate. This is a nit since the primary use case is a one-time admin override, but worth noting. 3. **FastAPI parameter patterns are correct** -- `skip_proration: bool = False` as a query param on POST and `prorate: bool = Query(True)` on GET are both clean. The keyword-only `*` separator on `send_first_payment_email` is good practice. 4. **Checkout URL construction** -- `checkout_url += "&prorate=false"` is safe because the base URL already contains `?token=...`, so the ampersand is correct. ### BLOCKERS 1. **No test coverage for the new `skip_proration` / `prorate=False` paths.** There are existing tests for the prorated path (`test_first_payment.py`, `test_first_payment_email.py`) but zero tests exercise `skip_proration=True` on the email service, `prorate=False` on the checkout route, or `skip_proration=True` on the admin blast endpoint. New functionality with zero test coverage is a blocker per review criteria. Needed tests: - `test_send_first_payment_email` with `skip_proration=True` -- verifies `charge_fee == monthly_fee`, checkout URL contains `&prorate=false`, and email label is "April fee" (not "Prorated April fee") - `test_first_payment_checkout` with `prorate=false` -- verifies `amount_cents == monthly_fee * 100` (no proration calc) 2. **Plaintext email label inconsistency** (item 1 above) -- The HTML body uses `{fee_label}` but the plaintext body hardcodes "Prorated April fee". This means `skip_proration=True` produces an email where HTML says "April fee" but plaintext says "Prorated April fee". This is a functional bug affecting email clients that render plaintext. ### NITS - The "Since you're joining partway through April, your first month's fee covers the remaining days" copy should ideally be conditional on `skip_proration` in both HTML and plaintext bodies. Not blocking since this is an admin override path, but it reads incorrectly when charging full price. - The `prorate` param on the checkout route is unauthenticated -- any user with a valid token could append `&prorate=false` to pay the full fee instead of the prorated amount. Since `prorate=False` charges MORE (not less), this is not a financial risk, but worth documenting the intent. ### SOP COMPLIANCE - [x] Branch named after issue (`458-skip-proration-v2`) - [x] PR body follows template (Summary, Changes, Test Plan, Related) - [x] Related references plan slugs (`sop-contract-offer`, `project-westside-basketball`) - [x] No secrets committed - [ ] Tests exist -- **no tests for new functionality** ### PROCESS OBSERVATIONS This is noted as the same logic from QA-approved PR #470, rebased on current main. The prior approval does not carry forward -- the plaintext label bug (item 1) either existed in #470 and was missed, or was introduced in the rebase. Either way it needs fixing in this PR. The change failure risk is low (small surface area, admin-controlled flag), but the missing tests and plaintext inconsistency must be addressed before merge. ### VERDICT: NOT APPROVED Two blockers: (1) zero test coverage for the new skip_proration/prorate paths, (2) plaintext email label bug where `{fee_label}` is not used in the plaintext body. Fix both and re-request review.
fix: plaintext fee_label bug + add skip_proration tests
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
087d19535d
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

PR #472 Review

Re-review after fixes for two blockers from previous review.

DOMAIN REVIEW

Stack: Python / FastAPI / SQLAlchemy / Stripe / Gmail email service.

Logic correctness: The skip_proration / prorate flag threads cleanly through all three layers:

  • POST /admin/email/first-payment?skip_proration=true passes the flag to send_first_payment_email
  • send_first_payment_email conditionally uses flat monthly_fee vs _calculate_prorated_fee(), builds dynamic fee_label, and appends &prorate=false to the checkout URL
  • GET /checkout/first-payment?token=X&prorate=false charges monthly_fee * 100 cents instead of calling _calculate_prorated_cents()

FastAPI typing: prorate: bool = Query(True) correctly parses string "false" to False via Pydantic coercion. The skip_proration: bool = False on the admin route is a plain query param default, also correct.

Keyword-only enforcement: The * separator before skip_proration in send_first_payment_email forces callers to pass it by name. Good defensive pattern.

BLOCKERS

None. Both previous blockers are resolved:

  1. Tests added -- Three new tests across three files:

    • test_first_payment_no_prorate -- verifies unit_amount == 5000 (50 * 100) when prorate=false
    • test_blast_skip_proration_forwarded -- verifies the admin blast endpoint forwards skip_proration=True to the email service
    • test_send_first_payment_email_skip_proration -- verifies HTML contains $200 (not $165), URL contains prorate=false, plain text uses "April fee" (not "Prorated April fee")
  2. Hardcoded label replaced -- fee_label is now dynamic: "April fee" when skipping proration, "Prorated April fee" otherwise. Used in both HTML and plaintext bodies.

NITS

  1. Misleading body copy when skip_proration=True: The paragraph "Since you're joining partway through April, your first month's fee covers the remaining days in the month" is still rendered unconditionally in both HTML (line ~1073) and plaintext (line ~1112). When skip_proration=True, they are paying the full month -- not "remaining days." Consider making this paragraph conditional or rewording it. Non-blocking since the fee label and amount are correct and the user will see "April fee: $200" which is unambiguous.

  2. PR body stats stale: The checklist says "19 insertions, 3 files" but the actual diff is 90 additions across 6 files. Tests were likely added after the PR body was written. Cosmetic only.

SOP COMPLIANCE

  • Branch named after issue: 458-skip-proration-v2
  • PR body has Summary, Changes, Test Plan, Related sections
  • Related references plan slug (sop-contract-offer, project-westside-basketball)
  • Tests exist (3 new test functions covering all 3 changed source files)
  • No secrets committed
  • No scope creep -- all changes directly serve the skip_proration feature

PROCESS OBSERVATIONS

Clean re-review cycle. Both blockers from the first review were addressed directly. The test coverage is thorough -- it validates the flag at each layer (admin route forwarding, email content, checkout amount). Change failure risk is low given the mock-based test approach and the flag defaulting to existing behavior (prorate=True / skip_proration=False).

VERDICT: APPROVED

## PR #472 Review Re-review after fixes for two blockers from previous review. ### DOMAIN REVIEW **Stack**: Python / FastAPI / SQLAlchemy / Stripe / Gmail email service. **Logic correctness**: The `skip_proration` / `prorate` flag threads cleanly through all three layers: - `POST /admin/email/first-payment?skip_proration=true` passes the flag to `send_first_payment_email` - `send_first_payment_email` conditionally uses flat `monthly_fee` vs `_calculate_prorated_fee()`, builds dynamic `fee_label`, and appends `&prorate=false` to the checkout URL - `GET /checkout/first-payment?token=X&prorate=false` charges `monthly_fee * 100` cents instead of calling `_calculate_prorated_cents()` **FastAPI typing**: `prorate: bool = Query(True)` correctly parses string `"false"` to `False` via Pydantic coercion. The `skip_proration: bool = False` on the admin route is a plain query param default, also correct. **Keyword-only enforcement**: The `*` separator before `skip_proration` in `send_first_payment_email` forces callers to pass it by name. Good defensive pattern. ### BLOCKERS None. Both previous blockers are resolved: 1. **Tests added** -- Three new tests across three files: - `test_first_payment_no_prorate` -- verifies `unit_amount == 5000` (50 * 100) when `prorate=false` - `test_blast_skip_proration_forwarded` -- verifies the admin blast endpoint forwards `skip_proration=True` to the email service - `test_send_first_payment_email_skip_proration` -- verifies HTML contains `$200` (not `$165`), URL contains `prorate=false`, plain text uses `"April fee"` (not `"Prorated April fee"`) 2. **Hardcoded label replaced** -- `fee_label` is now dynamic: `"April fee"` when skipping proration, `"Prorated April fee"` otherwise. Used in both HTML and plaintext bodies. ### NITS 1. **Misleading body copy when skip_proration=True**: The paragraph "Since you're joining partway through April, your first month's fee covers the remaining days in the month" is still rendered unconditionally in both HTML (line ~1073) and plaintext (line ~1112). When `skip_proration=True`, they are paying the full month -- not "remaining days." Consider making this paragraph conditional or rewording it. Non-blocking since the fee label and amount are correct and the user will see "April fee: $200" which is unambiguous. 2. **PR body stats stale**: The checklist says "19 insertions, 3 files" but the actual diff is 90 additions across 6 files. Tests were likely added after the PR body was written. Cosmetic only. ### SOP COMPLIANCE - [x] Branch named after issue: `458-skip-proration-v2` - [x] PR body has Summary, Changes, Test Plan, Related sections - [x] Related references plan slug (`sop-contract-offer`, `project-westside-basketball`) - [x] Tests exist (3 new test functions covering all 3 changed source files) - [x] No secrets committed - [x] No scope creep -- all changes directly serve the skip_proration feature ### PROCESS OBSERVATIONS Clean re-review cycle. Both blockers from the first review were addressed directly. The test coverage is thorough -- it validates the flag at each layer (admin route forwarding, email content, checkout amount). Change failure risk is low given the mock-based test approach and the flag defaulting to existing behavior (`prorate=True` / `skip_proration=False`). ### VERDICT: APPROVED
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
This pull request has changes conflicting with the target branch.
  • tests/test_first_payment.py
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 458-skip-proration-v2:458-skip-proration-v2
git switch 458-skip-proration-v2

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.

git switch main
git merge --no-ff 458-skip-proration-v2
git switch 458-skip-proration-v2
git rebase main
git switch main
git merge --ff-only 458-skip-proration-v2
git switch 458-skip-proration-v2
git rebase main
git switch main
git merge --no-ff 458-skip-proration-v2
git switch main
git merge --squash 458-skip-proration-v2
git switch main
git merge --ff-only 458-skip-proration-v2
git switch main
git merge 458-skip-proration-v2
git push origin main
Sign in to join this conversation.
No description provided.