fix: sync jersey fields to player record from generic checkout webhook #172
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!172
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "170-fix-generic-checkout-jersey-sync"
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
The generic checkout system (
orderstable from migration 013) was not syncingjersey_option,jersey_order_status,jersey_size, orjersey_numberback to theplayerstable after payment. Parents who ordered through/checkout/create-sessionappeared to have no jersey order when querying players. Two confirmed affected players: Anaiyah Fesolai (id=96) and Ayvah Apaisa (id=112).Changes
src/basketball_api/services/jersey_sync.py(NEW) -- Shared service module withsync_player_jersey_from_order()andmap_product_to_jersey_option(). Maps product name/type toJerseyOptionenum, extracts sizing fromorder.custom_data, and updates player record. Only acts on jersey-category products.src/basketball_api/routes/webhooks.py--_handle_generic_order_completed()now loads the product and callssync_player_jersey_from_order()after settingorder.status = paid.src/basketball_api/routes/checkout.py-- Opt-out path increate_checkout_session()now callssync_player_jersey_from_order()after creating the order. Added duplicate order prevention (409 for existing paid/pending orders for same player+product).alembic/versions/023_backfill_player_jersey_from_orders.py(NEW) -- One-time data migration to reconcile existing paid jersey orders back to player records. Idempotent, guarded byWHERE jersey_order_status = 'none'to avoid overwriting legacy-path data.tests/test_checkout.py-- 7 new tests: webhook jersey sync (reversible + warmup), non-jersey product skip, opt-out sync, duplicate order prevention (paid, pending, canceled-allows-reorder).Test Plan
python -m pytest tests/test_checkout.py tests/test_jersey.py -v-- 66 passedtest_temp_team_dedupin admin teams, unrelated -- confirmed fails on main)ruff formatandruff checkcleanReview Checklist
jersey_order_status = 'none')Related
alembic/versions/013_generic_checkout_system.pySelf-Review
Reviewed the full diff against issue #170 acceptance criteria.
Findings
1. Missing deprecation warning on legacy endpoint (FIXED)
Issue spec under Decisions says: "Legacy path (
/jersey/checkout): Keep active for now. Add a deprecationlogger.warningon each call." This was missing from the initial commit. Fixed in commitd7d3764-- addedlogger.warning("DEPRECATED: /jersey/checkout called. Use /checkout/create-session instead.")at the top ofjersey_checkout().2. Missing test:
test_webhook_skips_player_sync_for_non_jersey_product(FIXED)Issue spec listed this as a required test. Added in commit
f3abf24-- creates a tournament-category product, fires a webhook, and asserts player jersey fields remain untouched.Verified Against Acceptance Criteria
_handle_generic_order_completedsyncs player jersey fields for jersey-category productscheckout.pyopt-out path syncs player jersey fieldsjersey_order_status = 'none')test_temp_team_dedup)No Issues Found
Code is ready for human review.
PR #172 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Alembic / Stripe webhooks
Architecture (jersey_sync.py service)
The new
services/jersey_sync.pymodule correctly centralizes the jersey-field sync logic shared between the webhook handler (paid orders) and the checkout route (opt-outs). Both call sites (webhooks.py:167,checkout.py:181) pass(db, order, product)cleanly. The service guards onProductCategory.jerseybefore touching player records, preventing unintended side effects on tournament/equipment products.map_product_to_jersey_option()uses name-based matching ("warmup","reversible","jersey"substring checks) which is fragile if product names change. However, this mirrors existing product naming conventions and is the pragmatic approach given thatProductTypeonly distinguishesone_time/subscription/opt_out-- there is noJerseyOptionfield on theProductmodel itself. Acceptable for now.Webhook handler (webhooks.py)
The
_handle_generic_order_completed()function correctly loads the product and callssync_player_jersey_from_order()after settingorder.status = paid. The product fetch (db.query(Product).filter(Product.id == order.product_id).first()) is guarded withif product:. Good defensive coding.Note: The legacy
_handle_jersey_checkout_completed()(lines 180-235) still contains inline jersey-sync logic that reads from Stripe metadata rather thanorder.custom_data. This is intentionally kept for backward compatibility with existing checkout sessions created via the legacy/jersey/checkoutendpoint. The deprecation warning on that endpoint (jersey.py:182) makes the migration path clear.Checkout route (checkout.py)
The duplicate order prevention logic (lines 147-164) is well-scoped -- checks for
paidorpendingstatus on the same(player_id, product_id)pair. Returns 409 with an informative message including the existing order ID. Canceled orders correctly allow re-ordering.The
db.flush()beforesync_player_jersey_from_order()in the opt-out path (line 178) is correct -- ensuresorder.idis available before the sync function queries for the player.Migration 023
Idempotent backfill migration. Correctly uses
sa.table()inline references (no ORM dependency). TheWHERE jersey_order_status = 'none'guard prevents overwriting data set by the legacy jersey checkout flow. The migration's_map_name_to_jersey_option()duplicates the service-layer mapping, but this is correct practice for migrations -- they must be self-contained.No-op downgrade is documented with rationale. Chain is correct:
022 -> 023.Type hints and docstrings: All new functions have PEP 484 type hints and PEP 257 docstrings. Good.
Error handling:
JerseySizevalidation uses try/except ValueError with a warning log on invalid values (jersey_sync.py:76).jersey_numberis coerced to string (line 81). Both are defensive and correct.BLOCKERS
None.
NITS
Legacy handler DRY (
webhooks.py:180-235):_handle_jersey_checkout_completedduplicates jersey-sync logic inline. When the legacy endpoint is eventually removed, this handler should also be removed or refactored to delegate tojersey_sync.py. Consider opening a cleanup issue to track this.Name-based product mapping fragility (
jersey_sync.py:33-37): Themap_product_to_jersey_option()function relies on substring matching against product names. If someone creates a product named "Tournament Jersey Giveaway" in the equipment category, and it somehow hadcategory=jersey, the mapping would incorrectly returnreversible. The category guard insync_player_jersey_from_ordermitigates this, but a future improvement could add ajersey_optioncolumn to theProductmodel for explicit mapping.Migration
print()statement (023:119):print(f"Backfilled jersey fields for {updated} player(s)...")-- consider usingop.get_context().configlogger or just dropping it.print()in migrations works but is noisy in automated runs. Very minor.SOP COMPLIANCE
170-fix-generic-checkout-jersey-sync-> issue #170)Closes #170) and discovered scope (#171)PROCESS OBSERVATIONS
/jersey/checkoutendpoint now has a deprecation warning and docstring notice. The legacy webhook handler is preserved for in-flight sessions. Clean migration path.jersey_order_status = 'none') prevents overwriting legitimate data. Tests cover happy path, edge cases, and negative cases.VERDICT: APPROVED