Skip to content

align donation request review schema with user flows - #122

Open
brian-fu wants to merge 8 commits into
feat/approve-reject-donation-itemfrom
feat/donation-request-schema
Open

align donation request review schema with user flows#122
brian-fu wants to merge 8 commits into
feat/approve-reject-donation-itemfrom
feat/donation-request-schema

Conversation

@brian-fu

@brian-fu brian-fu commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — base is feat/approve-reject-donation-item, not main. Second of three: approve-reject-donation-item (dialogs) → this (backend schema)donation-request-page (frontend). Review/merge the base first.

Backend schema + API for the admin donation-request review flow from Figma. No frontend changes.

Enums

  • FurnitureStatus.REJECTED
  • FurnitureRejectionReasoncondition / pickup / location / other (mirrors DEFAULT_REJECT_REASONS in RejectItemDialog.tsx)
  • DonationReviewStatuspending_review / partially_reviewed / reviewed / scheduled (derived, not stored)

Models + migration (20260724_donation_review)

  • Furniturerejection_reason, rejection_details, reviewed_at
  • new furniture_photos table — ordered, cascade-delete. Donors submit several photos per item, which the single image_url string could not hold. image_url stays as the thumbnail and seeds the table on migrate.
  • Pickupscheduled_date, note, confirmed_at, and route_id relaxed to nullable — a pickup is scheduled against a donation during review, before dispatch assigns a route.
  • Donationsmoking_household, has_pets — asked once on the donor form; previously only existed per-Furniture.

Endpoints

Method Path Purpose
GET /donations/{id}/detail donation + donor + items + photos + pickup
GET /furniture/{id}/detail item + ordered photos
PUT /furniture/{id}/photos replace the photo set; list order = display order, max 5
POST /furniture/{id}/approve
POST /furniture/{id}/reject
POST /pickups/{id}/confirm

Business rules

  • Rejecting requires a reason; reason other requires non-blank details; approving clears both. Enforced on create, PUT (validates the merged row) and the review endpoints. Changing the reason clears details written for the previous one.
  • review_status is computed from item statuses and the pickup, so it can't drift. A scheduled pickup only outranks a review that is actually complete.
  • A donation can hold both a review-time pickup (date, no route) and a dispatch one (route, no date); the review screen resolves to the scheduled one.
  • Moving a confirmed pickup to a new date clears confirmed_at (matches the Edit Pickup dialog warning). Editing only the note, or re-sending the same date, keeps it.
  • confirm is idempotent and refuses a pickup with no date. Notes are capped at 500 characters.
  • Datetimes accepted with a UTC offset are converted to naive UTC before storage. The columns are TIMESTAMP WITHOUT TIME ZONE and asyncpg rejects tz-aware values, so Date.toISOString() output would otherwise fail on Postgres.

Testing

  • 32 tests in tests/test_donation_review.py; full suite 164 passing; black --check clean.
  • Migration verified against a scratch Postgres 15: full chain applies to an empty database, the image_url backfill seeds correctly, alembic check reports no model drift, and upgrade → downgrade → upgrade round-trips. The downgrade is deliberately lossy — it drops pickups that have no route and the whole photo table.
  • ⚠️ The production database is not provisioned yet, so the migration ships unapplied.

Support the admin donation-request review flow from Figma:

- Furniture gains rejection_reason / rejection_details / reviewed_at, and
  FurnitureStatus gains REJECTED. Rejecting requires a reason; reason "other"
  requires free-text details; approving clears any earlier rejection. The
  invariant is enforced on create, PUT and the new review endpoints.
- New furniture_photos table — donors submit several ordered photos per item,
  which Furniture.image_url (a single string) could not hold. image_url stays
  as the thumbnail and seeds the new table on migrate.
- Pickup gains scheduled_date / note / confirmed_at, and route_id relaxes to
  nullable: an admin schedules a pickup against a donation during review, long
  before dispatch assigns it to a route. Moving a confirmed pickup to a new
  date clears the confirmation, matching the warning in the edit dialog.
- Donation gains smoking_household / has_pets. The donor form asks these once,
  but they previously only existed per-Furniture, so the donor card had no
  single source to read.

review_status (pending_review / partially_reviewed / reviewed / scheduled) is
derived in the service layer rather than stored, so it cannot drift when an
item is re-reviewed or a pickup is removed.

New endpoints:
  GET  /donations/{id}/detail   donation + donor + items + photos + pickup
  GET  /furniture/{id}/detail   item + ordered photos
  PUT  /furniture/{id}/photos   replace the photo set; list order = position
  POST /furniture/{id}/approve
  POST /furniture/{id}/reject
  POST /pickups/{id}/confirm

The database is not provisioned yet, so the migration ships unapplied and is
verified only against the pytest suite (157 passing).
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
home-again Ready Ready Preview, Comment Jul 25, 2026 11:04pm

It was sitting among the unrelated private helpers near the bottom of the
file, far from the two functions that call it.
The rest of the codebase writes datetime.now(timezone.utc).replace(tzinfo=None)
inline — 33 lambdas in models/base.py and pickups.confirm_pickup do exactly
this — so a one-off helper in furniture.py was the outlier.
Every enum in enums.py is declared as a str mixin (class X(str, enum.Enum)),
so members already compare equal to their raw string values --
FurnitureStatus.REJECTED == "REJECTED" is True. Converting via .value before
the comparisons in _validate_rejection_fields was a no-op, so the helper and
both of its call lines are gone rather than inlined.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Not ready to approve

