feat: add GET /codes endpoint, enrich CodeResponse with location_name #15
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "14-add-get-codes-endpoint-enrich-coderespon"
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
Add a new
GET /codesendpoint that lists all coupon codes for the authenticated user across all locations in a single JOIN query. EnrichCodeResponsewithlocation_namefield populated in both the new endpoint and existing location-scoped endpoints.Changes
src/mcd_tracker_api/schemas.py: Addedlocation_name: str | None = NonetoCodeResponsesrc/mcd_tracker_api/routes/codes.py: New router withGET /codesendpoint. Single JOIN query (CouponUsage + Location),?status=active|redeemed|expiredfilter via enum, sorted byearned_atdescsrc/mcd_tracker_api/routes/locations.py: Updatedlog_code(POST) andlist_codes(GET) to returnlocation_namefrom the already-fetched Location objectsrc/mcd_tracker_api/main.py: Registered the new codes routertests/test_codes.py: 12 new tests covering cross-location aggregation, location_name, sort order, user isolation, status filters, invalid status 422, and location_name in existing endpointsTest Plan
ruff formatandruff checkcleanactive= not redeemed AND not expired,redeemed= redeemed flag true,expired= expires_at <= nowGET /locations/{id}/codesunchanged in behavior, only enriched withlocation_nameReview Checklist
Related
Add a new GET /codes endpoint that lists all coupon codes for the authenticated user across all locations via a single JOIN query. Supports ?status=active|redeemed|expired filtering. Enrich CodeResponse with location_name (str | None) populated in both the new endpoint and existing POST/GET /locations/{id}/codes endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>PR #15 Review
DOMAIN REVIEW
Tech stack: Python 3.14 / FastAPI / SQLAlchemy / Pydantic / Alembic / PostgreSQL
Python/FastAPI/SQLAlchemy checklist:
CodeStatus(str, Enum)is correctly typed.CouponUsage JOIN Location) avoids N+1. Explicit column selection in the query avoids loading unnecessary data. Session passed via FastAPI dependency injection -- correct pattern.Query()parameter with description for the status filter.response_model=list[CodeResponse]correctly declared. Enum-based query parameter gives automatic 422 on invalid values.CouponUsage.keycloak_sub == user.subin the JOIN filter. No raw SQL. Enum restricts status values to 3 known strings. No injection risk.# noqa: E712comments on SQLAlchemy boolean comparisons (== False,== True) are correct -- SQLAlchemy requires this form for SQL generation.Semantic observation -- "active" definition:
The
_count_active_codes()function inlocations.py(used for slot enforcement) defines "active" asexpires_at > now()only, which includes redeemed codes. The newGET /codes?status=activedefines "active" asredeemed == False AND expires_at > now(), which excludes redeemed codes. This is the correct design choice: for slot enforcement, a redeemed code still occupies a slot until expiry; for user-facing display, "active" means "still usable." The docstring incodes.pydocuments this clearly. No action needed, just noting for future reference.Backward compatibility: The
location_namefield is added toCodeResponsewithstr | None = None, so existing API consumers that do not expect this field will not break. The existingGET /locations/{id}/codesandPOST /locations/{id}/codesnow returnlocation_nameas enrichment only.BLOCKERS
None.
get_current_userdependency.NITS
CodeResponse construction repeated 3 times (
codes.py:72,locations.py:157,locations.py:196). All three manually map the same 9 fields. Consider a helper likeCodeResponse.from_usage(usage, location_name=...)or a classmethod to reduce duplication. Not a blocker since this is not in an auth path, but if fields are added toCodeResponselater, all 3 sites must be updated in lockstep.No pagination on
GET /codes. For a personal tracker app, the dataset is likely small enough that this is fine. But if a user accumulates hundreds of codes over time, returning all in a single response could become slow. Alimit/offsetor cursor-based pagination would be a future improvement. Non-blocking for now.Sort order test is weak (
test_sorted_by_earned_at_desc). The test creates 3 codes in a tight loop without explicitearned_atvalues, relying onserver_default=func.now()to produce distinct timestamps. In practice, all 3 rows may get the samefunc.now()value within a single transaction, making the sort order test pass vacuously (a sorted list equals itself reversed when all values are identical). Consider setting explicitearned_atvalues withtimedeltaoffsets to make the test deterministic.from_attributes = Trueunused on new code path.CodeResponsehasmodel_config = ConfigDict(from_attributes=True), which allows direct ORM object mapping. The new endpoint manually constructsCodeResponse(...)from named Row tuple fields instead. This is fine and arguably more explicit, but worth noting that thefrom_attributesconfig is now vestigial for this particular endpoint's usage.SOP COMPLIANCE
14-add-get-codes-endpoint-enrich-coderespon(starts with #14)Closes #14plan-mcd-tracker)PROCESS OBSERVATIONS
Optionalfield default.Closes #14but does not referenceplan-mcd-tracker. Minor process gap -- should include the plan slug for traceability.VERDICT: APPROVED