feat: add Receipt model, photo upload, rename CouponUsage fields #9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "8-add-receipt-model-photo-upload-and-updat"
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 the Receipt model with photo upload endpoints and rename CouponUsage fields (
code->bogo_code,used_at->earned_at) with a nullablereceipt_idFK linking coupon usages to receipts.Closes #8
Changes
src/mcd_tracker_api/models.py: Add Receipt model, rename CouponUsage.code to bogo_code, used_at to earned_at, add nullable receipt_id FK, add bidirectional relationshipssrc/mcd_tracker_api/routes/receipts.py: New router with POST /receipts (multipart form upload), GET /receipts (list), GET /receipts/{id}/photo (serve file)src/mcd_tracker_api/routes/locations.py: Update all code/used_at references to bogo_code/earned_atsrc/mcd_tracker_api/schemas.py: Rename fields in CodeCreate, CodeResponse, CodeRedeemResponse; add ReceiptResponse schemasrc/mcd_tracker_api/config.py: Add upload_dir setting (default /data/uploads)src/mcd_tracker_api/main.py: Register receipts routerpyproject.toml: Add python-multipart dependencyalembic/versions/002_add_receipt_rename_coupon_fields.py: Migration: CREATE TABLE receipts, rename columns, add receipt_id FKalembic/env.py: Import Receipt modeltests/conftest.py: Import Receipt modeltests/test_models.py: Update field names, add 5 new Receipt model teststests/test_locations.py: Update all code references to bogo_codetests/test_dashboard.py: Update all code references to bogo_codetests/test_admin_stats.py: Update all code references to bogo_codetests/test_receipts.py: New test file with 13 tests for receipt endpointsTest Plan
Review Checklist
Related
Add Receipt model with photo upload endpoints (POST /receipts, GET /receipts, GET /receipts/{id}/photo). Rename CouponUsage.code to bogo_code and used_at to earned_at. Add receipt_id FK (nullable) to CouponUsage for linking receipts to coupon codes. Alembic migration 002 handles schema changes with column renames and new table creation. All 76 tests passing. Closes #8 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Self-Review
Reviewed: 15 files, +615 / -45 lines
Blockers
None.
Findings
All clear. Implementation is complete and correct:
op.alter_column(..., new_column_name=...)which is the correct Alembic API for Postgres.code->bogo_codeandused_at->earned_atupdated across models, schemas, routes (locations, dashboard, admin), and all test files. Zero stale references in diff.receipt_idis nullable on CouponUsage, so existing rows are unaffected.Nit (non-blocking)
photo: UploadFile = ...uses Ellipsis as default.File()from FastAPI would be more idiomatic but functionally equivalent. Not worth a fix cycle.Verdict
Ship it.
PR #9 Review
DOMAIN REVIEW
Tech stack: Python 3.12 / FastAPI / SQLAlchemy 2.0 / Alembic / Pydantic v2 / Postgres / Woodpecker CI
This PR adds a
Receiptmodel with photo upload endpoints and renames twoCouponUsagecolumns (code->bogo_code,used_at->earned_at). 15 files changed, 615 additions, 45 deletions.Alembic Migration (002)
op.alter_column()withnew_column_name-- correct for PostgresALTER TABLE ... RENAME COLUMN.receipt_idFK is correctly nullable withondelete="SET NULL"-- existing rows get NULL, backward compatible.downgrade()is complete and correctly reverses all operations in the right order (drop FK column, rename columns back, drop table).down_revision = "001"matches001_initial_schema.py.receiptstable is created before the FK column is added tocoupon_usages-- ordering is correct.Photo Upload Security
uuid.uuid4().hex + ext) -- no path traversal via user-supplied filenames. Good..jpg,.jpeg,.png,.webp) -- good.image/*) -- good.await photo.read()-- good, though the entire file is read into memory before the check. Acceptable for 10 MB limit.os.path.join(UPLOAD_DIR, filename)with a UUID filename prevents directory traversal. No user-controlled path segments. Secure.Schema Rename Propagation
.code(in CouponUsage context) orused_atacross all source and test files.CodeCreate,CodeResponse,CodeRedeemResponse), routes (locations.py), and all 4 test files (test_locations.py,test_models.py,test_dashboard.py,test_admin_stats.py).User Isolation
create_receipt: filtersLocationbykeycloak_sub == user.sub-- user A cannot upload to user B's location. Good.list_receipts: filters byReceipt.keycloak_sub == user.sub-- user A cannot see user B's receipts. Good.get_receipt_photo: filters byReceipt.id == receipt_id AND Receipt.keycloak_sub == user.sub-- user A cannot download user B's photos. Good.test_upload_receipt_other_users_location_404,test_list_receipts_excludes_other_users,test_get_receipt_photo_other_user_404). Solid.CI Pipeline
.woodpecker.yamlhas apostgresservice container with correct env vars.MCD_TRACKER_DATABASE_URLpointing to the service container.ruff check,ruff format --check, andpytest-- comprehensive.python-multipartdependency added topyproject.toml-- CI will pick it up viapip install .[dev].Test Coverage
SQLAlchemy Patterns
Receipt <-> CouponUsageis a one-to-one viauselistdefault (implicit).CouponUsage.receiptisMapped["Receipt | None"]andReceipt.coupon_usageisMapped["CouponUsage | None"]-- SQLAlchemy infersuselist=Falsefrom theMappedannotation. Correct for SQLAlchemy 2.0.cascade="all, delete-orphan"onLocation.receipts-- deleting a location cascades to receipts. Tested.ondelete="SET NULL"onCouponUsage.receipt_id-- deleting a receipt NULLs the FK. Correct.BLOCKERS
None. No blocking issues found.
NITS
photo_pathleaked in API response (/home/ldraney/mcd-tracker-api/src/mcd_tracker_api/schemas.py:41):ReceiptResponseincludes the raw server filesystem path (e.g.,/data/uploads/receipts/abc123.jpg). This exposes internal directory structure to the client. Consider replacing with a URL like/receipts/{id}/photoor omitting it entirely since the photo endpoint already exists. Not a security vulnerability (the path alone does not grant access), but it is information leakage that is bad practice.FileResponsehardcodesmedia_type="image/jpeg"(/home/ldraney/mcd-tracker-api/src/mcd_tracker_api/routes/receipts.py:125): A PNG upload served via/receipts/{id}/photowill be returned withContent-Type: image/jpeg. Consider detecting the actual type from the file extension or storing the content type in theReceiptmodel.UploadFiledefault value (/home/ldraney/mcd-tracker-api/src/mcd_tracker_api/routes/receipts.py:29):photo: UploadFile = ...usesEllipsisas a default, which works but is unconventional forUploadFile.photo: UploadFilewith no default (orFile(...)from FastAPI) is more idiomatic.No
receipt_idindex oncoupon_usages(/home/ldraney/mcd-tracker-api/alembic/versions/002_add_receipt_rename_coupon_fields.py:49-55): The migration adds thereceipt_idFK column but does not create an index. For a one-to-one relationship this is fine at small scale, but if you later query receipts by their linked coupon usage, an index would help. Low priority.No
VOLUMEor PVC mention for/data/uploads: The Dockerfile does not declare aVOLUMEand there are no k8s manifests in this repo. The upload directory defaults to/data/uploadswhich will be ephemeral in a container. This is likely tracked as deployment work in a separate issue/phase, but worth noting -- uploaded photos will be lost on pod restart without a PVC.PR body
## Relatedsection does not reference a plan slug. It only references#8. Per SOP, this section should include the plan slug (e.g.,plan-mcd-tracker).SOP COMPLIANCE
8-add-receipt-model-photo-upload-and-updat)#8, no plan slugPROCESS OBSERVATIONS
tempfileandunittest.mock.patchto avoid filesystem side effects is a good pattern.VERDICT: APPROVED