Validation: POST /admin/contract/offer endpoint (#425)

validation-425-2026-04-11 Doc

validation pass

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/offer endpoint + services/contract_offers.py + tests/test_contract_offer.py (9 test cases) + alembic migration 041 creating contract_audit_log table

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 pod basketball-api-5c4b9bcc-vvfsx running stable.
  • Schemacontract_audit_log table 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 + authcurl POST /admin/contract/offer without 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_log with 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_contracts in 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_URL hardcoded in contract_offers.py — should move to env config eventually. Non-blocking, Epilogue item.
  • 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)