feat: POST /public/interest endpoint for lead capture (#186) #187
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!187
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "186-interest-leads-endpoint"
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
Changes
src/basketball_api/models.py: AddedInterestLeadmodel with tenant_id, player_name, age_grade, parent_email, parent_phone, program, created_atalembic/versions/025_add_interest_leads_table.py: Migration to createinterest_leadstablesrc/basketball_api/routes/public.py: AddedPOST /public/interestendpoint withInterestLeadRequestschema, email/program validators,_notify_new_interest()stubtests/test_interest.py: 14 tests covering valid submissions, DB persistence, field normalization, and validation errorsTest Plan
Review Checklist
Related Notes
basketball-api-- the basketball program management APIQA Review -- PR #187
Scope Verification
PR implements exactly what issue #186 requests:
POST /public/interestendpoint for lead capture. No scope creep, no missing pieces.Code Quality
InterestLeadfollows existingmapped_columnstyle precisely. Correctly placed beforeOAuthToken. Good docstring.025correctly chains from024. Table definition matches model. Cleandowngrade().GET /public/teams. Email regex validation avoids unnecessaryemail-validatordependency. Program validation restricts tokings/queens. Best-effort notification with try/except never blocks the 201 response.Test Coverage
14 tests across two well-organized classes:
Findings
No issues found. Code is clean, follows existing patterns, tests are comprehensive.
VERDICT: APPROVED
PR #187 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / Alembic / Pydantic v2
Pattern consistency: The new endpoint follows the established
public.pyconventions well -- dedicated Pydantic request/response schemas, hardcodedtenant_id=1(matchingpublic_teams),Session = Depends(get_db)injection, and structured logging. The model placement inmodels.pyfollows existing ordering (alphabetical within the module). TheInterestLeadRequestschema correctly usesfield_validator(Pydantic v2 style) rather than the deprecatedvalidator. The migration chain is correct:024 -> 025.Migration quality: The
025_add_interest_leads_table.pymigration is clean and reversible (create_table/drop_table). Column types, lengths, and FK match the model. Theserver_default=sa.func.now()oncreated_atis correct.Email validation: The regex
_EMAIL_REis a reasonable RFC 5322 approximation. The.lower().strip()normalization is applied correctly. The order of operations in the validator (check regex first, then normalize) is correct.Program validation: Lowercases before comparison, returns lowercased value. Handles
Nonecorrectly.Error handling: The
_notify_new_intereststub is wrapped in a try/except that logs withexc_info=True-- failures never block the 201 response. This is the correct pattern for best-effort side effects.Test quality: 14 tests across two well-organized classes. Coverage includes: happy path (201), DB persistence verification, optional field omission, email lowercasing, program lowercasing,
created_atpopulation, missing required fields (3 tests), invalid email (2 tests), invalid program, empty body, and no-DB-record-on-validation-failure. Thepublic_clientfixture correctly overridesget_dbwithout auth -- matching the existingclientfixture pattern fromconftest.pybut deliberately unauthenticated. The_seed_tenantautouse fixture ensurestenant_id=1exists.BLOCKERS
None.
All four blocker criteria pass:
/public/teamspattern.NITS
Missing
index=Trueontenant_id: Every other model inmodels.pyusesmapped_column(ForeignKey("tenants.id"), index=True)fortenant_id. TheInterestLeadmodel usesmapped_column(Integer, ForeignKey("tenants.id"), nullable=False)-- no index. The migration also omits the index. This is a minor consistency gap. In a single-tenant deployment it has zero performance impact, but if the table grows or multi-tenancy is ever added, queries by tenant will need a sequential scan. Consider addingindex=Truefor pattern consistency.No
max_length/min_lengthon Pydantic string fields:player_nameandage_gradeaccept any non-empty string (Pydantic requires the field but allows""-- an empty string passes validation). A blankplayer_namewould create a useless lead record. Consider addingmin_length=1onplayer_name,age_grade, andparent_email(the email regex would catch empty email, but the other two have no guard). Alternatively, afield_validatorthat strips and rejects whitespace-only strings.No
max_lengthon Pydantic string fields: The DB columns have length constraints (String(200),String(100), etc.) but the Pydantic schema does not enforce them. A 10,000-characterplayer_namewould pass Pydantic validation and then fail at the DB level with a less user-friendly error. ConsiderField(max_length=200)onplayer_name, etc., to match the DB constraints.Logging PII:
_notify_new_interestlogsparent_emailat INFO level. This is a public endpoint so the volume of leads could be high. Consider whether email addresses in application logs are acceptable per your data handling posture, or whether the log should omit the email or use DEBUG level.player_nameandage_gradenot stripped: The email validator calls.lower().strip(), butplayer_nameandage_gradeare stored as-is. Leading/trailing whitespace in a player name would create messy data. Consider stripping these fields in the endpoint or via validators.SOP COMPLIANCE
186-interest-leads-endpointreferences issue #186Closes #186basketball-api -- the basketball program management APIinstead of a plan slug. Acceptable if this is standalone kanban work (no active plan).PROCESS OBSERVATIONS
_notify_new_interestTODO for gmail-sdk integration should be tracked as a separate issue to avoid it becoming forgotten tech debt.VERDICT: APPROVED