feat(tickets_v2): add per-product VAT rate and receipt VAT breakdown#967
Draft
feat(tickets_v2): add per-product VAT rate and receipt VAT breakdown#967
Conversation
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 <[email protected]>
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 <[email protected]>
- 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 <[email protected]>
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 <[email protected]>
hannesj
reviewed
Apr 24, 2026
Comment on lines
+3
to
+6
| const formatted = num.toString(); | ||
| if (locale === "fi" || locale === "sv") { | ||
| return formatted.replace(".", ","); | ||
| } |
There was a problem hiding this comment.
What about using new Intl.NumberFormat(locale).format(num)?
Contributor
|
Some early smokes... Found and fixed issues with Claude Opus in feat/tickets-v2-vat-fixes
Todo:
|
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 <[email protected]>
The CreateProductForm backend requires vat_percentage but the frontend new product modal was missing this field, causing a validation error. Co-authored-by: Copilot <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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