The migration currently relies on gen_random_uuid() for seeding, which is not available on default Postgres installs without enabling an extension and is likely to break alembic upgrade in real environments.

Pull request overview

Adds backend schema, persistence, and API endpoints to support the admin donation-request review flow (approve/reject with reasons, ordered item photos, donation “detail” composite including derived review_status, and pickup scheduling/confirmation).

Changes:

  • Introduces review-related enums and new/extended schemas for donation detail, furniture rejection, photos, and pickup confirmation.
  • Adds furniture_photos persistence + service/API to replace an item’s ordered photo set, plus computed donation review_status.
  • Extends pickup flow with scheduling fields, confirmation semantics, and corresponding tests + migration.
File summaries
File Description
backend/tests/test_donation_review.py New end-to-end-ish tests spanning furniture review, photo replacement/order, donation detail aggregation, and pickup scheduling/confirmation.
backend/migrations/versions/20260724_donation_review_flow.py Migration adding new donation/pickup/furniture columns and the new furniture_photos table with seed-from-thumbnail logic.
backend/app/services/pickups.py Adds note length validation, confirmation logic, and confirmation-clearing when scheduled date changes.
backend/app/services/furniture.py Adds approve/reject review operations, rejection invariant validation on create/update, and replace-all furniture photo set logic.
backend/app/services/donations.py Adds donation detail loader, pickup selection helper, derived review_status, and response shaping helper.
backend/app/schemas.py Adds schemas for furniture photos, rejection payload, furniture detail (with photos), pickup fields, and donation detail composite response.
backend/app/models/base.py Adds donation household fields, pickup scheduling/confirmation fields + nullable route, furniture rejection fields, and FurniturePhoto model + relationship.
backend/app/models/init.py Exports FurniturePhoto.
backend/app/enums.py Adds DonationReviewStatus, FurnitureStatus.REJECTED, and FurnitureRejectionReason.
backend/app/api/pickups.py Adds POST /pickups/{id}/confirm endpoint.
backend/app/api/furniture.py Adds furniture detail endpoint, photo replacement endpoint, and approve/reject endpoints.
backend/app/api/donations.py Adds GET /donations/{id}/detail composite endpoint.

Review details

  • Files reviewed: 12/12 changed files
  • Comments generated: 2
  • Review effort level: Low

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/app/schemas.py
Comment thread backend/migrations/versions/20260724_donation_review_flow.py
@brian-fu brian-fu changed the title backend: donation request review schema align donation request review schema with user flows Jul 25, 2026
Six fixes, all reproduced against a real Postgres 15 before and after.

Normalise inbound datetimes to naive UTC (schemas). The DateTime columns are
TIMESTAMP WITHOUT TIME ZONE and asyncpg refuses a tz-aware value outright, so
POST /pickups with the 'Z' that Date.toISOString() emits raised a DataError —
a 500, since it isn't an IntegrityError. SQLite silently dropped the offset
without converting, which is why the suite never saw it. Also applied to
Route.date, which had the identical latent failure.

Pick the scheduled pickup, not the newest one (donations service). Once
dispatch created its route-assigned row, get_active_pickup returned that
instead, and the donor's confirmed date and confirmed_at vanished from the
review screen.

Stop a pickup masking an unfinished review (donations service). Scheduling now
only outranks a review that is actually complete, so partially_reviewed stays
visible.

Clear stale rejection_details when the reason changes (furniture service).
Free text written for 'other' survived onto a different reason and kept
rendering on the rejected item's card.

Detach items before the lossy pickup delete (migration downgrade). The delete
tripped fk_furniture_pickup_id and aborted the whole downgrade whenever an item
was assigned to a review-scheduled pickup.

Drop position from the photo request body and cap the set at 5 (Copilot).
The endpoint always derived position from list order, so accepting one
advertised a field the server ignored. Removes the unused FurniturePhotoCreate.

Also orders Donation.furniture_items so the review screen's cards don't
reshuffle between loads.

Tests 25 -> 32, suite 157 -> 164; black clean.
Fixing Copilot's point by adding a third photo model left FurniturePhotoBase
with a single subclass and url declared twice. The Base existed to be shared by
FurniturePhotoCreate and FurniturePhoto; once Create was deleted as unused, the
fix was simply to move position off the Base rather than add a class beside it.

FurniturePhotoBase is now what a client sends (url only) and FurniturePhoto adds
position alongside id and the timestamps — same shape on the wire, one fewer
model, and the Base/response naming the rest of the file uses.
The migration creates ix_furniture_photos_furniture_id but the model didn't
declare it, so `alembic check` reported drift and the next autogenerate run
would have proposed dropping the index. The existing email indexes don't drift
because those columns are unique=True, which alembic matches on its own.
Not worth it at this table's expected size, and the only query against
furniture_id fetches one item's photos. Removed from the migration as well as
the model — dropping just the model attribute would have left the index in the
database and reintroduced the autogenerate drift it was added to fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants