Skip to content

feat(api): add receipts and receipt items tables with prices - #54

Merged
lauriechammah merged 2 commits into
developfrom
feature/16-receipts-price
Nov 8, 2026
Merged

lauriechammah merged 2 commits into
developfrom
feature/16-receipts-price

Conversation

@MarcAntoineGmd

@MarcAntoineGmd MarcAntoineGmd commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #16

Overview

The goal of this ticket is to design the tables for receipts, including the price information needed for invoice calculations, linked to donors and food items.

receipts: id (uuid, generated), donor_id, total_amount (numeric(12,2), cannot be negative), receipt_date (date, defaults to today in America/Montreal, used for the tax year), created_at (timestamptz, default now(), stored in UTC).

receipt_items: one row per food item on a receipt. receipt_id, food_item_id, donor_id, quantity (numeric(12,2), greater than 0), unit (varchar(20), not empty), unit_price (numeric(12,4), greater than 0), quality_percent (numeric(5,2), between 0 and 100). The primary key is (receipt_id, food_item_id).

Links:

  • A receipt belongs to one donor (ON DELETE RESTRICT).
  • receipt_items links to receipts through (receipt_id, donor_id) (CASCADE) and to food_items through (food_item_id, donor_id) (RESTRICT). A receipt can therefore only list food items from its own donor. Both parent tables get a unique constraint on (id, donor_id).
  • A unique index on food_item_id means a food item can only be on one receipt, so one donation cannot get two tax receipts.

Invoice calculations:

  • A line value is quantity x unit_price x quality_percent / 100, rounded to the cent, half away from zero. The receipt total is the sum of the rounded lines.
  • Quantity, unit, price and percentage are all copied onto each line, so later edits to the food item or the price grid do not change existing receipts.
  • All amounts use numeric, so there is no floating-point rounding.

Changes:

  • Receipt and ReceiptItem entities in src/MoissonMontreal.Core/Entities/
  • EF configurations in src/MoissonMontreal.Api/Data/Configurations/ (precision, constraints, composite relationships, UTC conversion of created_at)
  • MoissonDbContext: Receipts and ReceiptItems DbSets
  • Migration CreateReceipts in src/MoissonMontreal.Api/Data/Migrations/ (generated by dotnet ef)
  • SQL scripts generated from the migrations, saved without a BOM: docs/database/sql/0003_create_receipts.sql, plus 0002_create_donors_and_food_items.sql, which was missing for [Issue]: Database schema: donors and food items #15
  • dbdiagram.io schema, diagram and documentation (including the rounding and time zone rules): docs/database/receipts.dbml, docs/database/images/receipts.png, docs/database/receipts.md
  • Integration tests: tests/MoissonMontreal.Tests/Api/ReceiptsMigrationTests.cs

Donor and FoodItem are not modified. The migration adds the unique constraint ak_food_items_id_donor_id to food_items.

Review feedback addressed

  • Quantity and unit: they are now copied onto receipt_items, so a line can be recalculated after the food item is edited.
  • Rounding rule: each line is rounded to the cent, half away from zero, and the total is the sum of the rounded lines. It is documented in receipts.md, with a test for the $0.225 case.
  • BOM: removed from 0002 and 0003. The script command in receipts.md now includes the step to remove it.
  • Unit price precision: unit_price now has 4 decimal places.
  • Duplicate receipts: there is a unique index on food_item_id. Issue [Issue]: Receipt status state machine #31 will need to relax it for cancel-and-replace receipts. This is noted in receipts.md.
  • Donor mismatch: this is now enforced by composite foreign keys that include donor_id, as suggested.
  • Time zone:
    • The new receipt_date column holds the Montreal date for the tax year.
    • created_at is converted to UTC on write, so DateTimeOffset.Now no longer throws.

Testing

Unit and integration tests: dotnet test (Docker must be running). All 59 tests pass, including 30 in ReceiptsMigrationTests. They run against PostgreSQL 17 and check that:

  • the migration applies cleanly on an empty database
  • a receipt is linked to its donor and to its food items
  • the total, unit price (4 decimals) and quality percentage are stored exactly
  • a line keeps its quantity and unit when the food item is edited
  • the rounded line values add up to the stored total (12.50 x 4.00 x 75% + 3 x 2.49 x 100% = 44.97)
  • 1 x 0.45 x 50% = 0.225 rounds to 0.23
  • receipt_date is the Montreal date, and a created_at with a -05:00 offset is saved and read back as the same moment in UTC
  • a grade C item at 0% is accepted
  • deleting a receipt deletes its lines but keeps the food items
  • each rule is enforced by the right constraint:
    • fk_receipts_donors_donor_id
    • fk_receipt_items_receipts_receipt_id_donor_id
    • fk_receipt_items_food_items_food_item_id_donor_id
    • pk_receipt_items
    • ix_receipt_items_food_item_id
    • ck_receipts_total_amount_not_negative
    • ck_receipt_items_quantity_positive
    • ck_receipt_items_unit_not_empty
    • ck_receipt_items_unit_price_positive
    • ck_receipt_items_quality_percent_range

Manual testing: the three SQL scripts were run in order with psql -v ON_ERROR_STOP=1 on an empty PostgreSQL 17 database. Then the following were checked by hand:

  • the rounding
  • the Montreal date
  • a food item from another donor
  • a receipt line with another donor
  • a food item on two receipts

To try it locally:

  1. docker compose up -d
  2. dotnet ef database update --project src/MoissonMontreal.Api
  3. docker exec -it moisson-db psql -U moisson -d moisson_dev, then \d receipts and \d receipt_items

Screenshots / Screencasts

No frontend changes. Diagram of the schema from dbdiagram.io:

Screenshot from 2026-11-06 23-38-42

Checklist

  • Code is neat, readable, and works
  • Code is commented where appropriate and well-documented
  • Commit messages follow our guidelines
  • Issue number is linked
  • Branch is linked
  • Reviewers are assigned (one of your tech leads)

Notes

  • The files in Data/Migrations/ are generated by dotnet ef, so the entities and configurations are the files worth reviewing.
  • The database does not check that total_amount equals the sum of the rounded lines. That is left to the donation value calculation ([Issue]: Donation value calculation #30).
  • Because donor_id is part of both composite keys, EF does not allow changing the donor of a saved receipt or food item.
  • Receipt number, status, approvals and cancellation are out of scope and belong to their own issues ([Issue]: Receipt status state machine #31, [Issue]: Receipt numbering #32).

@MarcAntoineGmd MarcAntoineGmd added this to the Sprint - 1 milestone Nov 6, 2026
@MarcAntoineGmd MarcAntoineGmd self-assigned this Nov 6, 2026
@lauriechammah
lauriechammah requested review from lauriechammah and removed request for jspngl November 6, 2026 16:19

@anis022 anis022 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I left a few inline comments to address before merging, mostly about recalculating receipts reliably and data integrity.

@anis022 anis022 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@lauriechammah lauriechammah left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good!

@lauriechammah
lauriechammah merged commit f995cd0 into develop Nov 8, 2026
3 checks passed
@lauriechammah
lauriechammah deleted the feature/16-receipts-price branch November 8, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants