feat: migration 040 + JerseyPublicOrder model (#429) #433
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!433
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "429-jersey-public-migration-031"
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
Adds the
jersey_public_orderstable (System B public jersey intake, Keycloak-gated) via a new Alembic migration and appends aJerseyPublicOrderSQLAlchemy model tomodels.py.Discovered Scope (important — ticket was stale)
Ticket body claimed the alembic head on main was
030_add_registration_type_to_registrations.pyand that revision031was available. That is incorrect. The actual head onmainis039_add_recovery_email_sent.py— revisions 031 through 039 already exist (contract_config, contract_overrides, sponsors, first_payment, declined_contract, recovery_email_sent, etc.).Using
031would have collided with031_add_contract_config_to_teams.py. This PR uses revision040withdown_revision = "039". Branch name retains the429-prefix for label automation; the filename isalembic/versions/040_add_jersey_public_orders.py.The ticket body and
arch-jersey-intakedoc should be updated to reflect the correct migration number. Recommend Ava file a docs-refresh issue.Changes
alembic/versions/040_add_jersey_public_orders.py(new) — createspgcryptoextension idempotently, createsjersey_public_orderstable with UUID pk (gen_random_uuid()), threeCheckConstraints (kq,tier,status), two FKs (linked_parent_id,linked_player_id) withON DELETE SET NULL, and four indexes (idx_jpo_submitter_sub,idx_jpo_email,idx_jpo_status,idx_jpo_created_at DESC).downgrade()drops the indexes + table but leavespgcryptoalone.src/basketball_api/models.py(append-only) — newJerseyPublicOrderclass mapping the exact schema, matching the existing Mapped/mapped_columnstyle used byParent/Player/Sponsor. Imports forCheckConstraint,UUID,INET,uuidare declared inline below the final pre-existing class (Sponsor) as# noqa: E402aliases so no pre-existing import block or class is touched.tests/test_jersey_public_order.py(new) — three tests per AC: round-trip insert/fetch of all fields,IntegrityErroronkq='kingdoms',IntegrityErroronNULL submitter_keycloak_sub.git diff main -- src/basketball_api/models.py (append-only verification)
Pure additions, zero deletions. Diff starts after
Sponsor.tenantrelationship — no pre-existing class is modified.Test Plan / Verification Outputs
1. Model importable
2. Alembic upgrade 039 -> 040 (fresh DB, bootstrapped parents/players via
Base.metadata.create_allthen stamped 039 — pre-existing migrations 013/029 have tenant-seed bugs unrelated to this ticket)\d jersey_public_ordersconfirms:gen_random_uuid()idx_jpo_submitter_sub,idx_jpo_email,idx_jpo_status,idx_jpo_created_at DESC)ON DELETE SET NULL3. Round-trip
4. Pytest
5. Ruff
Acceptance Criteria
alembic upgrade headapplies 040 cleanly (verified against fresh Postgres 16)alembic downgrade -1reverses 040 cleanly (head -> 039)alembic upgrade headre-applies cleanly after downgradepgcryptoextension created idempotently (CREATE EXTENSION IF NOT EXISTS pgcrypto), NOT dropped on downgradekq,tier,statussubmitter_keycloak_subNOT NULL enforcedON DELETE SET NULLJerseyPublicOrderimportable frombasketball_api.modelsgit diff main -- src/basketball_api/models.pyshows only additions (66 insertions, 0 deletions)Review Checklist
040_add_jersey_public_orders.pymatchesdown_revision = "039"chainmodels.pydiff is additions-only below theSponsorclass (verify viagit diff main -- src/basketball_api/models.py)pgcryptoextension creation is idempotent and NOT reversed indowngrade()kings/queens,90-reversible/130-reversible-shooter,pending/reviewed/fulfilled/cancelled)ON DELETE SET NULLfor bothlinked_parent_idandlinked_player_ididx_jpo_created_atis DESCtests/test_jersey_public_order.pycover all three AC cases and passRelated
Closes #429
Related Notes
WS-S31(admin public jersey intake link)arch-jersey-intake#947(in_progress)#946(westside-landing frontend T1 — not touched by this PR)#430(POST endpoint),#432(GET endpoint)feedback_funnel_requires_auth.md— every row has a verified Keycloak identityfeedback_discovered_scope_always_tracked.md— discovered-scope note abovealembic/versions/030_add_registration_type_to_registrations.py,alembic/versions/036_add_sponsors_table.pyNotes for Reviewer
divisionenum type on re-run) were observed during local verification. They do NOT affect this PR; bootstrapping viaBase.metadata.create_all+alembic stamp 039was used to isolate the 039 -> 040 step. These should be filed as separate tickets if not already tracked..woodpecker.yaml. If CI fails on an earlier migration, that is pre-existing scope, not regression from this PR.PR #433 Review
DOMAIN REVIEW
Stack: Python / SQLAlchemy 2.x (Mapped/mapped_column) / Alembic / Postgres (pgcrypto, INET, UUID). Applying Python + SQLAlchemy + Alembic + OWASP checklist.
Migration
alembic/versions/040_add_jersey_public_orders.py:revision = "040",down_revision = "039"— correct chain, no collision with existing031_add_contract_config_to_teams.py. Dev agent's 031→040 rebase decision is correct.CREATE EXTENSION IF NOT EXISTS pgcrypto(idempotent) at top of upgrade — correct.gen_random_uuid()server default,submitter_keycloak_subNOT NULL,emailString(320),kqString(10),preferred_number_1..3nullable String(2),tierString(40),statusString(20) default'pending', FKs onlinked_parent_id/linked_player_id,reviewed_at/reviewed_bynullable,submission_ipINET nullable,created_atserver defaultnow().kq IN ('kings','queens')tier IN ('90-reversible','130-reversible-shooter')status IN ('pending','reviewed','fulfilled','cancelled')ondelete="SET NULL".idx_jpo_submitter_sub,idx_jpo_email,idx_jpo_status,idx_jpo_created_at(DESC viasa.text("created_at DESC")).downgrade()drops 4 indexes + table; explicit inline comment# Do NOT drop pgcrypto — other tables may rely on it.Downgrade sanity correct.Model
src/basketball_api/models.py(append-only):@@ -615,3 +615,69 @@— strictly additions belowSponsor.tenantrelationship. 66 insertions, 0 deletions. No pre-existing class touched.JerseyPublicOrdercolumns exactly mirror the migration schema: types, nullability, server defaults, and FKs all match.__table_args__carries the 3 CheckConstraints with identical SQL and names as the migration — no drift between ORM and DDL.iduses bothdefault=_uuid.uuid4(Python-side) andserver_default=text("gen_random_uuid()")(DB-side) — belt-and-suspenders, good pattern for round-trip tests that bypass server default.submission_ipdeclaredMapped[str | None] = mapped_column(_INET(), nullable=True)— INET stored, test comparesstr(fetched.submission_ip) == "192.0.2.10"which is correct because SQLAlchemy returnsipaddressobjects from INET.ForeignKey,String,DateTime,func,text,datetime,Mapped,mapped_column) from the pre-existing top-of-file block. No duplicate symbols.Tests
tests/test_jersey_public_order.py:test_insert_and_fetch_all_fields: meaningful — asserts every field after a freshdb.query().one()fetch (not just "insert didn't raise"). Also validates server defaults (status == "pending",created_at is not None,isinstance(order.id, uuid.UUID)). Good coverage of the round trip.test_invalid_kq_rejected: useskq="kingdoms", catchesIntegrityError, rolls back. Correct.test_null_submitter_keycloak_sub_rejected: passessubmitter_keycloak_sub=None, catchesIntegrityError, rolls back. Correct.OWASP / security review:
submitter_keycloak_subNOT NULL enforces the "every row has a verified Keycloak identity" convention fromfeedback_funnel_requires_auth.md. Good.ON DELETE SET NULLon parent/player FKs is correct for an intake table — we don't want cascade deletes nuking historical order records when a parent/player row is cleaned up.Ruff / PEP: Dev agent reports
ruff formatandruff checkclean. Inline import block belowSponsoris marked# noqa: E402, which is the correct escape hatch for a deliberate module-level import-after-code pattern.BLOCKERS
None.
NITS
# noqa: E402inmodels.py(lines ~620–624). The dev agent chose this deliberately to keep the diff strictly append-only belowSponsor— defensible tradeoff and documented in the PR body. Long-term, these should be hoisted to the top import block during a cleanup pass (track as Epilogue subphase, not a blocker).server_default=sa.text("'pending'")in the migration — usesserver_default="pending"(string form). SQLAlchemy quotes this correctly for Postgres, so it's functionally fine, butsa.text("'pending'")is slightly more explicit. Style-only.tierCHECK constraint absent. The three AC tests cover kq + NOT NULL + round-trip. A fourth test assertingtier='invalid'also raises IntegrityError would round out the CHECK coverage. Non-blocking because the migration code fortieris identical-pattern tokqwhich IS tested, and the round-trip test exercises a validtiervalue.SOP COMPLIANCE
429-jersey-public-migration-031— follows{issue-number}-{kebab-case}convention (the-031suffix is stale from the original ticket title; not a blocker per Ava's note, docs drift will be fixed separately).WS-S31, archarch-jersey-intake, board#947, and downstream issues#430/#432..env, no credentials.tofu fmtN/A (Python PR). Ruff format + check clean.feedback_discovered_scope_always_tracked.md.PROCESS OBSERVATIONS
submission_ipNULL vs populated — future POST endpoint (#430) should emit a metric or log for this. Not this PR's concern; flagging for #430 review.VERDICT: APPROVED