Validation: POST /admin/contract/offer endpoint (#425)
Validation: POST /admin/contract/offer endpoint (basketball-api#425)
Verdict
PASS — fully deployed, end-to-end tested in prod, 6 successful mints including the first-ever signed→offered tier-change flow.
Scope
- Forgejo issue: basketball-api#425
- PR merged: #426
- Board item: #931 on board-westside-basketball
- Deliverable: new
POST /admin/contract/offerendpoint +services/contract_offers.py+tests/test_contract_offer.py(9 test cases) + alembic migration 041 creatingcontract_audit_logtable
What was validated in prod
- Deployment — alembic_version advanced to 041 on basketball-api pod (verified via
kubectl exec ... psql -c "SELECT version_num FROM alembic_version;"). New podbasketball-api-5c4b9bcc-vvfsxrunning stable. - Schema —
contract_audit_logtable exists with correct 8 columns (id, ts, player_id FK, event_type, old_state jsonb, new_state jsonb, actor, source), PK, index on player_id. - Endpoint routing + auth —
curl POST /admin/contract/offerwithout JWT returns HTTP 401 (correctly auth-gated via Keycloak). - State transition: none → offered — minted offers for 5 players (Alice 202, Brian 191, Kevin 198, Vince 189, Marie 192). Each returned valid
contract_token,contract_url, flipped contract_status to 'offered'. HTTP 200 on all 5. - State transition: signed → offered (tier change) — Jacelyn Bronson (player id 97) moved from 16U Elite Queens (signed, $160) to 16U Local Queens (offered, $160). Audit row written to
contract_audit_logwith id=1, event_type='tier_change', actor='draneylucas@gmail.com'. Previous signed state archived in old_state JSONB. First-ever signed-to-re-offer flow on the platform. - Integration with blast — after minting, all 6 newly-offered players were picked up by
query_unsigned_contractsin the subsequent blast (verified by email_log entries with correct recipients).
Known edge case discovered during validation
Jacelyn's first mint attempt returned HTTP 500 due to a latent AgeGroup enum bug — not a #425 bug. The migration 040 (#422) had inserted
age_group='U16' into team 12, which SQLAlchemy couldn't read (Python enum names are lowercase, values uppercase — SQLAlchemy reads by name). Mitigation: UPDATE teams SET age_group = NULL WHERE id = 12. Root cause tracked as basketball-api#446. The endpoint code itself is correct.Audit trail
The
contract_audit_log table now has its first real prod row (id=1) documenting Jacelyn's tier change. This is the foundation for the write-capability audit system scoped in pal-e-deployments#104.Known debt (non-blocking)
- Nit (from QA review pass 2):
_CONTRACT_BASE_URLhardcoded incontract_offers.py— should move to env config eventually. Non-blocking, Epilogue item.
Related
- basketball-api#422 (migration 040, 16U Local Queens team) — co-validated tonight
- basketball-api#420 (Alice dedupe migration 042) — co-validated tonight
- basketball-api#424 (Marcus 2026-04-10 batch — the umbrella ticket that exercised this endpoint end-to-end)
- basketball-api#446 (AgeGroup enum mismatch — latent bug exposed during validation)