feat(tickets_v2): add per-product VAT rate and receipt VAT breakdown#967
Draft
japsu wants to merge 22 commits into
Draft
feat(tickets_v2): add per-product VAT rate and receipt VAT breakdown#967japsu wants to merge 22 commits into
japsu wants to merge 22 commits into
Conversation
hannesj
reviewed
Apr 24, 2026
Contributor
|
Some early smokes... Found and fixed issues with Claude Opus in feat/tickets-v2-vat-fixes
Todo:
|
Products now have a vat_percentage field (Finnish rates: 0%, 10%, 13.5%, 25.5%). Prices remain VAT-inclusive. Receipt emails show a dynamic VAT breakdown instead of the hardcoded "(VAT 0%)" placeholder. The shop product card shows the applicable rate, and the order summary table includes a per-rate VAT breakdown in the footer. The admin product form exposes vatPercentage as a SingleSelect field; changing the rate triggers a new product revision. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PaytrailItem model and populates the items array in Paytrail payment requests, enabling per-rate VAT breakdown in Paytrail merchant reports. Each order line becomes one item with unitPrice, units, vatPercentage, and a description (product title). For new orders, the order is fetched back from the DB inside the same transaction to retrieve product details. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add format_vat_rate helper to strip trailing zeros from VAT rate display (25.50 → 25.5, 10.00 → 10). Applied in email templates via template filter and in frontend via parseFloat in translation functions. - Assert Paytrail item sum equals payment amount via model_validator on CreatePaymentRequest, catching mismatches at construction time. - Raise UnsaneSituation instead of silently passing empty items when fetched_order is None after creation in app.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
format_vat_rate now accepts a language parameter and uses comma as decimal separator for Finnish and Swedish locales (25,5%) while keeping period for English (25.5%). Email templates pass the locale explicitly via filter argument (eg. format_vat_rate:"fi"). On the frontend, a new formatVatRate helper follows the same pattern as formatMoney. ProductCard and ProductsTable accept a locale prop and format the rate before passing it to the vatIncluded translation function, which now receives a pre-formatted string. Also adds missing vatPercentage to GraphQL queries in profile order detail and admin order detail pages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vatIncluded is a function that cannot be serialized across the server/client boundary. Moving it to serverAttributes prevents the 'Functions cannot be passed directly to Client Components' error on the /products page. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CreateProductForm backend requires vat_percentage but the frontend new product modal was missing this field, causing a validation error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CreateProduct mutation was passing form_data directly to the Django form without converting camelCase keys (e.g. vatPercentage) to snake_case (vat_percentage), causing validation to fail. The UpdateProduct mutation already did this conversion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Paytrail API requires vatPercentage to be a number, but Pydantic v2 serializes Decimal as a string in JSON mode. Changing the type to float ensures correct JSON serialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The order API serializes with by_alias=True, but OrderProduct was missing the vatPercentage alias. The frontend received vat_percentage (snake_case) which didn't match its vatPercentage interface, causing NaN% VAT display and duplicate React keys. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3a12458 to
e00b013
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rows are months, columns are VAT percentages found in paid orders plus a total column. Only paid orders (status=3) are included; refunds are noted in the footer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VAT amount = gross * rate / (100 + rate) since prices are VAT-inclusive. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pull title, column title, and footer dicts into module-level constants so the empty-result and non-empty paths share the same wording. - Use the shared format_vat_rate helper for column titles (with Swedish added) instead of duplicating the formatting logic. - Quantize values to cents in Decimal space before converting to float so rounding is deterministic and the SUM total row matches the per-cell sums. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the per-rate gross/vat/net summation into VatBreakdownLine.from_order_products so the formula lives next to the model and OrderMixin.vat_breakdown is a one-liner. Also hoists the local defaultdict import to module scope. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Decimal('10.00').normalize() returns Decimal('1E+1'), so the previous
str(rate.normalize()) produced '1E+1' for 10% VAT and '1E+2' for 100%,
breaking receipt emails (used as a Django template filter) and the
admin order detail VAT rows.
Use {rate:f} formatting and strip trailing zeros from the string instead.
The asserted doctest output now actually holds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both NullProvider and PaytrailProvider short-circuit zero-price orders before reading order_products, so the extra Order.get round trip after order.save() is wasted work for those orders. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unit: - format_vat_rate parametrized over the edge cases that broke before the scientific-notation fix (10.00 -> '10', not '1E+1') - VatBreakdownLine.from_order_products across empty, multi-rate, and zero-rate inputs Integration (pytest-django): - VatByMonth.report on an empty event - A full run with multiple months, multiple VAT rates, an unpaid order (excluded by status filter) and a zero-VAT paid order (excluded by the vat_percentage > 0 filter) - UTC-vs-Europe/Helsinki month boundary check - Localized VAT column titles Also fix Report.total_row showing '' instead of 'Total' in the first column: the month column was using total_by=TotalBy.NONE which suppresses the label; default total_by=SUM is the right choice for a STRING header column. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
vat_percentagefield toProduct(Finnish rates: 0%, 10%, 13.5%, 25.5%); prices remain VAT-inclusive(VAT 0%)placeholderProductsTable) includes per-rate VAT breakdown rows in the footervatPercentageas aSingleSelectfield; changing the rate triggers a new product revision (same as price)Test plan
DEBUG=True uv run python manage.py migrateProductCardshows "incl. X% VAT" next to the price in the shopdocker compose -f docker-compose.test.yml run --rm testnpm run test(fromkompassi-v2-frontend/)🤖 Generated with Claude Code