Skip to content

feat(tickets_v2): add per-product VAT rate and receipt VAT breakdown#967

Draft
japsu wants to merge 10 commits intomainfrom
feat/tickets-v2-vat
Draft

feat(tickets_v2): add per-product VAT rate and receipt VAT breakdown#967
japsu wants to merge 10 commits intomainfrom
feat/tickets-v2-vat

Conversation

@japsu
Copy link
Copy Markdown
Contributor

@japsu japsu commented Apr 24, 2026

Summary

  • Adds vat_percentage field to Product (Finnish rates: 0%, 10%, 13.5%, 25.5%); prices remain VAT-inclusive
  • Receipt emails now show a dynamic VAT breakdown per rate instead of the hardcoded (VAT 0%) placeholder
  • Shop product card shows the applicable VAT rate next to the price
  • Order summary table (ProductsTable) includes per-rate VAT breakdown rows in the footer
  • Admin product form exposes vatPercentage as a SingleSelect field; changing the rate triggers a new product revision (same as price)

Test plan

  • Run migration: DEBUG=True uv run python manage.py migrate
  • Create a product with a non-zero VAT rate via the admin product form — confirm the field appears and saves correctly
  • Change the VAT rate on an already-sold product — confirm a new revision is created
  • Place an order for products with mixed VAT rates — confirm the receipt email shows correct per-rate VAT breakdown
  • Confirm ProductCard shows "incl. X% VAT" next to the price in the shop
  • Confirm the order summary page shows a VAT breakdown in the table footer
  • Backend tests: docker compose -f docker-compose.test.yml run --rm test
  • Frontend lint/type-check: npm run test (from kompassi-v2-frontend/)

🤖 Generated with Claude Code

japsu and others added 5 commits April 24, 2026 17:30
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]>
Comment on lines +3 to +6
const formatted = num.toString();
if (locale === "fi" || locale === "sv") {
return formatted.replace(".", ",");
}
Copy link
Copy Markdown

@hannesj hannesj Apr 24, 2026

Choose a reason for hiding this comment

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

What about using new Intl.NumberFormat(locale).format(num)?

@Aketzu
Copy link
Copy Markdown
Contributor

Aketzu commented Apr 24, 2026

Some early smokes... Found and fixed issues with Claude Opus in feat/tickets-v2-vat-fixes

  • New product form doesn't show VAT percentages
  • Saving new product doesn't work because vatPercentage is not snake_cased
  • Paytrail requires vatPercentage to not be a string
  • Order complete shows "NaN% VAT" (snake_camel mixup)

Todo:

  • Email shows "VAT 1E+1%"
  • Receipt must show seller's name, contact info, VAT number, order date

Aketzu and others added 5 commits April 25, 2026 22:43
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]>
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.

3 participants