From a08a3f816d95503c9aa15baf780b94697b386bd1 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Fri, 24 Apr 2026 17:30:33 +0300 Subject: [PATCH 01/22] feat(tickets_v2): add per-product VAT rate and receipt VAT breakdown 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 --- .../[eventSlug]/products/[productId]/page.tsx | 20 +++++++++++++ .../google-material-symbols/README.md | 1 - .../src/components/tickets/ProductCard.tsx | 3 ++ .../src/components/tickets/ProductsTable.tsx | 30 +++++++++++++++++++ kompassi-v2-frontend/src/services/tickets.ts | 2 ++ kompassi-v2-frontend/src/translations/en.tsx | 7 +++++ kompassi-v2-frontend/src/translations/fi.tsx | 7 +++++ kompassi-v2-frontend/src/translations/sv.tsx | 7 +++++ .../graphql/mutations/create_product.py | 1 + .../graphql/mutations/update_product.py | 1 + kompassi/tickets_v2/graphql/product_full.py | 1 + .../tickets_v2/graphql/product_limited.py | 1 + .../migrations/0008_product_vat_percentage.py | 24 +++++++++++++++ kompassi/tickets_v2/models/order.py | 20 ++++++++++++- kompassi/tickets_v2/models/product.py | 7 +++++ kompassi/tickets_v2/models/receipt.py | 1 + .../optimized_server/models/order.py | 19 ++++++++++-- .../optimized_server/models/product.py | 7 ++++- .../optimized_server/models/sql/get_order.sql | 4 ++- .../models/sql/get_order_with_customer.sql | 4 ++- .../models/sql/list_products.sql | 3 +- .../templates/tickets_v2_cancel_en.eml | 4 ++- .../templates/tickets_v2_cancel_fi.eml | 4 ++- .../templates/tickets_v2_receipt_en.eml | 4 ++- .../templates/tickets_v2_receipt_fi.eml | 4 ++- 25 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 kompassi/tickets_v2/migrations/0008_product_vat_percentage.py diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx index b174d01f9..6bd935950 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx @@ -29,6 +29,7 @@ graphql(` title description price + vatPercentage eticketsPerProduct maxPerOrder } @@ -41,6 +42,7 @@ graphql(` title description price + vatPercentage eticketsPerProduct maxPerOrder availableFrom @@ -183,6 +185,17 @@ export default async function AdminProductDetailPage(props: Props) { decimalPlaces: 2, ...t.clientAttributes.unitPrice, }, + { + slug: "vatPercentage", + type: "SingleSelect", + choices: [ + { slug: "0.00", title: "0%" }, + { slug: "10.00", title: "10%" }, + { slug: "13.50", title: "13.5%" }, + { slug: "25.50", title: "25.5%" }, + ], + ...t.clientAttributes.vatPercentage, + }, { slug: "eticketsPerProduct", type: "NumberField", @@ -294,6 +307,13 @@ export default async function AdminProductDetailPage(props: Props) { getCellContents: (product) => formatMoney(product.price), className: "col-1 align-middle", }, + { + slug: "vatPercentage", + title: t.clientAttributes.vatPercentage.title, + getCellContents: (product) => + t.clientAttributes.vatIncluded(product.vatPercentage), + className: "col-1 align-middle", + }, ]; return ( diff --git a/kompassi-v2-frontend/src/components/google-material-symbols/README.md b/kompassi-v2-frontend/src/components/google-material-symbols/README.md index 9f3440b52..cfa4266db 100644 --- a/kompassi-v2-frontend/src/components/google-material-symbols/README.md +++ b/kompassi-v2-frontend/src/components/google-material-symbols/README.md @@ -16,7 +16,6 @@ Current approach is as follows: 3. Download the SVG 4. Using eg. OpenInNewTab.tsx as a template, make the SVG into a React component - - You may need to tweak the vertical translate to make the icon align with text. The `.material-symbol` class lives in `globals.scss`. diff --git a/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx b/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx index d7d4d0e26..b5be5afcb 100644 --- a/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx +++ b/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx @@ -25,6 +25,9 @@ export default function ProductCard({ product, messages: t, children }: Props) {
{formatMoney(product.price)} +
+ {t.clientAttributes.vatIncluded(product.vatPercentage)} +
diff --git a/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx b/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx index 6480f1bea..05d2007aa 100644 --- a/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx +++ b/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx @@ -6,6 +6,7 @@ interface Product { title: string; quantity: number; price: string; + vatPercentage: string; } interface Order { @@ -20,6 +21,24 @@ interface Props { compact?: boolean; } +function computeVatBreakdown( + products: Product[], +): { rate: string; vat: string }[] { + const totals = new Map(); + for (const p of products) { + const gross = parseFloat(p.price) * p.quantity; + const prev = totals.get(p.vatPercentage) ?? 0; + totals.set(p.vatPercentage, prev + gross); + } + return Array.from(totals.entries()) + .sort(([a], [b]) => parseFloat(a) - parseFloat(b)) + .map(([rate, gross]) => { + const r = parseFloat(rate); + const vat = (gross * r) / (100 + r); + return { rate, vat: vat.toFixed(2) }; + }); +} + export default function ProductsTable({ order, messages: t, @@ -63,6 +82,8 @@ export default function ProductsTable({ className = "table table-striped " + (className ?? "mb-5"); + const vatBreakdown = computeVatBreakdown(order.products); + return ( @@ -75,6 +96,15 @@ export default function ProductsTable({ {formatMoney(order.totalPrice)} + {vatBreakdown.map(({ rate, vat }) => ( + + + {t.Product.clientAttributes.vatIncluded(rate)} + + + {formatMoney(vat)} + + ))} ); diff --git a/kompassi-v2-frontend/src/services/tickets.ts b/kompassi-v2-frontend/src/services/tickets.ts index 5019557da..fa475d0e6 100644 --- a/kompassi-v2-frontend/src/services/tickets.ts +++ b/kompassi-v2-frontend/src/services/tickets.ts @@ -30,6 +30,7 @@ export interface Product { title: string; description: string; price: string; + vatPercentage: string; maxPerOrder: number; available?: boolean; } @@ -169,6 +170,7 @@ export interface Order { title: string; price: string; quantity: number; + vatPercentage: string; }[]; } diff --git a/kompassi-v2-frontend/src/translations/en.tsx b/kompassi-v2-frontend/src/translations/en.tsx index dbcfd5e94..0bd019d7a 100644 --- a/kompassi-v2-frontend/src/translations/en.tsx +++ b/kompassi-v2-frontend/src/translations/en.tsx @@ -531,6 +531,13 @@ const translations = { selectedQuotas: "Selected quotas", soldOut: "Sold out", isAvailable: "Availability schedule", + vatPercentage: { + title: "VAT rate", + helpText: + "The VAT rate that applies to this product. Prices are VAT-inclusive.", + }, + vatIncluded: (rate: string) => `incl. ${rate}% VAT`, + vatBreakdown: "VAT breakdown", dragToReorder: "Drag to reorder", newProductQuota: { title: "Quota", diff --git a/kompassi-v2-frontend/src/translations/fi.tsx b/kompassi-v2-frontend/src/translations/fi.tsx index 553eb77f4..683c86bec 100644 --- a/kompassi-v2-frontend/src/translations/fi.tsx +++ b/kompassi-v2-frontend/src/translations/fi.tsx @@ -526,6 +526,13 @@ const translations: Translations = { selectedQuotas: "Valitut kiintiöt", soldOut: "Loppuunmyyty", isAvailable: "Saatavuusaika", + vatPercentage: { + title: "ALV-prosentti", + helpText: + "Tähän tuotteeseen sovellettava arvonlisäveroprosentti. Hinnat sisältävät ALV:n.", + }, + vatIncluded: (rate: string) => `sis. ${rate}% ALV`, + vatBreakdown: "ALV-erittely", dragToReorder: "Vedä ja pudota järjestääksesi tuotteita", newProductQuota: { title: "Kiintiö", diff --git a/kompassi-v2-frontend/src/translations/sv.tsx b/kompassi-v2-frontend/src/translations/sv.tsx index cdf3c569d..070c64b7c 100644 --- a/kompassi-v2-frontend/src/translations/sv.tsx +++ b/kompassi-v2-frontend/src/translations/sv.tsx @@ -518,6 +518,13 @@ const translations: Translations = { selectedQuotas: "Valda kvoter", soldOut: "Slutsåld", isAvailable: "Tillgänglighetsschema", + vatPercentage: { + title: "Momssats", + helpText: + "Den momssats som gäller för denna produkt. Priserna inkluderar moms.", + }, + vatIncluded: (rate: string) => `inkl. ${rate}% moms`, + vatBreakdown: "Momsspecifikation", dragToReorder: "Dra för att sortera om", newProductQuota: { title: "Kvot", diff --git a/kompassi/tickets_v2/graphql/mutations/create_product.py b/kompassi/tickets_v2/graphql/mutations/create_product.py index 341881136..2725d2fd1 100644 --- a/kompassi/tickets_v2/graphql/mutations/create_product.py +++ b/kompassi/tickets_v2/graphql/mutations/create_product.py @@ -28,6 +28,7 @@ class Meta: "title", "description", "price", + "vat_percentage", ] diff --git a/kompassi/tickets_v2/graphql/mutations/update_product.py b/kompassi/tickets_v2/graphql/mutations/update_product.py index 00d149b6a..7750f800f 100644 --- a/kompassi/tickets_v2/graphql/mutations/update_product.py +++ b/kompassi/tickets_v2/graphql/mutations/update_product.py @@ -31,6 +31,7 @@ class Meta: "title", "description", "price", + "vat_percentage", "max_per_order", "etickets_per_product", "available_from", diff --git a/kompassi/tickets_v2/graphql/product_full.py b/kompassi/tickets_v2/graphql/product_full.py index 6b66b6c7e..a3041730c 100644 --- a/kompassi/tickets_v2/graphql/product_full.py +++ b/kompassi/tickets_v2/graphql/product_full.py @@ -17,6 +17,7 @@ class Meta: "title", "description", "price", + "vat_percentage", "available_from", "available_until", "max_per_order", diff --git a/kompassi/tickets_v2/graphql/product_limited.py b/kompassi/tickets_v2/graphql/product_limited.py index bb68b2877..9b629802a 100644 --- a/kompassi/tickets_v2/graphql/product_limited.py +++ b/kompassi/tickets_v2/graphql/product_limited.py @@ -20,6 +20,7 @@ class Meta: "title", "description", "price", + "vat_percentage", "available_from", "available_until", "max_per_order", diff --git a/kompassi/tickets_v2/migrations/0008_product_vat_percentage.py b/kompassi/tickets_v2/migrations/0008_product_vat_percentage.py new file mode 100644 index 000000000..952a13198 --- /dev/null +++ b/kompassi/tickets_v2/migrations/0008_product_vat_percentage.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.3 on 2026-04-24 14:16 + +from decimal import Decimal + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tickets_v2", "0007_no_receipt_on_cancel"), + ] + + operations = [ + migrations.AddField( + model_name="product", + name="vat_percentage", + field=models.DecimalField( + decimal_places=2, + default=Decimal("0"), + help_text="VAT percentage applied to this product. Prices are inclusive of VAT.", + max_digits=4, + ), + ), + ] diff --git a/kompassi/tickets_v2/models/order.py b/kompassi/tickets_v2/models/order.py index 135edde83..c5859ffaa 100644 --- a/kompassi/tickets_v2/models/order.py +++ b/kompassi/tickets_v2/models/order.py @@ -25,7 +25,7 @@ from kompassi.graphql_api.language import SUPPORTED_LANGUAGES from ..optimized_server.models.enums import PaymentProvider, PaymentStampType, PaymentStatus, RefundType -from ..optimized_server.models.order import OrderProduct +from ..optimized_server.models.order import OrderProduct, VatBreakdownLine from ..optimized_server.utils.formatting import format_order_number from ..optimized_server.utils.uuid7 import uuid7 from ..utils.event_partitions import EventPartitionsMixin @@ -70,6 +70,7 @@ def _get_product(cls, event_id: int | str, product_id: int | str) -> Product: "title", "description", "price", + "vat_percentage", "etickets_per_product", ) } @@ -83,11 +84,27 @@ def products(self) -> list[OrderProduct]: title=product.title, price=product.price, quantity=quantity, + vat_percentage=product.vat_percentage, ) for product_id, quantity in self.product_data.items() # type: ignore if quantity > 0 and (product := self._get_product(self.event_id, product_id)) # type: ignore ] + @cached_property + def vat_breakdown(self) -> list[VatBreakdownLine]: + from collections import defaultdict + + totals: dict[Decimal, Decimal] = defaultdict(Decimal) + for op in self.products: + totals[op.vat_percentage] += op.price * op.quantity + result = [] + for rate in sorted(totals): + gross = totals[rate] + vat = (gross * rate / (100 + rate)).quantize(Decimal("0.01")) + net = gross - vat + result.append(VatBreakdownLine(rate=rate, gross=gross, vat=vat, net=net)) + return result + @cached_property def etickets(self) -> list[Product]: """ @@ -204,6 +221,7 @@ def products(self) -> list[OrderProduct]: title=product.title, price=product.price, quantity=quantity, + vat_percentage=product.vat_percentage, ) for (product_id, quantity) in self.product_data.items() if (product := products_by_id[int(product_id)]) and quantity > 0 diff --git a/kompassi/tickets_v2/models/product.py b/kompassi/tickets_v2/models/product.py index 6a1a30a98..6a7447f56 100644 --- a/kompassi/tickets_v2/models/product.py +++ b/kompassi/tickets_v2/models/product.py @@ -1,5 +1,6 @@ from __future__ import annotations +from decimal import Decimal from pathlib import Path from typing import ClassVar @@ -88,6 +89,12 @@ class Product(models.Model): created_at = models.DateTimeField(auto_now_add=True) price = models.DecimalField(max_digits=10, decimal_places=2) + vat_percentage = models.DecimalField( + max_digits=4, + decimal_places=2, + default=Decimal("0"), + help_text="VAT percentage applied to this product. Prices are inclusive of VAT.", + ) title = models.TextField() description = models.TextField() diff --git a/kompassi/tickets_v2/models/receipt.py b/kompassi/tickets_v2/models/receipt.py index c9dc890ce..c0bcd7c4d 100644 --- a/kompassi/tickets_v2/models/receipt.py +++ b/kompassi/tickets_v2/models/receipt.py @@ -315,6 +315,7 @@ def body(self) -> str: order_number=self.order_number, products=self.products, total_price=self.total_price, + vat_breakdown=self.vat_breakdown, have_etickets=self.have_etickets, is_refund=self.receipt_type == ReceiptType.REFUNDED, first_name=self.first_name, diff --git a/kompassi/tickets_v2/optimized_server/models/order.py b/kompassi/tickets_v2/optimized_server/models/order.py index e0e265eac..57398108e 100644 --- a/kompassi/tickets_v2/optimized_server/models/order.py +++ b/kompassi/tickets_v2/optimized_server/models/order.py @@ -125,6 +125,14 @@ class OrderProduct(pydantic.BaseModel): title: str price: Decimal quantity: int + vat_percentage: Decimal + + +class VatBreakdownLine(pydantic.BaseModel): + rate: Decimal + gross: Decimal + vat: Decimal + net: Decimal class Order(pydantic.BaseModel, populate_by_name=True): @@ -176,8 +184,10 @@ async def get(cls, db: AsyncConnection, event_id: int, order_id: UUID) -> Order order_number = 0 language_ = "" - async for total_, order_number_, language_, title, price, quantity, status_ in cursor: - order_products.append(OrderProduct(title=title, price=price, quantity=quantity)) + async for total_, order_number_, language_, title, price, quantity, vat_percentage, status_ in cursor: + order_products.append( + OrderProduct(title=title, price=price, quantity=quantity, vat_percentage=vat_percentage) + ) total_price, order_number, language, status = total_, order_number_, language_, status_ if not order_products: @@ -228,13 +238,16 @@ async def get(cls, db: AsyncConnection, event_id: int, order_id: UUID) -> OrderW title, price, quantity, + vat_percentage, status_, first_name_, last_name_, email_, phone_, ) in cursor: - order_products.append(OrderProduct(title=title, price=price, quantity=quantity)) + order_products.append( + OrderProduct(title=title, price=price, quantity=quantity, vat_percentage=vat_percentage) + ) total_price, order_number, language, status = total_, order_number_, language_, status_ first_name, last_name, email, phone = first_name_, last_name_, email_, phone_ diff --git a/kompassi/tickets_v2/optimized_server/models/product.py b/kompassi/tickets_v2/optimized_server/models/product.py index b71dafaba..677befa6f 100644 --- a/kompassi/tickets_v2/optimized_server/models/product.py +++ b/kompassi/tickets_v2/optimized_server/models/product.py @@ -19,6 +19,10 @@ class Product(pydantic.BaseModel, populate_by_name=True): validation_alias="maxPerOrder", serialization_alias="maxPerOrder", ) + vat_percentage: Decimal = pydantic.Field( + validation_alias="vatPercentage", + serialization_alias="vatPercentage", + ) available: bool | None list_query: ClassVar[bytes] = (Path(__file__).parent / "sql" / "list_products.sql").read_bytes() @@ -30,7 +34,7 @@ async def get_products(cls, aconn: AsyncConnection, event_id: int) -> list[Self] products = [] async for row in cursor: - id, title, description, price, max_per_order, available = row + id, title, description, price, max_per_order, vat_percentage, available = row products.append( cls( id=id, @@ -38,6 +42,7 @@ async def get_products(cls, aconn: AsyncConnection, event_id: int) -> list[Self] description=description, price=price, max_per_order=max_per_order, + vat_percentage=vat_percentage, available=available, ) ) diff --git a/kompassi/tickets_v2/optimized_server/models/sql/get_order.sql b/kompassi/tickets_v2/optimized_server/models/sql/get_order.sql index ce9724478..8f79bc183 100644 --- a/kompassi/tickets_v2/optimized_server/models/sql/get_order.sql +++ b/kompassi/tickets_v2/optimized_server/models/sql/get_order.sql @@ -5,6 +5,7 @@ select p2.title, p2.price, p2.quantity, + p2.vat_percentage, o.cached_status as status from tickets_v2_order o @@ -12,7 +13,8 @@ from select p.title, p.price, - cast(pd.value as int) as quantity + cast(pd.value as int) as quantity, + p.vat_percentage from tickets_v2_product p join jsonb_each(o.product_data) pd on (cast(pd.key as int) = p.id) diff --git a/kompassi/tickets_v2/optimized_server/models/sql/get_order_with_customer.sql b/kompassi/tickets_v2/optimized_server/models/sql/get_order_with_customer.sql index 9efac7f1c..516536550 100644 --- a/kompassi/tickets_v2/optimized_server/models/sql/get_order_with_customer.sql +++ b/kompassi/tickets_v2/optimized_server/models/sql/get_order_with_customer.sql @@ -5,6 +5,7 @@ select p2.title, p2.price, p2.quantity, + p2.vat_percentage, o.cached_status as status, o.last_name, o.first_name, @@ -16,7 +17,8 @@ from select p.title, p.price, - cast(pd.value as int) as quantity + cast(pd.value as int) as quantity, + p.vat_percentage from tickets_v2_product p join jsonb_each(o.product_data) pd on (cast(pd.key as int) = p.id) diff --git a/kompassi/tickets_v2/optimized_server/models/sql/list_products.sql b/kompassi/tickets_v2/optimized_server/models/sql/list_products.sql index da1dc8e8e..91e687178 100644 --- a/kompassi/tickets_v2/optimized_server/models/sql/list_products.sql +++ b/kompassi/tickets_v2/optimized_server/models/sql/list_products.sql @@ -22,6 +22,7 @@ select p.description, p.price, p.max_per_order, + p.vat_percentage, bool_and(qa.available) as available from tickets_v2_product p @@ -32,5 +33,5 @@ where and p.superseded_by_id is null and p.available_from <= now() and (p.available_until is null or p.available_until > now()) -group by 1, 2, 3, 4, 5 +group by 1, 2, 3, 4, 5, 6 order by p.ordering; diff --git a/kompassi/tickets_v2/templates/tickets_v2_cancel_en.eml b/kompassi/tickets_v2/templates/tickets_v2_cancel_en.eml index 2a131c3cc..af62f13e2 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_cancel_en.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_cancel_en.eml @@ -8,7 +8,9 @@ Contents of the order: {% for op in products %}{{ op.quantity }} pcs {{ op.title }} {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} -Total: {{ total_price|format_money }} (VAT 0%) +Total: {{ total_price|format_money }} (incl. VAT) +{% for vb in vat_breakdown %}VAT {{ vb.rate }}%: {{ vb.vat|format_money }} (of {{ vb.gross|format_money }}) +{% endfor %} If you suspect this is in error, please contact us by replying to this message.{% if is_refund %} diff --git a/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml b/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml index d3e4e3329..50ed89001 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml @@ -7,7 +7,9 @@ Tilaus sisälsi seuraavat tuotteet: {% for op in products %}{{ op.quantity }} kpl {{ op.title }} {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} -Yhteensä: {{ total_price|format_money }} (ALV 0%) +Yhteensä: {{ total_price|format_money }} (sis. ALV) +{% for vb in vat_breakdown %}ALV {{ vb.rate }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) +{% endfor %} Jos epäilet, että tilauksesi on peruuntunut virheellisesti, ota yhteyttä asiakaspalveluun vastaamalla tähän viestiin.{% if is_refund %} diff --git a/kompassi/tickets_v2/templates/tickets_v2_receipt_en.eml b/kompassi/tickets_v2/templates/tickets_v2_receipt_en.eml index afa12c139..f835e0417 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_receipt_en.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_receipt_en.eml @@ -8,7 +8,9 @@ We confirm the following products have been paid for: {% for op in products %}{{ op.quantity }} pcs {{ op.title }} {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} -Total: {{ total_price|format_money }} (VAT 0%){% if have_etickets %} +Total: {{ total_price|format_money }} (incl. VAT) +{% for vb in vat_breakdown %}VAT {{ vb.rate }}%: {{ vb.vat|format_money }} (of {{ vb.gross|format_money }}) +{% endfor %}{% if have_etickets %} Please find attached your electronic tickets. The electronic ticket will be exchanged for a wristband at the ticket exchange when you arrive at the event. diff --git a/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml b/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml index 9dbfdd8ef..7764adf89 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml @@ -8,7 +8,9 @@ Vahvistamme maksetuiksi seuraavat lipputuotteet: {% for op in products %}{{ op.quantity }} kpl {{ op.title }} {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} -Yhteensä: {{ total_price|format_money }} (ALV 0%){% if have_etickets %} +Yhteensä: {{ total_price|format_money }} (sis. ALV) +{% for vb in vat_breakdown %}ALV {{ vb.rate }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) +{% endfor %}{% if have_etickets %} Löydät tilaamasi sähköiset liput liitetiedostosta. Sähköinen lippu vaihdetaan rannekkeeseen lipunvaihtopisteessä saapuessasi tapahtumaan. Voit From 4e13651bcdd3605c666b56e2993e09a5366169ea Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Fri, 24 Apr 2026 17:40:16 +0300 Subject: [PATCH 02/22] feat(tickets_v2): send items in Paytrail CreatePaymentRequest 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 --- kompassi/tickets_v2/optimized_server/app.py | 5 ++- .../optimized_server/providers/null.py | 3 +- .../optimized_server/providers/paytrail.py | 37 ++++++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/kompassi/tickets_v2/optimized_server/app.py b/kompassi/tickets_v2/optimized_server/app.py index b997880f3..36199a31c 100644 --- a/kompassi/tickets_v2/optimized_server/app.py +++ b/kompassi/tickets_v2/optimized_server/app.py @@ -104,7 +104,10 @@ async def create_order( try: async with db.transaction(): result = await order.save(db, event.id) - request, request_stamp = provider.prepare_for_new_order(order, result) + fetched_order = await Order.get(db, event.id, result.order_id) + request, request_stamp = provider.prepare_for_new_order( + order, result, fetched_order.products if fetched_order else [] + ) await request_stamp.save(db) except NotEnoughTickets as e: raise HTTPException(409, "NOT_ENOUGH_TICKETS") from e diff --git a/kompassi/tickets_v2/optimized_server/providers/null.py b/kompassi/tickets_v2/optimized_server/providers/null.py index b32b00ad7..b0f720eb2 100644 --- a/kompassi/tickets_v2/optimized_server/providers/null.py +++ b/kompassi/tickets_v2/optimized_server/providers/null.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: from ..models.event import Event - from ..models.order import CreateOrderRequest, CreateOrderResult, OrderWithCustomer + from ..models.order import CreateOrderRequest, CreateOrderResult, OrderProduct, OrderWithCustomer @dataclass @@ -22,6 +22,7 @@ def prepare_for_new_order( self, _order: CreateOrderRequest, result: CreateOrderResult, + _order_products: list[OrderProduct], ) -> tuple[None, PaymentStamp]: if result.total_price != 0: raise ProviderCannot("Null provider cannot handle non-zero price orders") diff --git a/kompassi/tickets_v2/optimized_server/providers/paytrail.py b/kompassi/tickets_v2/optimized_server/providers/paytrail.py index a2d39e90b..5f0404e56 100644 --- a/kompassi/tickets_v2/optimized_server/providers/paytrail.py +++ b/kompassi/tickets_v2/optimized_server/providers/paytrail.py @@ -5,6 +5,7 @@ from collections.abc import Mapping from dataclasses import dataclass from datetime import UTC, datetime +from decimal import Decimal from enum import Enum from typing import Any, ClassVar, Literal, Self from uuid import UUID, uuid4 @@ -19,7 +20,7 @@ from ..models.customer import Customer from ..models.enums import PaymentProvider, PaymentStampType, PaymentStatus from ..models.event import Event -from ..models.order import CreateOrderRequest, CreateOrderResult, Order, OrderWithCustomer +from ..models.order import CreateOrderRequest, CreateOrderResult, Order, OrderProduct, OrderWithCustomer from ..models.payment_stamp import PaymentStamp from ..utils.paytrail_hmac import calculate_hmac @@ -79,6 +80,34 @@ def for_refund_callback(cls, event_slug: str, order_id: UUID) -> CallbackUrls: ) +class PaytrailItem(pydantic.BaseModel): + """ + A line item in a Paytrail payment request. + Including items enables per-rate VAT breakdown in Paytrail reports. + + Docs: https://docs.paytrail.com/#/?id=item + """ + + unit_price: int = pydantic.Field(serialization_alias="unitPrice") + units: int + vat_percentage: Decimal = pydantic.Field(serialization_alias="vatPercentage") + product_code: str = pydantic.Field(serialization_alias="productCode") + description: str + + @classmethod + def from_order_products(cls, order_products: list[OrderProduct]) -> list[PaytrailItem]: + return [ + cls( + unit_price=int(op.price * 100), + units=op.quantity, + vat_percentage=op.vat_percentage, + product_code=str(i + 1), + description=op.title, + ) + for i, op in enumerate(order_products) + ] + + class CreatePaymentResponse(pydantic.BaseModel): transaction_id: str = pydantic.Field( validation_alias="transactionId", @@ -105,6 +134,7 @@ class CreatePaymentRequest(pydantic.BaseModel): currency: Literal["EUR"] = "EUR" language: str customer: Customer + items: list[PaytrailItem] redirect_urls: CallbackUrls = pydantic.Field(serialization_alias="redirectUrls") callback_urls: CallbackUrls | None = pydantic.Field(serialization_alias="callbackUrls") @@ -124,12 +154,14 @@ def from_create_order_request( event: Event, request: CreateOrderRequest, result: CreateOrderResult, + order_products: list[OrderProduct], ) -> CreatePaymentRequest: return cls( reference=result.reference, amount_cents=int(result.total_price * 100), language=request.language, customer=request.customer, + items=PaytrailItem.from_order_products(order_products), redirect_urls=CallbackUrls.for_payment_redirect(event.slug, result.order_id), callback_urls=None if DEBUG else CallbackUrls.for_payment_callback(event.slug, result.order_id), ) @@ -145,6 +177,7 @@ def from_order( amount_cents=int(order.total_price * 100), language=order.language, customer=order.customer, + items=PaytrailItem.from_order_products(order.products), redirect_urls=CallbackUrls.for_payment_redirect(event.slug, order.id), callback_urls=None if DEBUG else CallbackUrls.for_payment_callback(event.slug, order.id), ) @@ -397,6 +430,7 @@ def prepare_for_new_order( self, order: CreateOrderRequest, result: CreateOrderResult, + order_products: list[OrderProduct], ) -> tuple[PreparedCreatePaymentRequest | None, PaymentStamp]: if result.total_price == 0: return None, PaymentStamp.for_zero_price_order( @@ -411,6 +445,7 @@ def prepare_for_new_order( self.event, order, result, + order_products, ).prepare( self.event, result.order_id, From 3beda3c96fef89034ba31e8d2ab5adc8b1037d12 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Fri, 24 Apr 2026 17:49:41 +0300 Subject: [PATCH 03/22] fix(tickets_v2): review fixes for VAT branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- kompassi-v2-frontend/src/translations/en.tsx | 2 +- kompassi-v2-frontend/src/translations/fi.tsx | 2 +- kompassi-v2-frontend/src/translations/sv.tsx | 2 +- kompassi/tickets_v2/optimized_server/app.py | 8 ++++---- .../optimized_server/providers/paytrail.py | 7 +++++++ .../optimized_server/utils/formatting.py | 16 ++++++++++++++++ .../templates/tickets_v2_cancel_en.eml | 2 +- .../templates/tickets_v2_cancel_fi.eml | 2 +- .../templates/tickets_v2_receipt_en.eml | 2 +- .../templates/tickets_v2_receipt_fi.eml | 2 +- .../tickets_v2/templatetags/tickets_v2_tags.py | 3 ++- 11 files changed, 36 insertions(+), 12 deletions(-) diff --git a/kompassi-v2-frontend/src/translations/en.tsx b/kompassi-v2-frontend/src/translations/en.tsx index 0bd019d7a..f4dbdfc68 100644 --- a/kompassi-v2-frontend/src/translations/en.tsx +++ b/kompassi-v2-frontend/src/translations/en.tsx @@ -536,7 +536,7 @@ const translations = { helpText: "The VAT rate that applies to this product. Prices are VAT-inclusive.", }, - vatIncluded: (rate: string) => `incl. ${rate}% VAT`, + vatIncluded: (rate: string) => `incl. ${parseFloat(rate)}% VAT`, vatBreakdown: "VAT breakdown", dragToReorder: "Drag to reorder", newProductQuota: { diff --git a/kompassi-v2-frontend/src/translations/fi.tsx b/kompassi-v2-frontend/src/translations/fi.tsx index 683c86bec..d44063e0e 100644 --- a/kompassi-v2-frontend/src/translations/fi.tsx +++ b/kompassi-v2-frontend/src/translations/fi.tsx @@ -531,7 +531,7 @@ const translations: Translations = { helpText: "Tähän tuotteeseen sovellettava arvonlisäveroprosentti. Hinnat sisältävät ALV:n.", }, - vatIncluded: (rate: string) => `sis. ${rate}% ALV`, + vatIncluded: (rate: string) => `sis. ${parseFloat(rate)}% ALV`, vatBreakdown: "ALV-erittely", dragToReorder: "Vedä ja pudota järjestääksesi tuotteita", newProductQuota: { diff --git a/kompassi-v2-frontend/src/translations/sv.tsx b/kompassi-v2-frontend/src/translations/sv.tsx index 070c64b7c..bfd25feaa 100644 --- a/kompassi-v2-frontend/src/translations/sv.tsx +++ b/kompassi-v2-frontend/src/translations/sv.tsx @@ -523,7 +523,7 @@ const translations: Translations = { helpText: "Den momssats som gäller för denna produkt. Priserna inkluderar moms.", }, - vatIncluded: (rate: string) => `inkl. ${rate}% moms`, + vatIncluded: (rate: string) => `inkl. ${parseFloat(rate)}% moms`, vatBreakdown: "Momsspecifikation", dragToReorder: "Dra för att sortera om", newProductQuota: { diff --git a/kompassi/tickets_v2/optimized_server/app.py b/kompassi/tickets_v2/optimized_server/app.py index 36199a31c..be4968bbc 100644 --- a/kompassi/tickets_v2/optimized_server/app.py +++ b/kompassi/tickets_v2/optimized_server/app.py @@ -10,7 +10,7 @@ from kompassi.graphql_api.language import DEFAULT_LANGUAGE, getattr_message_in_language from .db import DB, lifespan -from .excs import InvalidProducts, NotEnoughTickets, ProviderCannot +from .excs import InvalidProducts, NotEnoughTickets, ProviderCannot, UnsaneSituation from .models.enums import PaymentStampType from .models.event import Event from .models.order import CreateOrderRequest, Order, OrderWithCustomer @@ -105,9 +105,9 @@ async def create_order( async with db.transaction(): result = await order.save(db, event.id) fetched_order = await Order.get(db, event.id, result.order_id) - request, request_stamp = provider.prepare_for_new_order( - order, result, fetched_order.products if fetched_order else [] - ) + if fetched_order is None: + raise UnsaneSituation("Order not found after creation") + request, request_stamp = provider.prepare_for_new_order(order, result, fetched_order.products) await request_stamp.save(db) except NotEnoughTickets as e: raise HTTPException(409, "NOT_ENOUGH_TICKETS") from e diff --git a/kompassi/tickets_v2/optimized_server/providers/paytrail.py b/kompassi/tickets_v2/optimized_server/providers/paytrail.py index 5f0404e56..e917258f2 100644 --- a/kompassi/tickets_v2/optimized_server/providers/paytrail.py +++ b/kompassi/tickets_v2/optimized_server/providers/paytrail.py @@ -138,6 +138,13 @@ class CreatePaymentRequest(pydantic.BaseModel): redirect_urls: CallbackUrls = pydantic.Field(serialization_alias="redirectUrls") callback_urls: CallbackUrls | None = pydantic.Field(serialization_alias="callbackUrls") + @pydantic.model_validator(mode="after") + def validate_items_sum(self) -> Self: + items_sum = sum(item.unit_price * item.units for item in self.items) + if items_sum != self.amount_cents: + raise ValueError(f"Items sum {items_sum} does not match amount {self.amount_cents}") + return self + @pydantic.field_validator("language", mode="before") @staticmethod def validate_language(value: str): diff --git a/kompassi/tickets_v2/optimized_server/utils/formatting.py b/kompassi/tickets_v2/optimized_server/utils/formatting.py index c7caded45..da4292d8b 100644 --- a/kompassi/tickets_v2/optimized_server/utils/formatting.py +++ b/kompassi/tickets_v2/optimized_server/utils/formatting.py @@ -17,6 +17,22 @@ def format_money(euros: Decimal) -> str: return f"{euros:0.2f} €".replace(".", ",") +def format_vat_rate(rate: Decimal) -> str: + """ + Format a VAT rate for display, stripping unnecessary trailing zeros. + + >>> format_vat_rate(Decimal('25.50')) + '25.5' + >>> format_vat_rate(Decimal('10.00')) + '10' + >>> format_vat_rate(Decimal('0')) + '0' + >>> format_vat_rate(Decimal('13.50')) + '13.5' + """ + return str(rate.normalize()) + + def format_order_number(order_number: int): """ >>> format_order_number(423125) diff --git a/kompassi/tickets_v2/templates/tickets_v2_cancel_en.eml b/kompassi/tickets_v2/templates/tickets_v2_cancel_en.eml index af62f13e2..cc4f361e5 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_cancel_en.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_cancel_en.eml @@ -9,7 +9,7 @@ Contents of the order: {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} Total: {{ total_price|format_money }} (incl. VAT) -{% for vb in vat_breakdown %}VAT {{ vb.rate }}%: {{ vb.vat|format_money }} (of {{ vb.gross|format_money }}) +{% for vb in vat_breakdown %}VAT {{ vb.rate|format_vat_rate }}%: {{ vb.vat|format_money }} (of {{ vb.gross|format_money }}) {% endfor %} If you suspect this is in error, please contact us by replying to this message.{% if is_refund %} diff --git a/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml b/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml index 50ed89001..7234d91dd 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml @@ -8,7 +8,7 @@ Tilaus sisälsi seuraavat tuotteet: {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} Yhteensä: {{ total_price|format_money }} (sis. ALV) -{% for vb in vat_breakdown %}ALV {{ vb.rate }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) +{% for vb in vat_breakdown %}ALV {{ vb.rate|format_vat_rate }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) {% endfor %} Jos epäilet, että tilauksesi on peruuntunut virheellisesti, diff --git a/kompassi/tickets_v2/templates/tickets_v2_receipt_en.eml b/kompassi/tickets_v2/templates/tickets_v2_receipt_en.eml index f835e0417..886ea522d 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_receipt_en.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_receipt_en.eml @@ -9,7 +9,7 @@ We confirm the following products have been paid for: {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} Total: {{ total_price|format_money }} (incl. VAT) -{% for vb in vat_breakdown %}VAT {{ vb.rate }}%: {{ vb.vat|format_money }} (of {{ vb.gross|format_money }}) +{% for vb in vat_breakdown %}VAT {{ vb.rate|format_vat_rate }}%: {{ vb.vat|format_money }} (of {{ vb.gross|format_money }}) {% endfor %}{% if have_etickets %} Please find attached your electronic tickets. The electronic ticket will be diff --git a/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml b/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml index 7764adf89..a9e6f0dae 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml @@ -9,7 +9,7 @@ Vahvistamme maksetuiksi seuraavat lipputuotteet: {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} Yhteensä: {{ total_price|format_money }} (sis. ALV) -{% for vb in vat_breakdown %}ALV {{ vb.rate }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) +{% for vb in vat_breakdown %}ALV {{ vb.rate|format_vat_rate }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) {% endfor %}{% if have_etickets %} Löydät tilaamasi sähköiset liput liitetiedostosta. Sähköinen lippu diff --git a/kompassi/tickets_v2/templatetags/tickets_v2_tags.py b/kompassi/tickets_v2/templatetags/tickets_v2_tags.py index 03dd9fe76..8c88b8352 100644 --- a/kompassi/tickets_v2/templatetags/tickets_v2_tags.py +++ b/kompassi/tickets_v2/templatetags/tickets_v2_tags.py @@ -1,7 +1,8 @@ from django.template import Library -from ..optimized_server.utils.formatting import format_money, format_order_number +from ..optimized_server.utils.formatting import format_money, format_order_number, format_vat_rate register = Library() register.filter(format_money) register.filter(format_order_number) +register.filter(format_vat_rate) From b946f209cbb237754d603436346ced89f9734ab9 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Fri, 24 Apr 2026 17:55:46 +0300 Subject: [PATCH 04/22] fix(tickets_v2): locale-aware VAT rate formatting 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 --- .../[eventSlug]/orders-admin/[orderId]/page.tsx | 2 ++ .../[eventSlug]/orders/[orderId]/page.tsx | 2 +- .../[eventSlug]/products/[productId]/page.tsx | 5 ++++- .../app/[locale]/[eventSlug]/tickets/page.tsx | 1 + .../orders/[eventSlug]/[orderId]/page.tsx | 7 ++++++- .../src/components/tickets/ProductCard.tsx | 13 +++++++++++-- .../src/components/tickets/ProductsTable.tsx | 7 ++++++- .../src/helpers/formatVatRate.ts | 8 ++++++++ kompassi-v2-frontend/src/translations/en.tsx | 3 +-- kompassi-v2-frontend/src/translations/fi.tsx | 2 +- kompassi-v2-frontend/src/translations/sv.tsx | 2 +- .../optimized_server/utils/formatting.py | 16 ++++++++++++---- .../templates/tickets_v2_cancel_fi.eml | 2 +- .../templates/tickets_v2_receipt_fi.eml | 2 +- 14 files changed, 56 insertions(+), 16 deletions(-) create mode 100644 kompassi-v2-frontend/src/helpers/formatVatRate.ts diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/orders-admin/[orderId]/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/orders-admin/[orderId]/page.tsx index 57820e82c..72a775eed 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/orders-admin/[orderId]/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/orders-admin/[orderId]/page.tsx @@ -94,6 +94,7 @@ const query = graphql(` title quantity price + vatPercentage } paymentStamps { ...AdminOrderPaymentStamp @@ -542,6 +543,7 @@ export default async function AdminOrderPage(props: Props) { - + {showPayButton && (
diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx index 6bd935950..6eef8fdf2 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx @@ -20,6 +20,7 @@ import SubmitButton from "@/components/forms/SubmitButton"; import ModalButton from "@/components/ModalButton"; import TicketsAdminView from "@/components/tickets/TicketsAdminView"; import formatMoney from "@/helpers/formatMoney"; +import formatVatRate from "@/helpers/formatVatRate"; import getPageTitle from "@/helpers/getPageTitle"; import { getTranslations } from "@/translations"; @@ -311,7 +312,9 @@ export default async function AdminProductDetailPage(props: Props) { slug: "vatPercentage", title: t.clientAttributes.vatPercentage.title, getCellContents: (product) => - t.clientAttributes.vatIncluded(product.vatPercentage), + t.clientAttributes.vatIncluded( + formatVatRate(product.vatPercentage, locale), + ), className: "col-1 align-middle", }, ]; diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/tickets/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/tickets/page.tsx index c8d633a41..0b2c86ba5 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/tickets/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/tickets/page.tsx @@ -74,6 +74,7 @@ export default async function TicketsPage(props: Props) { ); diff --git a/kompassi-v2-frontend/src/app/[locale]/profile/orders/[eventSlug]/[orderId]/page.tsx b/kompassi-v2-frontend/src/app/[locale]/profile/orders/[eventSlug]/[orderId]/page.tsx index 064b52ada..374437c33 100644 --- a/kompassi-v2-frontend/src/app/[locale]/profile/orders/[eventSlug]/[orderId]/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/profile/orders/[eventSlug]/[orderId]/page.tsx @@ -32,6 +32,7 @@ const query = graphql(` title quantity price + vatPercentage } event { @@ -122,7 +123,11 @@ export default async function ProfileOrderPage(props: Props) { event={order.event} /> - + {order.canPay && (
diff --git a/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx b/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx index b5be5afcb..7bc944dad 100644 --- a/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx +++ b/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx @@ -3,16 +3,23 @@ import Card from "react-bootstrap/Card"; import CardBody from "react-bootstrap/CardBody"; import CardTitle from "react-bootstrap/CardTitle"; import formatMoney from "@/helpers/formatMoney"; +import formatVatRate from "@/helpers/formatVatRate"; import { Product } from "@/services/tickets"; import { Translations } from "@/translations/en"; interface Props { product: Product; + locale: string; messages: Translations["Tickets"]["Product"]; children?: ReactNode; } -export default function ProductCard({ product, messages: t, children }: Props) { +export default function ProductCard({ + product, + locale, + messages: t, + children, +}: Props) { const className = product.available ? "" : "text-muted"; return ( @@ -26,7 +33,9 @@ export default function ProductCard({ product, messages: t, children }: Props) {
{formatMoney(product.price)}
- {t.clientAttributes.vatIncluded(product.vatPercentage)} + {t.clientAttributes.vatIncluded( + formatVatRate(product.vatPercentage, locale), + )}
diff --git a/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx b/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx index 05d2007aa..1fd2ae906 100644 --- a/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx +++ b/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx @@ -1,5 +1,6 @@ import { Column, DataTable } from "../DataTable"; import formatMoney from "@/helpers/formatMoney"; +import formatVatRate from "@/helpers/formatVatRate"; import type { Translations } from "@/translations/en"; interface Product { @@ -16,6 +17,7 @@ interface Order { interface Props { order: Order; + locale: string; messages: Translations["Tickets"]; className?: string; compact?: boolean; @@ -41,6 +43,7 @@ function computeVatBreakdown( export default function ProductsTable({ order, + locale, messages: t, className, compact, @@ -99,7 +102,9 @@ export default function ProductsTable({ {vatBreakdown.map(({ rate, vat }) => ( - {t.Product.clientAttributes.vatIncluded(rate)} + {t.Product.clientAttributes.vatIncluded( + formatVatRate(rate, locale), + )} {formatMoney(vat)} diff --git a/kompassi-v2-frontend/src/helpers/formatVatRate.ts b/kompassi-v2-frontend/src/helpers/formatVatRate.ts new file mode 100644 index 000000000..d38cd57b6 --- /dev/null +++ b/kompassi-v2-frontend/src/helpers/formatVatRate.ts @@ -0,0 +1,8 @@ +export default function formatVatRate(value: string, locale: string = "en") { + const num = parseFloat(value); + const formatted = num.toString(); + if (locale === "fi" || locale === "sv") { + return formatted.replace(".", ","); + } + return formatted; +} diff --git a/kompassi-v2-frontend/src/translations/en.tsx b/kompassi-v2-frontend/src/translations/en.tsx index f4dbdfc68..ac549fa50 100644 --- a/kompassi-v2-frontend/src/translations/en.tsx +++ b/kompassi-v2-frontend/src/translations/en.tsx @@ -1,5 +1,4 @@ import { JSX, ReactNode } from "react"; - const translations = { Common: { ok: "OK", @@ -536,7 +535,7 @@ const translations = { helpText: "The VAT rate that applies to this product. Prices are VAT-inclusive.", }, - vatIncluded: (rate: string) => `incl. ${parseFloat(rate)}% VAT`, + vatIncluded: (rate: string) => `incl. ${rate}% VAT`, vatBreakdown: "VAT breakdown", dragToReorder: "Drag to reorder", newProductQuota: { diff --git a/kompassi-v2-frontend/src/translations/fi.tsx b/kompassi-v2-frontend/src/translations/fi.tsx index d44063e0e..683c86bec 100644 --- a/kompassi-v2-frontend/src/translations/fi.tsx +++ b/kompassi-v2-frontend/src/translations/fi.tsx @@ -531,7 +531,7 @@ const translations: Translations = { helpText: "Tähän tuotteeseen sovellettava arvonlisäveroprosentti. Hinnat sisältävät ALV:n.", }, - vatIncluded: (rate: string) => `sis. ${parseFloat(rate)}% ALV`, + vatIncluded: (rate: string) => `sis. ${rate}% ALV`, vatBreakdown: "ALV-erittely", dragToReorder: "Vedä ja pudota järjestääksesi tuotteita", newProductQuota: { diff --git a/kompassi-v2-frontend/src/translations/sv.tsx b/kompassi-v2-frontend/src/translations/sv.tsx index bfd25feaa..070c64b7c 100644 --- a/kompassi-v2-frontend/src/translations/sv.tsx +++ b/kompassi-v2-frontend/src/translations/sv.tsx @@ -523,7 +523,7 @@ const translations: Translations = { helpText: "Den momssats som gäller för denna produkt. Priserna inkluderar moms.", }, - vatIncluded: (rate: string) => `inkl. ${parseFloat(rate)}% moms`, + vatIncluded: (rate: string) => `inkl. ${rate}% moms`, vatBreakdown: "Momsspecifikation", dragToReorder: "Dra för att sortera om", newProductQuota: { diff --git a/kompassi/tickets_v2/optimized_server/utils/formatting.py b/kompassi/tickets_v2/optimized_server/utils/formatting.py index da4292d8b..0cbb5a0ae 100644 --- a/kompassi/tickets_v2/optimized_server/utils/formatting.py +++ b/kompassi/tickets_v2/optimized_server/utils/formatting.py @@ -17,20 +17,28 @@ def format_money(euros: Decimal) -> str: return f"{euros:0.2f} €".replace(".", ",") -def format_vat_rate(rate: Decimal) -> str: +def format_vat_rate(rate: Decimal, language: str = "en") -> str: """ Format a VAT rate for display, stripping unnecessary trailing zeros. + Uses comma as decimal separator for Finnish and Swedish locales. >>> format_vat_rate(Decimal('25.50')) '25.5' + >>> format_vat_rate(Decimal('25.50'), 'fi') + '25,5' >>> format_vat_rate(Decimal('10.00')) '10' + >>> format_vat_rate(Decimal('10.00'), 'fi') + '10' >>> format_vat_rate(Decimal('0')) '0' - >>> format_vat_rate(Decimal('13.50')) - '13.5' + >>> format_vat_rate(Decimal('13.50'), 'sv') + '13,5' """ - return str(rate.normalize()) + result = str(rate.normalize()) + if language in ("fi", "sv"): + result = result.replace(".", ",") + return result def format_order_number(order_number: int): diff --git a/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml b/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml index 7234d91dd..ba8223385 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_cancel_fi.eml @@ -8,7 +8,7 @@ Tilaus sisälsi seuraavat tuotteet: {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} Yhteensä: {{ total_price|format_money }} (sis. ALV) -{% for vb in vat_breakdown %}ALV {{ vb.rate|format_vat_rate }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) +{% for vb in vat_breakdown %}ALV {{ vb.rate|format_vat_rate:"fi" }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) {% endfor %} Jos epäilet, että tilauksesi on peruuntunut virheellisesti, diff --git a/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml b/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml index a9e6f0dae..04c08ccdb 100644 --- a/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml +++ b/kompassi/tickets_v2/templates/tickets_v2_receipt_fi.eml @@ -9,7 +9,7 @@ Vahvistamme maksetuiksi seuraavat lipputuotteet: {{ op.quantity }} x {{ op.price|format_money }} {% endfor %} Yhteensä: {{ total_price|format_money }} (sis. ALV) -{% for vb in vat_breakdown %}ALV {{ vb.rate|format_vat_rate }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) +{% for vb in vat_breakdown %}ALV {{ vb.rate|format_vat_rate:"fi" }}%: {{ vb.vat|format_money }} (sisältyy summaan {{ vb.gross|format_money }}) {% endfor %}{% if have_etickets %} Löydät tilaamasi sähköiset liput liitetiedostosta. Sähköinen lippu From 18192b3a3218793f04260d64397810ad31bbac81 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Fri, 24 Apr 2026 17:56:44 +0300 Subject: [PATCH 05/22] chore: generate frontend types --- kompassi-v2-frontend/src/__generated__/gql.ts | 24 +++++++++--------- .../src/__generated__/graphql.ts | 25 +++++++++++-------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/kompassi-v2-frontend/src/__generated__/gql.ts b/kompassi-v2-frontend/src/__generated__/gql.ts index a8b20a7a4..49385f321 100644 --- a/kompassi-v2-frontend/src/__generated__/gql.ts +++ b/kompassi-v2-frontend/src/__generated__/gql.ts @@ -34,7 +34,7 @@ type Documents = { "\n fragment AdminOrderPaymentStamp on LimitedPaymentStampType {\n id\n createdAt\n correlationId\n provider\n type\n status\n data\n }\n": typeof types.AdminOrderPaymentStampFragmentDoc, "\n fragment AdminOrderReceipt on LimitedReceiptType {\n correlationId\n createdAt\n email\n type\n status\n }\n": typeof types.AdminOrderReceiptFragmentDoc, "\n fragment AdminOrderCode on LimitedCodeType {\n code\n literateCode\n status\n usedOn\n productText\n }\n": typeof types.AdminOrderCodeFragmentDoc, - "\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n": typeof types.AdminOrderDetailDocument, + "\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n vatPercentage\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n": typeof types.AdminOrderDetailDocument, "\n mutation AdminCreateOrder($input: CreateOrderInput!) {\n createOrder(input: $input) {\n order {\n event {\n slug\n }\n id\n }\n }\n }\n": typeof types.AdminCreateOrderDocument, "\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n": typeof types.NewOrderProductFragmentDoc, "\n query NewOrderPage($eventSlug: String!) {\n event(slug: $eventSlug) {\n name\n slug\n\n tickets {\n products {\n ...NewOrderProduct\n }\n }\n }\n }\n": typeof types.NewOrderPageDocument, @@ -50,8 +50,8 @@ type Documents = { "\n query PeoplePage(\n $eventSlug: String!\n $filters: [DimensionFilterInput!]\n $locale: String\n $search: String\n $returnNone: Boolean = false\n ) {\n event(slug: $eventSlug) {\n slug\n name\n timezone\n\n involvement {\n dimensions(publicOnly: false) {\n ...DimensionFilter\n ...CachedDimensionsBadges\n ...DimensionValueSelect\n }\n\n people(filters: $filters, search: $search, returnNone: $returnNone) {\n ...InvolvedPerson\n }\n }\n }\n }\n": typeof types.PeoplePageDocument, "\n mutation UpdateProduct($input: UpdateProductInput!) {\n updateProduct(input: $input) {\n product {\n id\n }\n }\n }\n": typeof types.UpdateProductDocument, "\n mutation DeleteProduct($input: DeleteProductInput!) {\n deleteProduct(input: $input) {\n id\n }\n }\n": typeof types.DeleteProductDocument, - "\n fragment AdminProductOldVersion on LimitedProductType {\n createdAt\n title\n description\n price\n eticketsPerProduct\n maxPerOrder\n }\n": typeof types.AdminProductOldVersionFragmentDoc, - "\n fragment AdminProductDetail on FullProductType {\n id\n createdAt\n title\n description\n price\n eticketsPerProduct\n maxPerOrder\n availableFrom\n availableUntil\n canDelete\n\n quotas {\n id\n }\n\n supersededBy {\n id\n }\n\n oldVersions {\n ...AdminProductOldVersion\n }\n }\n": typeof types.AdminProductDetailFragmentDoc, + "\n fragment AdminProductOldVersion on LimitedProductType {\n createdAt\n title\n description\n price\n vatPercentage\n eticketsPerProduct\n maxPerOrder\n }\n": typeof types.AdminProductOldVersionFragmentDoc, + "\n fragment AdminProductDetail on FullProductType {\n id\n createdAt\n title\n description\n price\n vatPercentage\n eticketsPerProduct\n maxPerOrder\n availableFrom\n availableUntil\n canDelete\n\n quotas {\n id\n }\n\n supersededBy {\n id\n }\n\n oldVersions {\n ...AdminProductOldVersion\n }\n }\n": typeof types.AdminProductDetailFragmentDoc, "\n query AdminProductDetailPage($eventSlug: String!, $productId: String!) {\n event(slug: $eventSlug) {\n name\n slug\n\n tickets {\n quotas {\n id\n name\n countTotal\n }\n\n product(id: $productId) {\n ...AdminProductDetail\n }\n }\n }\n }\n": typeof types.AdminProductDetailPageDocument, "\n mutation CreateProduct($input: CreateProductInput!) {\n createProduct(input: $input) {\n product {\n id\n }\n }\n }\n": typeof types.CreateProductDocument, "\n mutation ReorderProducts($input: ReorderProductsInput!) {\n reorderProducts(input: $input) {\n products {\n id\n }\n }\n }\n": typeof types.ReorderProductsDocument, @@ -178,7 +178,7 @@ type Documents = { "\n mutation RevokeKeyPair($id: String!) {\n revokeKeyPair(id: $id) {\n id\n }\n }\n": typeof types.RevokeKeyPairDocument, "\n fragment ProfileEncryptionKeys on KeyPairType {\n id\n createdAt\n }\n": typeof types.ProfileEncryptionKeysFragmentDoc, "\n query ProfileEncryptionKeys {\n profile {\n keypairs {\n ...ProfileEncryptionKeys\n }\n }\n }\n": typeof types.ProfileEncryptionKeysDocument, - "\n query ProfileOrderDetail($eventSlug: String!, $orderId: String!) {\n profile {\n tickets {\n order(eventSlug: $eventSlug, id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n products {\n title\n quantity\n price\n }\n\n event {\n slug\n name\n }\n }\n }\n }\n }\n": typeof types.ProfileOrderDetailDocument, + "\n query ProfileOrderDetail($eventSlug: String!, $orderId: String!) {\n profile {\n tickets {\n order(eventSlug: $eventSlug, id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n products {\n title\n quantity\n price\n vatPercentage\n }\n\n event {\n slug\n name\n }\n }\n }\n }\n }\n": typeof types.ProfileOrderDetailDocument, "\n mutation ConfirmEmail($input: ConfirmEmailInput!) {\n confirmEmail(input: $input) {\n user {\n email\n }\n }\n }\n": typeof types.ConfirmEmailDocument, "\n fragment ProfileOrder on ProfileOrderType {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n\n event {\n slug\n name\n }\n }\n": typeof types.ProfileOrderFragmentDoc, "\n query ProfileOrders {\n profile {\n tickets {\n orders {\n ...ProfileOrder\n }\n\n haveUnlinkedOrders\n }\n }\n }\n": typeof types.ProfileOrdersDocument, @@ -231,7 +231,7 @@ const documents: Documents = { "\n fragment AdminOrderPaymentStamp on LimitedPaymentStampType {\n id\n createdAt\n correlationId\n provider\n type\n status\n data\n }\n": types.AdminOrderPaymentStampFragmentDoc, "\n fragment AdminOrderReceipt on LimitedReceiptType {\n correlationId\n createdAt\n email\n type\n status\n }\n": types.AdminOrderReceiptFragmentDoc, "\n fragment AdminOrderCode on LimitedCodeType {\n code\n literateCode\n status\n usedOn\n productText\n }\n": types.AdminOrderCodeFragmentDoc, - "\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n": types.AdminOrderDetailDocument, + "\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n vatPercentage\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n": types.AdminOrderDetailDocument, "\n mutation AdminCreateOrder($input: CreateOrderInput!) {\n createOrder(input: $input) {\n order {\n event {\n slug\n }\n id\n }\n }\n }\n": types.AdminCreateOrderDocument, "\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n": types.NewOrderProductFragmentDoc, "\n query NewOrderPage($eventSlug: String!) {\n event(slug: $eventSlug) {\n name\n slug\n\n tickets {\n products {\n ...NewOrderProduct\n }\n }\n }\n }\n": types.NewOrderPageDocument, @@ -247,8 +247,8 @@ const documents: Documents = { "\n query PeoplePage(\n $eventSlug: String!\n $filters: [DimensionFilterInput!]\n $locale: String\n $search: String\n $returnNone: Boolean = false\n ) {\n event(slug: $eventSlug) {\n slug\n name\n timezone\n\n involvement {\n dimensions(publicOnly: false) {\n ...DimensionFilter\n ...CachedDimensionsBadges\n ...DimensionValueSelect\n }\n\n people(filters: $filters, search: $search, returnNone: $returnNone) {\n ...InvolvedPerson\n }\n }\n }\n }\n": types.PeoplePageDocument, "\n mutation UpdateProduct($input: UpdateProductInput!) {\n updateProduct(input: $input) {\n product {\n id\n }\n }\n }\n": types.UpdateProductDocument, "\n mutation DeleteProduct($input: DeleteProductInput!) {\n deleteProduct(input: $input) {\n id\n }\n }\n": types.DeleteProductDocument, - "\n fragment AdminProductOldVersion on LimitedProductType {\n createdAt\n title\n description\n price\n eticketsPerProduct\n maxPerOrder\n }\n": types.AdminProductOldVersionFragmentDoc, - "\n fragment AdminProductDetail on FullProductType {\n id\n createdAt\n title\n description\n price\n eticketsPerProduct\n maxPerOrder\n availableFrom\n availableUntil\n canDelete\n\n quotas {\n id\n }\n\n supersededBy {\n id\n }\n\n oldVersions {\n ...AdminProductOldVersion\n }\n }\n": types.AdminProductDetailFragmentDoc, + "\n fragment AdminProductOldVersion on LimitedProductType {\n createdAt\n title\n description\n price\n vatPercentage\n eticketsPerProduct\n maxPerOrder\n }\n": types.AdminProductOldVersionFragmentDoc, + "\n fragment AdminProductDetail on FullProductType {\n id\n createdAt\n title\n description\n price\n vatPercentage\n eticketsPerProduct\n maxPerOrder\n availableFrom\n availableUntil\n canDelete\n\n quotas {\n id\n }\n\n supersededBy {\n id\n }\n\n oldVersions {\n ...AdminProductOldVersion\n }\n }\n": types.AdminProductDetailFragmentDoc, "\n query AdminProductDetailPage($eventSlug: String!, $productId: String!) {\n event(slug: $eventSlug) {\n name\n slug\n\n tickets {\n quotas {\n id\n name\n countTotal\n }\n\n product(id: $productId) {\n ...AdminProductDetail\n }\n }\n }\n }\n": types.AdminProductDetailPageDocument, "\n mutation CreateProduct($input: CreateProductInput!) {\n createProduct(input: $input) {\n product {\n id\n }\n }\n }\n": types.CreateProductDocument, "\n mutation ReorderProducts($input: ReorderProductsInput!) {\n reorderProducts(input: $input) {\n products {\n id\n }\n }\n }\n": types.ReorderProductsDocument, @@ -375,7 +375,7 @@ const documents: Documents = { "\n mutation RevokeKeyPair($id: String!) {\n revokeKeyPair(id: $id) {\n id\n }\n }\n": types.RevokeKeyPairDocument, "\n fragment ProfileEncryptionKeys on KeyPairType {\n id\n createdAt\n }\n": types.ProfileEncryptionKeysFragmentDoc, "\n query ProfileEncryptionKeys {\n profile {\n keypairs {\n ...ProfileEncryptionKeys\n }\n }\n }\n": types.ProfileEncryptionKeysDocument, - "\n query ProfileOrderDetail($eventSlug: String!, $orderId: String!) {\n profile {\n tickets {\n order(eventSlug: $eventSlug, id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n products {\n title\n quantity\n price\n }\n\n event {\n slug\n name\n }\n }\n }\n }\n }\n": types.ProfileOrderDetailDocument, + "\n query ProfileOrderDetail($eventSlug: String!, $orderId: String!) {\n profile {\n tickets {\n order(eventSlug: $eventSlug, id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n products {\n title\n quantity\n price\n vatPercentage\n }\n\n event {\n slug\n name\n }\n }\n }\n }\n }\n": types.ProfileOrderDetailDocument, "\n mutation ConfirmEmail($input: ConfirmEmailInput!) {\n confirmEmail(input: $input) {\n user {\n email\n }\n }\n }\n": types.ConfirmEmailDocument, "\n fragment ProfileOrder on ProfileOrderType {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n\n event {\n slug\n name\n }\n }\n": types.ProfileOrderFragmentDoc, "\n query ProfileOrders {\n profile {\n tickets {\n orders {\n ...ProfileOrder\n }\n\n haveUnlinkedOrders\n }\n }\n }\n": types.ProfileOrdersDocument, @@ -505,7 +505,7 @@ export function graphql(source: "\n fragment AdminOrderCode on LimitedCodeType /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n"]; +export function graphql(source: "\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n vatPercentage\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n vatPercentage\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -569,11 +569,11 @@ export function graphql(source: "\n mutation DeleteProduct($input: DeleteProduc /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment AdminProductOldVersion on LimitedProductType {\n createdAt\n title\n description\n price\n eticketsPerProduct\n maxPerOrder\n }\n"): (typeof documents)["\n fragment AdminProductOldVersion on LimitedProductType {\n createdAt\n title\n description\n price\n eticketsPerProduct\n maxPerOrder\n }\n"]; +export function graphql(source: "\n fragment AdminProductOldVersion on LimitedProductType {\n createdAt\n title\n description\n price\n vatPercentage\n eticketsPerProduct\n maxPerOrder\n }\n"): (typeof documents)["\n fragment AdminProductOldVersion on LimitedProductType {\n createdAt\n title\n description\n price\n vatPercentage\n eticketsPerProduct\n maxPerOrder\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment AdminProductDetail on FullProductType {\n id\n createdAt\n title\n description\n price\n eticketsPerProduct\n maxPerOrder\n availableFrom\n availableUntil\n canDelete\n\n quotas {\n id\n }\n\n supersededBy {\n id\n }\n\n oldVersions {\n ...AdminProductOldVersion\n }\n }\n"): (typeof documents)["\n fragment AdminProductDetail on FullProductType {\n id\n createdAt\n title\n description\n price\n eticketsPerProduct\n maxPerOrder\n availableFrom\n availableUntil\n canDelete\n\n quotas {\n id\n }\n\n supersededBy {\n id\n }\n\n oldVersions {\n ...AdminProductOldVersion\n }\n }\n"]; +export function graphql(source: "\n fragment AdminProductDetail on FullProductType {\n id\n createdAt\n title\n description\n price\n vatPercentage\n eticketsPerProduct\n maxPerOrder\n availableFrom\n availableUntil\n canDelete\n\n quotas {\n id\n }\n\n supersededBy {\n id\n }\n\n oldVersions {\n ...AdminProductOldVersion\n }\n }\n"): (typeof documents)["\n fragment AdminProductDetail on FullProductType {\n id\n createdAt\n title\n description\n price\n vatPercentage\n eticketsPerProduct\n maxPerOrder\n availableFrom\n availableUntil\n canDelete\n\n quotas {\n id\n }\n\n supersededBy {\n id\n }\n\n oldVersions {\n ...AdminProductOldVersion\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -1081,7 +1081,7 @@ export function graphql(source: "\n query ProfileEncryptionKeys {\n profile /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ProfileOrderDetail($eventSlug: String!, $orderId: String!) {\n profile {\n tickets {\n order(eventSlug: $eventSlug, id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n products {\n title\n quantity\n price\n }\n\n event {\n slug\n name\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ProfileOrderDetail($eventSlug: String!, $orderId: String!) {\n profile {\n tickets {\n order(eventSlug: $eventSlug, id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n products {\n title\n quantity\n price\n }\n\n event {\n slug\n name\n }\n }\n }\n }\n }\n"]; +export function graphql(source: "\n query ProfileOrderDetail($eventSlug: String!, $orderId: String!) {\n profile {\n tickets {\n order(eventSlug: $eventSlug, id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n products {\n title\n quantity\n price\n vatPercentage\n }\n\n event {\n slug\n name\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ProfileOrderDetail($eventSlug: String!, $orderId: String!) {\n profile {\n tickets {\n order(eventSlug: $eventSlug, id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n canPay\n canCancel\n products {\n title\n quantity\n price\n vatPercentage\n }\n\n event {\n slug\n name\n }\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/kompassi-v2-frontend/src/__generated__/graphql.ts b/kompassi-v2-frontend/src/__generated__/graphql.ts index 38f4a053d..56b54b58d 100644 --- a/kompassi-v2-frontend/src/__generated__/graphql.ts +++ b/kompassi-v2-frontend/src/__generated__/graphql.ts @@ -671,6 +671,8 @@ export type FullProductType = { /** The product superseding this product, if any. */ supersededBy?: Maybe; title: Scalars['String']['output']; + /** VAT percentage applied to this product. Prices are inclusive of VAT. */ + vatPercentage: Scalars['Decimal']['output']; }; /** @@ -1214,6 +1216,8 @@ export type LimitedProductType = { price: Scalars['Decimal']['output']; quotas: Array>; title: Scalars['String']['output']; + /** VAT percentage applied to this product. Prices are inclusive of VAT. */ + vatPercentage: Scalars['Decimal']['output']; }; /** Represent Person without a way to traverse back to Event. */ @@ -1943,6 +1947,7 @@ export type OrderProductType = { price: Scalars['Decimal']['output']; quantity: Scalars['Int']['output']; title: Scalars['String']['output']; + vatPercentage: Scalars['Decimal']['output']; }; export type OwnProfileType = { @@ -3032,7 +3037,7 @@ export type AdminOrderDetailQueryVariables = Exact<{ }>; -export type AdminOrderDetailQuery = { __typename?: 'Query', event?: { __typename?: 'FullEventType', slug: string, name: string, tickets?: { __typename?: 'TicketsV2EventMetaType', order?: { __typename?: 'FullOrderType', id: string, formattedOrderNumber: string, createdAt: string, totalPrice: any, status: PaymentStatus, eticketsLink?: string | null, firstName: string, lastName: string, email: string, phone: string, canRefund: boolean, canRefundManually: boolean, canMarkAsPaid: boolean, products: Array<{ __typename?: 'OrderProductType', title: string, quantity: number, price: any }>, paymentStamps: Array<{ __typename?: 'LimitedPaymentStampType', id: string, createdAt: string, correlationId: string, provider: PaymentProvider, type: PaymentStampType, status: PaymentStatus, data: unknown }>, receipts: Array<{ __typename?: 'LimitedReceiptType', correlationId: string, createdAt: string, email: string, type: ReceiptType, status: ReceiptStatus }>, codes: Array<{ __typename?: 'LimitedCodeType', code: string, literateCode: string, status: CodeStatus, usedOn?: string | null, productText: string }> } | null } | null } | null }; +export type AdminOrderDetailQuery = { __typename?: 'Query', event?: { __typename?: 'FullEventType', slug: string, name: string, tickets?: { __typename?: 'TicketsV2EventMetaType', order?: { __typename?: 'FullOrderType', id: string, formattedOrderNumber: string, createdAt: string, totalPrice: any, status: PaymentStatus, eticketsLink?: string | null, firstName: string, lastName: string, email: string, phone: string, canRefund: boolean, canRefundManually: boolean, canMarkAsPaid: boolean, products: Array<{ __typename?: 'OrderProductType', title: string, quantity: number, price: any, vatPercentage: any }>, paymentStamps: Array<{ __typename?: 'LimitedPaymentStampType', id: string, createdAt: string, correlationId: string, provider: PaymentProvider, type: PaymentStampType, status: PaymentStatus, data: unknown }>, receipts: Array<{ __typename?: 'LimitedReceiptType', correlationId: string, createdAt: string, email: string, type: ReceiptType, status: ReceiptStatus }>, codes: Array<{ __typename?: 'LimitedCodeType', code: string, literateCode: string, status: CodeStatus, usedOn?: string | null, productText: string }> } | null } | null } | null }; export type AdminCreateOrderMutationVariables = Exact<{ input: CreateOrderInput; @@ -3113,9 +3118,9 @@ export type DeleteProductMutationVariables = Exact<{ export type DeleteProductMutation = { __typename?: 'Mutation', deleteProduct?: { __typename?: 'DeleteProduct', id: string } | null }; -export type AdminProductOldVersionFragment = { __typename?: 'LimitedProductType', createdAt: string, title: string, description: string, price: any, eticketsPerProduct: number, maxPerOrder: number }; +export type AdminProductOldVersionFragment = { __typename?: 'LimitedProductType', createdAt: string, title: string, description: string, price: any, vatPercentage: any, eticketsPerProduct: number, maxPerOrder: number }; -export type AdminProductDetailFragment = { __typename?: 'FullProductType', id: number, createdAt: string, title: string, description: string, price: any, eticketsPerProduct: number, maxPerOrder: number, availableFrom?: string | null, availableUntil?: string | null, canDelete: boolean, quotas: Array<{ __typename?: 'LimitedQuotaType', id: string }>, supersededBy?: { __typename?: 'LimitedProductType', id: number } | null, oldVersions: Array<{ __typename?: 'LimitedProductType', createdAt: string, title: string, description: string, price: any, eticketsPerProduct: number, maxPerOrder: number }> }; +export type AdminProductDetailFragment = { __typename?: 'FullProductType', id: number, createdAt: string, title: string, description: string, price: any, vatPercentage: any, eticketsPerProduct: number, maxPerOrder: number, availableFrom?: string | null, availableUntil?: string | null, canDelete: boolean, quotas: Array<{ __typename?: 'LimitedQuotaType', id: string }>, supersededBy?: { __typename?: 'LimitedProductType', id: number } | null, oldVersions: Array<{ __typename?: 'LimitedProductType', createdAt: string, title: string, description: string, price: any, vatPercentage: any, eticketsPerProduct: number, maxPerOrder: number }> }; export type AdminProductDetailPageQueryVariables = Exact<{ eventSlug: Scalars['String']['input']; @@ -3123,7 +3128,7 @@ export type AdminProductDetailPageQueryVariables = Exact<{ }>; -export type AdminProductDetailPageQuery = { __typename?: 'Query', event?: { __typename?: 'FullEventType', name: string, slug: string, tickets?: { __typename?: 'TicketsV2EventMetaType', quotas: Array<{ __typename?: 'FullQuotaType', id: string, name: string, countTotal: number }>, product: { __typename?: 'FullProductType', id: number, createdAt: string, title: string, description: string, price: any, eticketsPerProduct: number, maxPerOrder: number, availableFrom?: string | null, availableUntil?: string | null, canDelete: boolean, quotas: Array<{ __typename?: 'LimitedQuotaType', id: string }>, supersededBy?: { __typename?: 'LimitedProductType', id: number } | null, oldVersions: Array<{ __typename?: 'LimitedProductType', createdAt: string, title: string, description: string, price: any, eticketsPerProduct: number, maxPerOrder: number }> } } | null } | null }; +export type AdminProductDetailPageQuery = { __typename?: 'Query', event?: { __typename?: 'FullEventType', name: string, slug: string, tickets?: { __typename?: 'TicketsV2EventMetaType', quotas: Array<{ __typename?: 'FullQuotaType', id: string, name: string, countTotal: number }>, product: { __typename?: 'FullProductType', id: number, createdAt: string, title: string, description: string, price: any, vatPercentage: any, eticketsPerProduct: number, maxPerOrder: number, availableFrom?: string | null, availableUntil?: string | null, canDelete: boolean, quotas: Array<{ __typename?: 'LimitedQuotaType', id: string }>, supersededBy?: { __typename?: 'LimitedProductType', id: number } | null, oldVersions: Array<{ __typename?: 'LimitedProductType', createdAt: string, title: string, description: string, price: any, vatPercentage: any, eticketsPerProduct: number, maxPerOrder: number }> } } | null } | null }; export type CreateProductMutationVariables = Exact<{ input: CreateProductInput; @@ -3925,7 +3930,7 @@ export type ProfileOrderDetailQueryVariables = Exact<{ }>; -export type ProfileOrderDetailQuery = { __typename?: 'Query', profile?: { __typename?: 'OwnProfileType', tickets: { __typename?: 'TicketsV2ProfileMetaType', order?: { __typename?: 'ProfileOrderType', id: string, formattedOrderNumber: string, createdAt: string, totalPrice: any, status: PaymentStatus, eticketsLink?: string | null, canPay: boolean, canCancel: boolean, products: Array<{ __typename?: 'OrderProductType', title: string, quantity: number, price: any }>, event: { __typename?: 'LimitedEventType', slug: string, name: string } } | null } } | null }; +export type ProfileOrderDetailQuery = { __typename?: 'Query', profile?: { __typename?: 'OwnProfileType', tickets: { __typename?: 'TicketsV2ProfileMetaType', order?: { __typename?: 'ProfileOrderType', id: string, formattedOrderNumber: string, createdAt: string, totalPrice: any, status: PaymentStatus, eticketsLink?: string | null, canPay: boolean, canCancel: boolean, products: Array<{ __typename?: 'OrderProductType', title: string, quantity: number, price: any, vatPercentage: any }>, event: { __typename?: 'LimitedEventType', slug: string, name: string } } | null } } | null }; export type ConfirmEmailMutationVariables = Exact<{ input: ConfirmEmailInput; @@ -4042,8 +4047,8 @@ export const InvolvedPersonDetailInvolvementFragmentDoc = {"kind":"Document","de export const InvolvedPersonDetailFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPersonDetail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileWithInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"nick"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}},{"kind":"Field","name":{"kind":"Name","value":"discordHandle"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"profileFieldSelector"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullProfileFieldSelector"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"involvements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"InvolvedPersonDetailInvolvement"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullProfileFieldSelector"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileFieldSelectorType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"nick"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}},{"kind":"Field","name":{"kind":"Name","value":"discordHandle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPersonDetailInvolvement"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"adminLink"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"cachedDimensions"}},{"kind":"Field","name":{"kind":"Name","value":"cachedAnnotations"}}]}}]} as unknown as DocumentNode; export const InvolvedPersonInvolvementFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPersonInvolvement"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"adminLink"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"cachedDimensions"}},{"kind":"Field","name":{"kind":"Name","value":"cachedAnnotations"}}]}}]} as unknown as DocumentNode; export const InvolvedPersonFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPerson"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileWithInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"nick"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"involvements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"InvolvedPersonInvolvement"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPersonInvolvement"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"adminLink"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"cachedDimensions"}},{"kind":"Field","name":{"kind":"Name","value":"cachedAnnotations"}}]}}]} as unknown as DocumentNode; -export const AdminProductOldVersionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductOldVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; -export const AdminProductDetailFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductDetail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"canDelete"}},{"kind":"Field","name":{"kind":"Name","value":"quotas"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"supersededBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"oldVersions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminProductOldVersion"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductOldVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; +export const AdminProductOldVersionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductOldVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; +export const AdminProductDetailFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductDetail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"canDelete"}},{"kind":"Field","name":{"kind":"Name","value":"quotas"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"supersededBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"oldVersions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminProductOldVersion"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductOldVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; export const ProductListFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProductList"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"countPaid"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}},{"kind":"Field","name":{"kind":"Name","value":"countAvailable"}}]}}]} as unknown as DocumentNode; export const ProgramAdminDetailHostFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProgramAdminDetailHost"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProgramHostType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cachedDimensions"}},{"kind":"Field","name":{"kind":"Name","value":"programHostRole"}},{"kind":"Field","name":{"kind":"Name","value":"person"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"nick"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}},{"kind":"Field","name":{"kind":"Name","value":"discordHandle"}}]}}]}}]} as unknown as DocumentNode; export const ProgramAdminDetailInvitationFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProgramAdminDetailInvitation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedInvitationType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"cachedDimensions"}}]}}]} as unknown as DocumentNode; @@ -4110,7 +4115,7 @@ export const ResendOrderConfirmationDocument = {"kind":"Document","definitions": export const UpdateOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateOrderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const CancelAndRefundOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CancelAndRefundOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CancelAndRefundOrderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cancelAndRefundOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const MarkOrderAsPaidDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkOrderAsPaid"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MarkOrderAsPaidInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markOrderAsPaid"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; -export const AdminOrderDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminOrderDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsLink"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"canRefund"}},{"kind":"Field","name":{"kind":"Name","value":"canRefundManually"}},{"kind":"Field","name":{"kind":"Name","value":"canMarkAsPaid"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"price"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paymentStamps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderPaymentStamp"}}]}},{"kind":"Field","name":{"kind":"Name","value":"receipts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderReceipt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"codes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderCode"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderPaymentStamp"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedPaymentStampType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"correlationId"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"data"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderReceipt"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedReceiptType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"correlationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderCode"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedCodeType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"literateCode"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"usedOn"}},{"kind":"Field","name":{"kind":"Name","value":"productText"}}]}}]} as unknown as DocumentNode; +export const AdminOrderDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminOrderDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsLink"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"canRefund"}},{"kind":"Field","name":{"kind":"Name","value":"canRefundManually"}},{"kind":"Field","name":{"kind":"Name","value":"canMarkAsPaid"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paymentStamps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderPaymentStamp"}}]}},{"kind":"Field","name":{"kind":"Name","value":"receipts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderReceipt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"codes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderCode"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderPaymentStamp"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedPaymentStampType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"correlationId"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"data"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderReceipt"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedReceiptType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"correlationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderCode"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedCodeType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"literateCode"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"usedOn"}},{"kind":"Field","name":{"kind":"Name","value":"productText"}}]}}]} as unknown as DocumentNode; export const AdminCreateOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AdminCreateOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOrderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const NewOrderPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"NewOrderPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NewOrderProduct"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NewOrderProduct"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"countPaid"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}},{"kind":"Field","name":{"kind":"Name","value":"countAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; export const AdminOrderListWithOrdersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminOrderListWithOrders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DimensionFilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"returnNone"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}},"defaultValue":{"kind":"BooleanValue","value":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProductChoice"}}]}},{"kind":"Field","name":{"kind":"Name","value":"orders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}},{"kind":"Argument","name":{"kind":"Name","value":"returnNone"},"value":{"kind":"Variable","name":{"kind":"Name","value":"returnNone"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderList"}}]}},{"kind":"Field","name":{"kind":"Name","value":"countTotalOrders"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProductChoice"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderList"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullOrderType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode; @@ -4119,7 +4124,7 @@ export const PersonPageDocument = {"kind":"Document","definitions":[{"kind":"Ope export const PeoplePageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeoplePage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DimensionFilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"returnNone"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}},"defaultValue":{"kind":"BooleanValue","value":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"involvement"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dimensions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"publicOnly"},"value":{"kind":"BooleanValue","value":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"DimensionFilter"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CachedDimensionsBadges"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"DimensionValueSelect"}}]}},{"kind":"Field","name":{"kind":"Name","value":"people"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}},{"kind":"Argument","name":{"kind":"Name","value":"returnNone"},"value":{"kind":"Variable","name":{"kind":"Name","value":"returnNone"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"InvolvedPerson"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DimensionFilterValue"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DimensionValueType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPersonInvolvement"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"adminLink"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"cachedDimensions"}},{"kind":"Field","name":{"kind":"Name","value":"cachedAnnotations"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DimensionFilter"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullDimensionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"isMultiValue"}},{"kind":"Field","name":{"kind":"Name","value":"isListFilter"}},{"kind":"Field","name":{"kind":"Name","value":"isKeyDimension"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"DimensionFilterValue"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CachedDimensionsBadges"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullDimensionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"values"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DimensionValueSelect"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullDimensionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"isTechnical"}},{"kind":"Field","name":{"kind":"Name","value":"isMultiValue"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPerson"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileWithInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"nick"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"involvements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"InvolvedPersonInvolvement"}}]}}]}}]} as unknown as DocumentNode; export const UpdateProductDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateProduct"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateProductInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateProduct"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteProductDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteProduct"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteProductInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteProduct"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; -export const AdminProductDetailPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminProductDetailPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"productId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"quotas"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"countTotal"}}]}},{"kind":"Field","name":{"kind":"Name","value":"product"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"productId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminProductDetail"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductOldVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductDetail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"canDelete"}},{"kind":"Field","name":{"kind":"Name","value":"quotas"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"supersededBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"oldVersions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminProductOldVersion"}}]}}]}}]} as unknown as DocumentNode; +export const AdminProductDetailPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminProductDetailPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"productId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"quotas"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"countTotal"}}]}},{"kind":"Field","name":{"kind":"Name","value":"product"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"productId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminProductDetail"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductOldVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminProductDetail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsPerProduct"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"canDelete"}},{"kind":"Field","name":{"kind":"Name","value":"quotas"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"supersededBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"oldVersions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminProductOldVersion"}}]}}]}}]} as unknown as DocumentNode; export const CreateProductDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProduct"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateProductInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createProduct"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const ReorderProductsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ReorderProducts"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ReorderProductsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reorderProducts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const ProductListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProductList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProductList"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProductList"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"countPaid"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}},{"kind":"Field","name":{"kind":"Name","value":"countAvailable"}}]}}]} as unknown as DocumentNode; @@ -4216,7 +4221,7 @@ export const TicketsAdminReportsPageDocument = {"kind":"Document","definitions": export const GenerateKeyPairDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"GenerateKeyPair"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"generateKeyPair"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const RevokeKeyPairDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RevokeKeyPair"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"revokeKeyPair"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const ProfileEncryptionKeysDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProfileEncryptionKeys"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"keypairs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProfileEncryptionKeys"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProfileEncryptionKeys"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"KeyPairType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode; -export const ProfileOrderDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProfileOrderDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"eventSlug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}},{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsLink"}},{"kind":"Field","name":{"kind":"Name","value":"canPay"}},{"kind":"Field","name":{"kind":"Name","value":"canCancel"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"price"}}]}},{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const ProfileOrderDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProfileOrderDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"eventSlug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}},{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsLink"}},{"kind":"Field","name":{"kind":"Name","value":"canPay"}},{"kind":"Field","name":{"kind":"Name","value":"canCancel"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const ConfirmEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ConfirmEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ConfirmEmailInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"confirmEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}}]}}]} as unknown as DocumentNode; export const ProfileOrdersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProfileOrders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProfileOrder"}}]}},{"kind":"Field","name":{"kind":"Name","value":"haveUnlinkedOrders"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProfileOrder"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileOrderType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsLink"}},{"kind":"Field","name":{"kind":"Name","value":"canPay"}},{"kind":"Field","name":{"kind":"Name","value":"canCancel"}},{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const ProfileProgramItemListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProfileProgramItemList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"program"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"programs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userRelation"},"value":{"kind":"EnumValue","value":"HOSTING"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProfileProgramItem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"programOffers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"dimension"},"value":{"kind":"StringValue","value":"state","block":false}},{"kind":"ObjectField","name":{"kind":"Name","value":"values"},"value":{"kind":"StringValue","value":"new","block":false}}]}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProfileResponsesTableRow"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProfileProgramItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProgramType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scheduleItems"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"endTime"}},{"kind":"Field","name":{"kind":"Name","value":"durationMinutes"}},{"kind":"Field","name":{"kind":"Name","value":"location"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"subtitle"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProfileResponsesTableRow"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileResponseType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"revisionCreatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"canEdit"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"mode"},"value":{"kind":"EnumValue","value":"OWNER"}}]},{"kind":"Field","name":{"kind":"Name","value":"values"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"keyFieldsOnly"},"value":{"kind":"BooleanValue","value":true}}]},{"kind":"Field","name":{"kind":"Name","value":"dimensions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"keyDimensionsOnly"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dimension"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}},{"kind":"Field","name":{"kind":"Name","value":"value"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"form"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"survey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; From 19d5c4dab877fa636989e8c5a0a1bcfe8242543b Mon Sep 17 00:00:00 2001 From: Anssi Kolehmainen Date: Fri, 24 Apr 2026 18:51:33 +0300 Subject: [PATCH 06/22] fix: move vatIncluded from clientAttributes to serverAttributes 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> --- .../src/app/[locale]/[eventSlug]/products/[productId]/page.tsx | 2 +- kompassi-v2-frontend/src/components/tickets/ProductCard.tsx | 2 +- kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx | 2 +- kompassi-v2-frontend/src/translations/en.tsx | 2 +- kompassi-v2-frontend/src/translations/fi.tsx | 2 +- kompassi-v2-frontend/src/translations/sv.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx index 6eef8fdf2..df130799d 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/[productId]/page.tsx @@ -312,7 +312,7 @@ export default async function AdminProductDetailPage(props: Props) { slug: "vatPercentage", title: t.clientAttributes.vatPercentage.title, getCellContents: (product) => - t.clientAttributes.vatIncluded( + t.serverAttributes.vatIncluded( formatVatRate(product.vatPercentage, locale), ), className: "col-1 align-middle", diff --git a/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx b/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx index 7bc944dad..10c73fcdb 100644 --- a/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx +++ b/kompassi-v2-frontend/src/components/tickets/ProductCard.tsx @@ -33,7 +33,7 @@ export default function ProductCard({
{formatMoney(product.price)}
- {t.clientAttributes.vatIncluded( + {t.serverAttributes.vatIncluded( formatVatRate(product.vatPercentage, locale), )}
diff --git a/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx b/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx index 1fd2ae906..6af70364e 100644 --- a/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx +++ b/kompassi-v2-frontend/src/components/tickets/ProductsTable.tsx @@ -102,7 +102,7 @@ export default function ProductsTable({ {vatBreakdown.map(({ rate, vat }) => ( - {t.Product.clientAttributes.vatIncluded( + {t.Product.serverAttributes.vatIncluded( formatVatRate(rate, locale), )} diff --git a/kompassi-v2-frontend/src/translations/en.tsx b/kompassi-v2-frontend/src/translations/en.tsx index ac549fa50..2bfc81801 100644 --- a/kompassi-v2-frontend/src/translations/en.tsx +++ b/kompassi-v2-frontend/src/translations/en.tsx @@ -535,7 +535,6 @@ const translations = { helpText: "The VAT rate that applies to this product. Prices are VAT-inclusive.", }, - vatIncluded: (rate: string) => `incl. ${rate}% VAT`, vatBreakdown: "VAT breakdown", dragToReorder: "Drag to reorder", newProductQuota: { @@ -545,6 +544,7 @@ const translations = { }, }, serverAttributes: { + vatIncluded: (rate: string) => `incl. ${rate}% VAT`, isAvailable: { untilFurtherNotice: "Available until further notice", untilTime: (formattedTime: string) => diff --git a/kompassi-v2-frontend/src/translations/fi.tsx b/kompassi-v2-frontend/src/translations/fi.tsx index 683c86bec..3326de557 100644 --- a/kompassi-v2-frontend/src/translations/fi.tsx +++ b/kompassi-v2-frontend/src/translations/fi.tsx @@ -531,7 +531,6 @@ const translations: Translations = { helpText: "Tähän tuotteeseen sovellettava arvonlisäveroprosentti. Hinnat sisältävät ALV:n.", }, - vatIncluded: (rate: string) => `sis. ${rate}% ALV`, vatBreakdown: "ALV-erittely", dragToReorder: "Vedä ja pudota järjestääksesi tuotteita", newProductQuota: { @@ -541,6 +540,7 @@ const translations: Translations = { }, }, serverAttributes: { + vatIncluded: (rate: string) => `sis. ${rate}% ALV`, isAvailable: { untilFurtherNotice: "Saatavilla toistaiseksi", untilTime: (formattedTime: string) => diff --git a/kompassi-v2-frontend/src/translations/sv.tsx b/kompassi-v2-frontend/src/translations/sv.tsx index 070c64b7c..6ba208e5e 100644 --- a/kompassi-v2-frontend/src/translations/sv.tsx +++ b/kompassi-v2-frontend/src/translations/sv.tsx @@ -523,7 +523,6 @@ const translations: Translations = { helpText: "Den momssats som gäller för denna produkt. Priserna inkluderar moms.", }, - vatIncluded: (rate: string) => `inkl. ${rate}% moms`, vatBreakdown: "Momsspecifikation", dragToReorder: "Dra för att sortera om", newProductQuota: { @@ -533,6 +532,7 @@ const translations: Translations = { }, }, serverAttributes: { + vatIncluded: (rate: string) => `inkl. ${rate}% moms`, isAvailable: { untilFurtherNotice: "Tillgänglig tills vidare", untilTime: (formattedTime: string) => From 9f1b8d8ef6086d04caf25c8ff0f2b087a765e48d Mon Sep 17 00:00:00 2001 From: Anssi Kolehmainen Date: Fri, 24 Apr 2026 18:54:33 +0300 Subject: [PATCH 07/22] fix: add VAT percentage field to new product form 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> --- .../src/app/[locale]/[eventSlug]/products/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/page.tsx index 5a2614087..42dd56a73 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/page.tsx @@ -183,6 +183,17 @@ export default async function ProductsPage(props: Props) { decimalPlaces: 2, ...t.clientAttributes.unitPrice, }, + { + slug: "vatPercentage", + type: "SingleSelect", + choices: [ + { slug: "0.00", title: "0%" }, + { slug: "10.00", title: "10%" }, + { slug: "13.50", title: "13.5%" }, + { slug: "25.50", title: "25.5%" }, + ], + ...t.clientAttributes.vatPercentage, + }, { slug: "quota", type: "NumberField", From 77ab9d3614043ec9c38f1c64b877fba30b95eedc Mon Sep 17 00:00:00 2001 From: Anssi Kolehmainen Date: Fri, 24 Apr 2026 18:56:38 +0300 Subject: [PATCH 08/22] fix: apply camel_case_keys_to_snake_case in CreateProduct mutation 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> --- kompassi/tickets_v2/graphql/mutations/create_product.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kompassi/tickets_v2/graphql/mutations/create_product.py b/kompassi/tickets_v2/graphql/mutations/create_product.py index 2725d2fd1..dba911c73 100644 --- a/kompassi/tickets_v2/graphql/mutations/create_product.py +++ b/kompassi/tickets_v2/graphql/mutations/create_product.py @@ -6,6 +6,7 @@ from kompassi.access.cbac import graphql_check_model from kompassi.core.models import Event +from kompassi.core.utils.form_utils import camel_case_keys_to_snake_case from kompassi.event_log_v2.utils.emit import emit from ...models.product import Product @@ -49,7 +50,7 @@ def mutate( event = Event.objects.get(slug=input.event_slug) graphql_check_model(Product, event.scope, info, operation="create") - form = CreateProductForm(data=input.form_data) # type: ignore + form = CreateProductForm(data=camel_case_keys_to_snake_case(input.form_data)) # type: ignore if not form.is_valid(): raise ValueError(form.errors) From b772348ba34d25e8c6df861d2e48104d1986b83f Mon Sep 17 00:00:00 2001 From: Anssi Kolehmainen Date: Fri, 24 Apr 2026 19:05:05 +0300 Subject: [PATCH 09/22] fix: serialize PaytrailItem vatPercentage as JSON number 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> --- kompassi/tickets_v2/optimized_server/providers/paytrail.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kompassi/tickets_v2/optimized_server/providers/paytrail.py b/kompassi/tickets_v2/optimized_server/providers/paytrail.py index e917258f2..1a8209377 100644 --- a/kompassi/tickets_v2/optimized_server/providers/paytrail.py +++ b/kompassi/tickets_v2/optimized_server/providers/paytrail.py @@ -5,7 +5,6 @@ from collections.abc import Mapping from dataclasses import dataclass from datetime import UTC, datetime -from decimal import Decimal from enum import Enum from typing import Any, ClassVar, Literal, Self from uuid import UUID, uuid4 @@ -90,7 +89,7 @@ class PaytrailItem(pydantic.BaseModel): unit_price: int = pydantic.Field(serialization_alias="unitPrice") units: int - vat_percentage: Decimal = pydantic.Field(serialization_alias="vatPercentage") + vat_percentage: float = pydantic.Field(serialization_alias="vatPercentage") product_code: str = pydantic.Field(serialization_alias="productCode") description: str From e00b0135093f54b54085a0d51cdec7bb86ae7cb0 Mon Sep 17 00:00:00 2001 From: Anssi Kolehmainen Date: Fri, 24 Apr 2026 19:38:34 +0300 Subject: [PATCH 10/22] fix: add serialization alias for OrderProduct.vat_percentage 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> --- kompassi/tickets_v2/optimized_server/models/order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kompassi/tickets_v2/optimized_server/models/order.py b/kompassi/tickets_v2/optimized_server/models/order.py index 57398108e..120985ecb 100644 --- a/kompassi/tickets_v2/optimized_server/models/order.py +++ b/kompassi/tickets_v2/optimized_server/models/order.py @@ -125,7 +125,7 @@ class OrderProduct(pydantic.BaseModel): title: str price: Decimal quantity: int - vat_percentage: Decimal + vat_percentage: Decimal = pydantic.Field(serialization_alias="vatPercentage") class VatBreakdownLine(pydantic.BaseModel): From 9a1247fd60f4176d03d70724e07ae264b9123aab Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Tue, 12 May 2026 23:19:58 +0300 Subject: [PATCH 11/22] fix: make vatPercentage required on add product form Co-Authored-By: Claude Sonnet 4.6 --- .../src/app/[locale]/[eventSlug]/products/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/page.tsx index 42dd56a73..67d86fd8d 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/products/page.tsx @@ -186,6 +186,7 @@ export default async function ProductsPage(props: Props) { { slug: "vatPercentage", type: "SingleSelect", + required: true, choices: [ { slug: "0.00", title: "0%" }, { slug: "10.00", title: "10%" }, From 6bc43de6fadff1865d8352ce6f3e1d9a0c966d70 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Tue, 12 May 2026 23:31:57 +0300 Subject: [PATCH 12/22] fix: add vatPercentage and locale to ProductCard in new order page Co-Authored-By: Claude Sonnet 4.6 --- kompassi-v2-frontend/src/__generated__/gql.ts | 6 +++--- kompassi-v2-frontend/src/__generated__/graphql.ts | 8 ++++---- .../app/[locale]/[eventSlug]/orders-admin/new/page.tsx | 8 +++++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/kompassi-v2-frontend/src/__generated__/gql.ts b/kompassi-v2-frontend/src/__generated__/gql.ts index 49385f321..6ef041232 100644 --- a/kompassi-v2-frontend/src/__generated__/gql.ts +++ b/kompassi-v2-frontend/src/__generated__/gql.ts @@ -36,7 +36,7 @@ type Documents = { "\n fragment AdminOrderCode on LimitedCodeType {\n code\n literateCode\n status\n usedOn\n productText\n }\n": typeof types.AdminOrderCodeFragmentDoc, "\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n vatPercentage\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n": typeof types.AdminOrderDetailDocument, "\n mutation AdminCreateOrder($input: CreateOrderInput!) {\n createOrder(input: $input) {\n order {\n event {\n slug\n }\n id\n }\n }\n }\n": typeof types.AdminCreateOrderDocument, - "\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n": typeof types.NewOrderProductFragmentDoc, + "\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n vatPercentage\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n": typeof types.NewOrderProductFragmentDoc, "\n query NewOrderPage($eventSlug: String!) {\n event(slug: $eventSlug) {\n name\n slug\n\n tickets {\n products {\n ...NewOrderProduct\n }\n }\n }\n }\n": typeof types.NewOrderPageDocument, "\n fragment OrderList on FullOrderType {\n id\n formattedOrderNumber\n displayName\n email\n createdAt\n totalPrice\n status\n }\n": typeof types.OrderListFragmentDoc, "\n fragment ProductChoice on FullProductType {\n id\n title\n }\n": typeof types.ProductChoiceFragmentDoc, @@ -233,7 +233,7 @@ const documents: Documents = { "\n fragment AdminOrderCode on LimitedCodeType {\n code\n literateCode\n status\n usedOn\n productText\n }\n": types.AdminOrderCodeFragmentDoc, "\n query AdminOrderDetail($eventSlug: String!, $orderId: String!) {\n event(slug: $eventSlug) {\n slug\n name\n\n tickets {\n order(id: $orderId) {\n id\n formattedOrderNumber\n createdAt\n totalPrice\n status\n eticketsLink\n firstName\n lastName\n email\n phone\n canRefund\n canRefundManually\n canMarkAsPaid\n products {\n title\n quantity\n price\n vatPercentage\n }\n paymentStamps {\n ...AdminOrderPaymentStamp\n }\n receipts {\n ...AdminOrderReceipt\n }\n codes {\n ...AdminOrderCode\n }\n }\n }\n }\n }\n": types.AdminOrderDetailDocument, "\n mutation AdminCreateOrder($input: CreateOrderInput!) {\n createOrder(input: $input) {\n order {\n event {\n slug\n }\n id\n }\n }\n }\n": types.AdminCreateOrderDocument, - "\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n": types.NewOrderProductFragmentDoc, + "\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n vatPercentage\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n": types.NewOrderProductFragmentDoc, "\n query NewOrderPage($eventSlug: String!) {\n event(slug: $eventSlug) {\n name\n slug\n\n tickets {\n products {\n ...NewOrderProduct\n }\n }\n }\n }\n": types.NewOrderPageDocument, "\n fragment OrderList on FullOrderType {\n id\n formattedOrderNumber\n displayName\n email\n createdAt\n totalPrice\n status\n }\n": types.OrderListFragmentDoc, "\n fragment ProductChoice on FullProductType {\n id\n title\n }\n": types.ProductChoiceFragmentDoc, @@ -513,7 +513,7 @@ export function graphql(source: "\n mutation AdminCreateOrder($input: CreateOrd /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n"): (typeof documents)["\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n"]; +export function graphql(source: "\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n vatPercentage\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n"): (typeof documents)["\n fragment NewOrderProduct on FullProductType {\n id\n title\n description\n price\n vatPercentage\n isAvailable\n availableFrom\n availableUntil\n countPaid\n countReserved\n countAvailable\n maxPerOrder\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/kompassi-v2-frontend/src/__generated__/graphql.ts b/kompassi-v2-frontend/src/__generated__/graphql.ts index 56b54b58d..a634abdac 100644 --- a/kompassi-v2-frontend/src/__generated__/graphql.ts +++ b/kompassi-v2-frontend/src/__generated__/graphql.ts @@ -3046,14 +3046,14 @@ export type AdminCreateOrderMutationVariables = Exact<{ export type AdminCreateOrderMutation = { __typename?: 'Mutation', createOrder?: { __typename?: 'CreateOrder', order?: { __typename?: 'FullOrderType', id: string, event: { __typename?: 'LimitedEventType', slug: string } } | null } | null }; -export type NewOrderProductFragment = { __typename?: 'FullProductType', id: number, title: string, description: string, price: any, isAvailable: boolean, availableFrom?: string | null, availableUntil?: string | null, countPaid: number, countReserved: number, countAvailable?: number | null, maxPerOrder: number }; +export type NewOrderProductFragment = { __typename?: 'FullProductType', id: number, title: string, description: string, price: any, vatPercentage: any, isAvailable: boolean, availableFrom?: string | null, availableUntil?: string | null, countPaid: number, countReserved: number, countAvailable?: number | null, maxPerOrder: number }; export type NewOrderPageQueryVariables = Exact<{ eventSlug: Scalars['String']['input']; }>; -export type NewOrderPageQuery = { __typename?: 'Query', event?: { __typename?: 'FullEventType', name: string, slug: string, tickets?: { __typename?: 'TicketsV2EventMetaType', products: Array<{ __typename?: 'FullProductType', id: number, title: string, description: string, price: any, isAvailable: boolean, availableFrom?: string | null, availableUntil?: string | null, countPaid: number, countReserved: number, countAvailable?: number | null, maxPerOrder: number }> } | null } | null }; +export type NewOrderPageQuery = { __typename?: 'Query', event?: { __typename?: 'FullEventType', name: string, slug: string, tickets?: { __typename?: 'TicketsV2EventMetaType', products: Array<{ __typename?: 'FullProductType', id: number, title: string, description: string, price: any, vatPercentage: any, isAvailable: boolean, availableFrom?: string | null, availableUntil?: string | null, countPaid: number, countReserved: number, countAvailable?: number | null, maxPerOrder: number }> } | null } | null }; export type OrderListFragment = { __typename?: 'FullOrderType', id: string, formattedOrderNumber: string, displayName: string, email: string, createdAt: string, totalPrice: any, status: PaymentStatus }; @@ -4039,7 +4039,7 @@ export const TransferConsentFormRegistryFragmentDoc = {"kind":"Document","defini export const AdminOrderPaymentStampFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderPaymentStamp"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedPaymentStampType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"correlationId"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"data"}}]}}]} as unknown as DocumentNode; export const AdminOrderReceiptFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderReceipt"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedReceiptType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"correlationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode; export const AdminOrderCodeFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderCode"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedCodeType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"literateCode"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"usedOn"}},{"kind":"Field","name":{"kind":"Name","value":"productText"}}]}}]} as unknown as DocumentNode; -export const NewOrderProductFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NewOrderProduct"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"countPaid"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}},{"kind":"Field","name":{"kind":"Name","value":"countAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; +export const NewOrderProductFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NewOrderProduct"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"countPaid"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}},{"kind":"Field","name":{"kind":"Name","value":"countAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; export const OrderListFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderList"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullOrderType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode; export const ProductChoiceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProductChoice"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]} as unknown as DocumentNode; export const FullProfileFieldSelectorFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullProfileFieldSelector"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileFieldSelectorType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"nick"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}},{"kind":"Field","name":{"kind":"Name","value":"discordHandle"}}]}}]} as unknown as DocumentNode; @@ -4117,7 +4117,7 @@ export const CancelAndRefundOrderDocument = {"kind":"Document","definitions":[{" export const MarkOrderAsPaidDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkOrderAsPaid"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MarkOrderAsPaidInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markOrderAsPaid"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const AdminOrderDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminOrderDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"eticketsLink"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phone"}},{"kind":"Field","name":{"kind":"Name","value":"canRefund"}},{"kind":"Field","name":{"kind":"Name","value":"canRefundManually"}},{"kind":"Field","name":{"kind":"Name","value":"canMarkAsPaid"}},{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paymentStamps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderPaymentStamp"}}]}},{"kind":"Field","name":{"kind":"Name","value":"receipts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderReceipt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"codes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AdminOrderCode"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderPaymentStamp"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedPaymentStampType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"correlationId"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"data"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderReceipt"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedReceiptType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"correlationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AdminOrderCode"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedCodeType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"literateCode"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"usedOn"}},{"kind":"Field","name":{"kind":"Name","value":"productText"}}]}}]} as unknown as DocumentNode; export const AdminCreateOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AdminCreateOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOrderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; -export const NewOrderPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"NewOrderPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NewOrderProduct"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NewOrderProduct"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"countPaid"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}},{"kind":"Field","name":{"kind":"Name","value":"countAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; +export const NewOrderPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"NewOrderPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NewOrderProduct"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NewOrderProduct"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"vatPercentage"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"availableFrom"}},{"kind":"Field","name":{"kind":"Name","value":"availableUntil"}},{"kind":"Field","name":{"kind":"Name","value":"countPaid"}},{"kind":"Field","name":{"kind":"Name","value":"countReserved"}},{"kind":"Field","name":{"kind":"Name","value":"countAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"maxPerOrder"}}]}}]} as unknown as DocumentNode; export const AdminOrderListWithOrdersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminOrderListWithOrders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DimensionFilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"returnNone"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}},"defaultValue":{"kind":"BooleanValue","value":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"tickets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProductChoice"}}]}},{"kind":"Field","name":{"kind":"Name","value":"orders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}},{"kind":"Argument","name":{"kind":"Name","value":"returnNone"},"value":{"kind":"Variable","name":{"kind":"Name","value":"returnNone"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderList"}}]}},{"kind":"Field","name":{"kind":"Name","value":"countTotalOrders"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProductChoice"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullProductType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderList"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullOrderType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"formattedOrderNumber"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]} as unknown as DocumentNode; export const CancelOwnOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CancelOwnOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CancelOwnUnpaidOrderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cancelOwnUnpaidOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const PersonPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PersonPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locale"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"involvement"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dimensions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"publicOnly"},"value":{"kind":"BooleanValue","value":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CachedDimensionsBadges"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"DimensionValueSelect"}},{"kind":"Field","name":{"kind":"Name","value":"isKeyDimension"}},{"kind":"Field","name":{"kind":"Name","value":"isShownInDetail"}}]}},{"kind":"Field","name":{"kind":"Name","value":"annotations"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"publicOnly"},"value":{"kind":"BooleanValue","value":false}},{"kind":"Argument","name":{"kind":"Name","value":"perksOnly"},"value":{"kind":"BooleanValue","value":true}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AnnotationsFormAnnotation"}}]}},{"kind":"Field","name":{"kind":"Name","value":"person"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"InvolvedPersonDetail"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullProfileFieldSelector"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileFieldSelectorType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"nick"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}},{"kind":"Field","name":{"kind":"Name","value":"discordHandle"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPersonDetailInvolvement"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"adminLink"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"cachedDimensions"}},{"kind":"Field","name":{"kind":"Name","value":"cachedAnnotations"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CachedDimensionsBadges"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullDimensionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"values"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DimensionValueSelect"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FullDimensionType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"isTechnical"}},{"kind":"Field","name":{"kind":"Name","value":"isMultiValue"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AnnotationsFormAnnotation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AnnotationType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"title"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"description"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"lang"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locale"}}}]},{"kind":"Field","name":{"kind":"Name","value":"isComputed"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"InvolvedPersonDetail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProfileWithInvolvementType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"nick"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}},{"kind":"Field","name":{"kind":"Name","value":"discordHandle"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"profileFieldSelector"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullProfileFieldSelector"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"involvements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"InvolvedPersonDetailInvolvement"}}]}}]}}]} as unknown as DocumentNode; diff --git a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/orders-admin/new/page.tsx b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/orders-admin/new/page.tsx index c21c3c217..2faec7f5c 100644 --- a/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/orders-admin/new/page.tsx +++ b/kompassi-v2-frontend/src/app/[locale]/[eventSlug]/orders-admin/new/page.tsx @@ -26,6 +26,7 @@ graphql(` title description price + vatPercentage isAvailable availableFrom availableUntil @@ -172,7 +173,12 @@ export default async function OrdersPage(props: Props) { onSubmit={adminCreateOrder.bind(null, locale, eventSlug)} > {products.map((product) => ( - +
{producT.clientAttributes.countReserved.title}:{" "} From 26913bd95d409ba99dd2c4db615a8aaee875e1d6 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Tue, 12 May 2026 23:34:08 +0300 Subject: [PATCH 13/22] chore: translation style --- kompassi-v2-frontend/src/translations/en.tsx | 2 +- kompassi-v2-frontend/src/translations/fi.tsx | 2 +- kompassi-v2-frontend/src/translations/sv.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kompassi-v2-frontend/src/translations/en.tsx b/kompassi-v2-frontend/src/translations/en.tsx index 2bfc81801..c67ff6788 100644 --- a/kompassi-v2-frontend/src/translations/en.tsx +++ b/kompassi-v2-frontend/src/translations/en.tsx @@ -544,7 +544,7 @@ const translations = { }, }, serverAttributes: { - vatIncluded: (rate: string) => `incl. ${rate}% VAT`, + vatIncluded: (rate: string) => `incl. VAT ${rate}%`, isAvailable: { untilFurtherNotice: "Available until further notice", untilTime: (formattedTime: string) => diff --git a/kompassi-v2-frontend/src/translations/fi.tsx b/kompassi-v2-frontend/src/translations/fi.tsx index 3326de557..7e10808a2 100644 --- a/kompassi-v2-frontend/src/translations/fi.tsx +++ b/kompassi-v2-frontend/src/translations/fi.tsx @@ -540,7 +540,7 @@ const translations: Translations = { }, }, serverAttributes: { - vatIncluded: (rate: string) => `sis. ${rate}% ALV`, + vatIncluded: (rate: string) => `sis. ALV ${rate}%`, isAvailable: { untilFurtherNotice: "Saatavilla toistaiseksi", untilTime: (formattedTime: string) => diff --git a/kompassi-v2-frontend/src/translations/sv.tsx b/kompassi-v2-frontend/src/translations/sv.tsx index 6ba208e5e..941f53089 100644 --- a/kompassi-v2-frontend/src/translations/sv.tsx +++ b/kompassi-v2-frontend/src/translations/sv.tsx @@ -532,7 +532,7 @@ const translations: Translations = { }, }, serverAttributes: { - vatIncluded: (rate: string) => `inkl. ${rate}% moms`, + vatIncluded: (rate: string) => `inkl. moms${rate}%`, isAvailable: { untilFurtherNotice: "Tillgänglig tills vidare", untilTime: (formattedTime: string) => From 0eb9a28662dcf4304f2ae4d54107b61d56d76e5c Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Tue, 12 May 2026 23:36:40 +0300 Subject: [PATCH 14/22] fix(tickets_v2): typecheck --- kompassi/tickets_v2/optimized_server/providers/paytrail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kompassi/tickets_v2/optimized_server/providers/paytrail.py b/kompassi/tickets_v2/optimized_server/providers/paytrail.py index 1a8209377..62bd9a726 100644 --- a/kompassi/tickets_v2/optimized_server/providers/paytrail.py +++ b/kompassi/tickets_v2/optimized_server/providers/paytrail.py @@ -94,12 +94,12 @@ class PaytrailItem(pydantic.BaseModel): description: str @classmethod - def from_order_products(cls, order_products: list[OrderProduct]) -> list[PaytrailItem]: + def from_order_products(cls, order_products: list[OrderProduct]) -> list[Self]: return [ cls( unit_price=int(op.price * 100), units=op.quantity, - vat_percentage=op.vat_percentage, + vat_percentage=float(op.vat_percentage), product_code=str(i + 1), description=op.title, ) From d07d9fefee8cf85131404dbcec6e5573372668d6 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Tue, 12 May 2026 23:47:10 +0300 Subject: [PATCH 15/22] feat: add VAT by month report to tickets_v2 reports 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 --- kompassi/tickets_v2/reports/__init__.py | 2 + .../reports/sql/report_vat_by_month.sql | 22 +++++ kompassi/tickets_v2/reports/vat_by_month.py | 97 +++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 kompassi/tickets_v2/reports/sql/report_vat_by_month.sql create mode 100644 kompassi/tickets_v2/reports/vat_by_month.py diff --git a/kompassi/tickets_v2/reports/__init__.py b/kompassi/tickets_v2/reports/__init__.py index e36ef9d51..94bd28737 100644 --- a/kompassi/tickets_v2/reports/__init__.py +++ b/kompassi/tickets_v2/reports/__init__.py @@ -9,6 +9,7 @@ from .sales_by_payment_provider import sales_by_payment_provider from .ticket_exchange_by_hour import TicketExchangeByHour from .ticket_exchange_by_product import ticket_exchange_by_product +from .vat_by_month import VatByMonth REPORTS = dict( orders_by_payment_status=OrdersByPaymentStatus.report, @@ -16,6 +17,7 @@ sales_by_payment_provider=sales_by_payment_provider, ticket_exchange_by_product=ticket_exchange_by_product, ticket_exchange_by_hour=TicketExchangeByHour.report, + vat_by_month=VatByMonth.report, ) diff --git a/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql b/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql new file mode 100644 index 000000000..7af646d99 --- /dev/null +++ b/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql @@ -0,0 +1,22 @@ +select + to_char( + date_trunc( + 'month', + to_timestamp( + ('x' || left(replace(o.id::text, '-', ''), 12))::bit(48)::bigint / 1000.0 + ) at time zone %(event_timezone)s + ), + 'YYYY-MM' + ) as year_month, + p.vat_percentage, + sum(p.price * pd.quantity::numeric) as gross_sales +from + tickets_v2_order o + join lateral jsonb_each_text(o.product_data) as pd(product_id, quantity) on true + join tickets_v2_product p on p.id = pd.product_id::int +where + o.event_id = %(event_id)s + and o.cached_status = 3 + and pd.quantity::int > 0 +group by 1, 2 +order by 1, 2 diff --git a/kompassi/tickets_v2/reports/vat_by_month.py b/kompassi/tickets_v2/reports/vat_by_month.py new file mode 100644 index 000000000..c0d48dc4d --- /dev/null +++ b/kompassi/tickets_v2/reports/vat_by_month.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from decimal import Decimal +from pathlib import Path + +from django.db import connection + +from kompassi.core.models.event import Event +from kompassi.graphql_api.language import DEFAULT_LANGUAGE +from kompassi.reports.models.column import Column +from kompassi.reports.models.enums import TotalBy, TypeOfColumn +from kompassi.reports.models.report import Report + +SQL_DIR = Path(__file__).parent / "sql" + + +def _format_vat_rate(rate: Decimal) -> dict[str, str]: + normalized = rate.normalize() + en = f"{normalized}%" + fi = f"{str(normalized).replace('.', ',')}%" + return dict(fi=fi, en=en) + + +class VatByMonth: + query = (SQL_DIR / "report_vat_by_month.sql").read_text() + + @classmethod + def report(cls, event: Event, lang: str = DEFAULT_LANGUAGE) -> Report: + with connection.cursor() as cursor: + cursor.execute(cls.query, dict(event_id=event.id, event_timezone=event.timezone_name)) + raw_rows = cursor.fetchall() + + if not raw_rows: + return Report( + slug="vat_by_month", + title=dict(fi="ALV-erittely kuukausittain", en="VAT by month"), + columns=[ + Column( + slug="month", + title=dict(fi="Kuukausi", en="Month"), + type=TypeOfColumn.STRING, + total_by=TotalBy.NONE, + ) + ], + rows=[], + lang=lang, + ) + + months: list[str] = sorted({row[0] for row in raw_rows}) + vat_rates: list[Decimal] = sorted({row[1] for row in raw_rows}) + data: dict[tuple[str, Decimal], Decimal] = {(row[0], row[1]): row[2] for row in raw_rows} + + columns: list[Column] = [ + Column( + slug="month", + title=dict(fi="Kuukausi", en="Month"), + type=TypeOfColumn.STRING, + total_by=TotalBy.NONE, + ), + *[ + Column( + slug=f"vat_{rate}", + title=_format_vat_rate(rate), + type=TypeOfColumn.CURRENCY, + ) + for rate in vat_rates + ], + Column( + slug="total", + title=dict(fi="Yhteensä", en="Total"), + type=TypeOfColumn.CURRENCY, + ), + ] + + rows: list[list] = [] + for month in months: + row_total = Decimal(0) + row: list = [month] + for rate in vat_rates: + gross = data.get((month, rate), Decimal(0)) + row.append(float(gross)) + row_total += gross + row.append(float(row_total)) + rows.append(row) + + return Report( + slug="vat_by_month", + title=dict(fi="ALV-erittely kuukausittain", en="VAT by month"), + columns=columns, + rows=rows, + has_total_row=True, + lang=lang, + footer=dict( + fi="Vain maksetut tilaukset on laskettu mukaan. Hyvitykset eivät sisälly raporttiin.", + en="Only paid orders are included. Refunds are not reflected in this report.", + ), + ) From 8df3e3652e027dea625e0b1dcddc7b03e8bb9961 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Tue, 12 May 2026 23:52:42 +0300 Subject: [PATCH 16/22] fix: report VAT tax amount instead of gross revenue VAT amount = gross * rate / (100 + rate) since prices are VAT-inclusive. Co-Authored-By: Claude Sonnet 4.6 --- kompassi/tickets_v2/reports/sql/report_vat_by_month.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql b/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql index 7af646d99..3d16e950c 100644 --- a/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql +++ b/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql @@ -9,7 +9,7 @@ select 'YYYY-MM' ) as year_month, p.vat_percentage, - sum(p.price * pd.quantity::numeric) as gross_sales + sum(p.price * pd.quantity::numeric * p.vat_percentage / (100 + p.vat_percentage)) as vat_amount from tickets_v2_order o join lateral jsonb_each_text(o.product_data) as pd(product_id, quantity) on true From a5050b47dcb698202a85438f9f318c95c4301462 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Tue, 12 May 2026 23:53:29 +0300 Subject: [PATCH 17/22] fix: omit 0% VAT rate from VAT by month report Co-Authored-By: Claude Sonnet 4.6 --- kompassi/tickets_v2/reports/sql/report_vat_by_month.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql b/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql index 3d16e950c..2e2c8d3bf 100644 --- a/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql +++ b/kompassi/tickets_v2/reports/sql/report_vat_by_month.sql @@ -18,5 +18,6 @@ where o.event_id = %(event_id)s and o.cached_status = 3 and pd.quantity::int > 0 + and p.vat_percentage > 0 group by 1, 2 order by 1, 2 From 8b5318a93e2bcd7926efbeb28925e264cba3dc7b Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Wed, 13 May 2026 00:02:10 +0300 Subject: [PATCH 18/22] refactor(tickets_v2): tidy VAT by month report - 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 --- kompassi/tickets_v2/reports/vat_by_month.py | 68 ++++++++++----------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/kompassi/tickets_v2/reports/vat_by_month.py b/kompassi/tickets_v2/reports/vat_by_month.py index c0d48dc4d..3ca4539a6 100644 --- a/kompassi/tickets_v2/reports/vat_by_month.py +++ b/kompassi/tickets_v2/reports/vat_by_month.py @@ -11,14 +11,27 @@ from kompassi.reports.models.enums import TotalBy, TypeOfColumn from kompassi.reports.models.report import Report +from ..optimized_server.utils.formatting import format_vat_rate + SQL_DIR = Path(__file__).parent / "sql" +TITLE = dict( + fi="ALV-erittely kuukausittain", + en="VAT by month", + sv="Moms per månad", +) +MONTH_COLUMN_TITLE = dict(fi="Kuukausi", en="Month", sv="Månad") +TOTAL_COLUMN_TITLE = dict(fi="Yhteensä", en="Total", sv="Totalt") +FOOTER = dict( + fi="Vain maksetut tilaukset on laskettu mukaan. Hyvitykset eivät sisälly raporttiin.", + en="Only paid orders are included. Refunds are not reflected in this report.", + sv="Endast betalda beställningar ingår. Återbetalningar visas inte i rapporten.", +) +CENT = Decimal("0.01") + -def _format_vat_rate(rate: Decimal) -> dict[str, str]: - normalized = rate.normalize() - en = f"{normalized}%" - fi = f"{str(normalized).replace('.', ',')}%" - return dict(fi=fi, en=en) +def _vat_column_title(rate: Decimal) -> dict[str, str]: + return {lang: f"{format_vat_rate(rate, lang)}%" for lang in ("fi", "en", "sv")} class VatByMonth: @@ -30,68 +43,49 @@ def report(cls, event: Event, lang: str = DEFAULT_LANGUAGE) -> Report: cursor.execute(cls.query, dict(event_id=event.id, event_timezone=event.timezone_name)) raw_rows = cursor.fetchall() - if not raw_rows: - return Report( - slug="vat_by_month", - title=dict(fi="ALV-erittely kuukausittain", en="VAT by month"), - columns=[ - Column( - slug="month", - title=dict(fi="Kuukausi", en="Month"), - type=TypeOfColumn.STRING, - total_by=TotalBy.NONE, - ) - ], - rows=[], - lang=lang, - ) - - months: list[str] = sorted({row[0] for row in raw_rows}) vat_rates: list[Decimal] = sorted({row[1] for row in raw_rows}) + months: list[str] = sorted({row[0] for row in raw_rows}) data: dict[tuple[str, Decimal], Decimal] = {(row[0], row[1]): row[2] for row in raw_rows} columns: list[Column] = [ Column( slug="month", - title=dict(fi="Kuukausi", en="Month"), + title=MONTH_COLUMN_TITLE, type=TypeOfColumn.STRING, total_by=TotalBy.NONE, ), - *[ + *( Column( slug=f"vat_{rate}", - title=_format_vat_rate(rate), + title=_vat_column_title(rate), type=TypeOfColumn.CURRENCY, ) for rate in vat_rates - ], + ), Column( slug="total", - title=dict(fi="Yhteensä", en="Total"), + title=TOTAL_COLUMN_TITLE, type=TypeOfColumn.CURRENCY, ), ] rows: list[list] = [] for month in months: - row_total = Decimal(0) row: list = [month] + row_total = Decimal(0) for rate in vat_rates: - gross = data.get((month, rate), Decimal(0)) - row.append(float(gross)) - row_total += gross + vat = data.get((month, rate), Decimal(0)).quantize(CENT) + row.append(float(vat)) + row_total += vat row.append(float(row_total)) rows.append(row) return Report( slug="vat_by_month", - title=dict(fi="ALV-erittely kuukausittain", en="VAT by month"), + title=TITLE, columns=columns, rows=rows, - has_total_row=True, + has_total_row=bool(rows), lang=lang, - footer=dict( - fi="Vain maksetut tilaukset on laskettu mukaan. Hyvitykset eivät sisälly raporttiin.", - en="Only paid orders are included. Refunds are not reflected in this report.", - ), + footer=FOOTER, ) From 0959df543c7411b8bf450a71a56717ff4157afe1 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Wed, 13 May 2026 00:03:04 +0300 Subject: [PATCH 19/22] refactor(tickets_v2): centralize VAT breakdown calculation 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 --- kompassi/tickets_v2/models/order.py | 13 +----------- .../optimized_server/models/order.py | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/kompassi/tickets_v2/models/order.py b/kompassi/tickets_v2/models/order.py index c5859ffaa..fe5b9af61 100644 --- a/kompassi/tickets_v2/models/order.py +++ b/kompassi/tickets_v2/models/order.py @@ -92,18 +92,7 @@ def products(self) -> list[OrderProduct]: @cached_property def vat_breakdown(self) -> list[VatBreakdownLine]: - from collections import defaultdict - - totals: dict[Decimal, Decimal] = defaultdict(Decimal) - for op in self.products: - totals[op.vat_percentage] += op.price * op.quantity - result = [] - for rate in sorted(totals): - gross = totals[rate] - vat = (gross * rate / (100 + rate)).quantize(Decimal("0.01")) - net = gross - vat - result.append(VatBreakdownLine(rate=rate, gross=gross, vat=vat, net=net)) - return result + return VatBreakdownLine.from_order_products(self.products) @cached_property def etickets(self) -> list[Product]: diff --git a/kompassi/tickets_v2/optimized_server/models/order.py b/kompassi/tickets_v2/optimized_server/models/order.py index 120985ecb..06c518668 100644 --- a/kompassi/tickets_v2/optimized_server/models/order.py +++ b/kompassi/tickets_v2/optimized_server/models/order.py @@ -1,10 +1,11 @@ from __future__ import annotations import json +from collections import defaultdict from datetime import datetime from decimal import Decimal from pathlib import Path -from typing import Annotated, Any, ClassVar +from typing import Annotated, Any, ClassVar, Self from uuid import UUID import pydantic @@ -128,12 +129,29 @@ class OrderProduct(pydantic.BaseModel): vat_percentage: Decimal = pydantic.Field(serialization_alias="vatPercentage") +CENT = Decimal("0.01") + + class VatBreakdownLine(pydantic.BaseModel): rate: Decimal gross: Decimal vat: Decimal net: Decimal + @classmethod + def from_order_products(cls, order_products: list[OrderProduct]) -> list[Self]: + totals: dict[Decimal, Decimal] = defaultdict(Decimal) + for op in order_products: + totals[op.vat_percentage] += op.price * op.quantity + + breakdown: list[Self] = [] + for rate in sorted(totals): + gross = totals[rate] + vat = (gross * rate / (100 + rate)).quantize(CENT) + net = gross - vat + breakdown.append(cls(rate=rate, gross=gross, vat=vat, net=net)) + return breakdown + class Order(pydantic.BaseModel, populate_by_name=True): id: UUID From a39d866d947f183e71433a82141007ea0ce45104 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Wed, 13 May 2026 00:04:38 +0300 Subject: [PATCH 20/22] fix(tickets_v2): avoid scientific notation in format_vat_rate 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 --- .../tickets_v2/optimized_server/utils/formatting.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/kompassi/tickets_v2/optimized_server/utils/formatting.py b/kompassi/tickets_v2/optimized_server/utils/formatting.py index 0cbb5a0ae..41bc16f44 100644 --- a/kompassi/tickets_v2/optimized_server/utils/formatting.py +++ b/kompassi/tickets_v2/optimized_server/utils/formatting.py @@ -22,6 +22,9 @@ def format_vat_rate(rate: Decimal, language: str = "en") -> str: Format a VAT rate for display, stripping unnecessary trailing zeros. Uses comma as decimal separator for Finnish and Swedish locales. + Decimal.normalize() is unsuitable here because it produces scientific + notation for values like Decimal('10.00') -> Decimal('1E+1'). + >>> format_vat_rate(Decimal('25.50')) '25.5' >>> format_vat_rate(Decimal('25.50'), 'fi') @@ -32,10 +35,16 @@ def format_vat_rate(rate: Decimal, language: str = "en") -> str: '10' >>> format_vat_rate(Decimal('0')) '0' + >>> format_vat_rate(Decimal('0.00')) + '0' >>> format_vat_rate(Decimal('13.50'), 'sv') '13,5' + >>> format_vat_rate(Decimal('0.50'), 'fi') + '0,5' """ - result = str(rate.normalize()) + result = f"{rate:f}" + if "." in result: + result = result.rstrip("0").rstrip(".") if language in ("fi", "sv"): result = result.replace(".", ",") return result From 16a7f5395654264a7cdd340d26b3c5032aed22bf Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Wed, 13 May 2026 00:05:48 +0300 Subject: [PATCH 21/22] perf(tickets_v2): skip Order.get for zero-price orders in create_order 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 --- kompassi/tickets_v2/optimized_server/app.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/kompassi/tickets_v2/optimized_server/app.py b/kompassi/tickets_v2/optimized_server/app.py index be4968bbc..1a4432a1d 100644 --- a/kompassi/tickets_v2/optimized_server/app.py +++ b/kompassi/tickets_v2/optimized_server/app.py @@ -13,7 +13,7 @@ from .excs import InvalidProducts, NotEnoughTickets, ProviderCannot, UnsaneSituation from .models.enums import PaymentStampType from .models.event import Event -from .models.order import CreateOrderRequest, Order, OrderWithCustomer +from .models.order import CreateOrderRequest, Order, OrderProduct, OrderWithCustomer from .models.product import Product from .providers.paytrail import PaymentCallback @@ -104,10 +104,15 @@ async def create_order( try: async with db.transaction(): result = await order.save(db, event.id) - fetched_order = await Order.get(db, event.id, result.order_id) - if fetched_order is None: - raise UnsaneSituation("Order not found after creation") - request, request_stamp = provider.prepare_for_new_order(order, result, fetched_order.products) + # Providers short-circuit zero-price orders without using products, + # so skip the extra Order.get round trip in that case. + order_products: list[OrderProduct] = [] + if result.total_price > 0: + fetched_order = await Order.get(db, event.id, result.order_id) + if fetched_order is None: + raise UnsaneSituation("Order not found after creation") + order_products = fetched_order.products + request, request_stamp = provider.prepare_for_new_order(order, result, order_products) await request_stamp.save(db) except NotEnoughTickets as e: raise HTTPException(409, "NOT_ENOUGH_TICKETS") from e From 11e516434e8feaccbf0d524dfde6ca43f4228d90 Mon Sep 17 00:00:00 2001 From: Luka Pajukanta Date: Wed, 13 May 2026 00:16:17 +0300 Subject: [PATCH 22/22] test(tickets_v2): add VAT branch unit and integration tests 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 --- kompassi/tickets_v2/reports/vat_by_month.py | 5 +- kompassi/tickets_v2/tests.py | 256 +++++++++++++++++++- 2 files changed, 258 insertions(+), 3 deletions(-) diff --git a/kompassi/tickets_v2/reports/vat_by_month.py b/kompassi/tickets_v2/reports/vat_by_month.py index 3ca4539a6..a6f493858 100644 --- a/kompassi/tickets_v2/reports/vat_by_month.py +++ b/kompassi/tickets_v2/reports/vat_by_month.py @@ -8,7 +8,7 @@ from kompassi.core.models.event import Event from kompassi.graphql_api.language import DEFAULT_LANGUAGE from kompassi.reports.models.column import Column -from kompassi.reports.models.enums import TotalBy, TypeOfColumn +from kompassi.reports.models.enums import TypeOfColumn from kompassi.reports.models.report import Report from ..optimized_server.utils.formatting import format_vat_rate @@ -52,7 +52,8 @@ def report(cls, event: Event, lang: str = DEFAULT_LANGUAGE) -> Report: slug="month", title=MONTH_COLUMN_TITLE, type=TypeOfColumn.STRING, - total_by=TotalBy.NONE, + # NOTE: total_by defaults to SUM, which is what makes the total + # row label show "Total" in column 0 (see Report.get_total_row). ), *( Column( diff --git a/kompassi/tickets_v2/tests.py b/kompassi/tickets_v2/tests.py index b1d59c85e..9c7f137ef 100644 --- a/kompassi/tickets_v2/tests.py +++ b/kompassi/tickets_v2/tests.py @@ -1,4 +1,6 @@ import json +from datetime import UTC, datetime +from decimal import Decimal from functools import cached_property from typing import Any, Literal from uuid import UUID, uuid4 @@ -8,13 +10,17 @@ import requests from kompassi.core.models.event import Event +from kompassi.tickets_v2.models.order import Order +from kompassi.tickets_v2.models.product import Product from kompassi.tickets_v2.optimized_server.models.api import CreateOrderResponse, GetOrderResponse, GetProductsResponse from kompassi.tickets_v2.optimized_server.models.customer import Customer from kompassi.tickets_v2.optimized_server.models.enums import PaymentStatus -from kompassi.tickets_v2.optimized_server.models.order import CreateOrderRequest +from kompassi.tickets_v2.optimized_server.models.order import CreateOrderRequest, OrderProduct, VatBreakdownLine from kompassi.tickets_v2.optimized_server.providers.paytrail import PaymentCallback, PaytrailStatus +from kompassi.tickets_v2.optimized_server.utils.formatting import format_vat_rate from kompassi.tickets_v2.optimized_server.utils.paytrail_hmac import calculate_hmac from kompassi.tickets_v2.optimized_server.utils.uuid7 import uuid7 +from kompassi.tickets_v2.reports.vat_by_month import VatByMonth PAYTRAIL_TEST_ACCOUNT = "375917" PAYTRAIL_TEST_SECRET = "SAIPPUAKAUPPIAS" @@ -234,3 +240,251 @@ def test_make_order(tickets_v2_client: TicketsV2Client): # user redirected to order page again get_order_response = tickets_v2_client.get_order(order_response.order_id) assert get_order_response.order.status == PaymentStatus.PAID + + +# --------------------------------------------------------------------------- +# VAT formatting / breakdown unit tests +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "rate,expected_en,expected_fi", + [ + (Decimal("0"), "0", "0"), + (Decimal("0.00"), "0", "0"), + (Decimal("10.00"), "10", "10"), + (Decimal("13.50"), "13.5", "13,5"), + (Decimal("14.00"), "14", "14"), + (Decimal("25.50"), "25.5", "25,5"), + (Decimal("0.50"), "0.5", "0,5"), + (Decimal("99.99"), "99.99", "99,99"), + ], +) +def test_format_vat_rate(rate, expected_en, expected_fi): + """ + Regression test: Decimal('10.00').normalize() returns Decimal('1E+1'), + so a naive str(rate.normalize()) implementation would render '1E+1' + instead of '10' in receipts. + """ + assert format_vat_rate(rate, "en") == expected_en + assert format_vat_rate(rate, "fi") == expected_fi + # Swedish uses the same decimal separator as Finnish + assert format_vat_rate(rate, "sv") == expected_fi + + +def test_vat_breakdown_empty(): + assert VatBreakdownLine.from_order_products([]) == [] + + +def test_vat_breakdown_sums_per_rate_and_returns_sorted(): + products = [ + OrderProduct(title="A", price=Decimal("125.50"), quantity=2, vat_percentage=Decimal("25.50")), + OrderProduct(title="B", price=Decimal("114.00"), quantity=1, vat_percentage=Decimal("14.00")), + OrderProduct(title="C", price=Decimal("125.50"), quantity=1, vat_percentage=Decimal("25.50")), + ] + breakdown = VatBreakdownLine.from_order_products(products) + + # Two rates present, sorted ascending. + assert [line.rate for line in breakdown] == [Decimal("14.00"), Decimal("25.50")] + + # 14% line: 1 * 114 = 114 gross, VAT = 114 * 14/114 = 14 + assert breakdown[0].gross == Decimal("114.00") + assert breakdown[0].vat == Decimal("14.00") + assert breakdown[0].net == Decimal("100.00") + + # 25.5% line: 3 * 125.50 = 376.50 gross, VAT = 376.50 * 25.5/125.5 = 76.50 + assert breakdown[1].gross == Decimal("376.50") + assert breakdown[1].vat == Decimal("76.50") + assert breakdown[1].net == Decimal("300.00") + + +def test_vat_breakdown_zero_rate_kept(): + """ + The breakdown itself preserves 0% rates (used in receipts to itemize + zero-rated lines). Filtering of 0% is the VAT-by-month report's concern. + """ + products = [ + OrderProduct(title="Free", price=Decimal("5.00"), quantity=1, vat_percentage=Decimal("0")), + ] + breakdown = VatBreakdownLine.from_order_products(products) + assert len(breakdown) == 1 + assert breakdown[0].rate == Decimal("0") + assert breakdown[0].gross == Decimal("5.00") + assert breakdown[0].vat == Decimal("0.00") + assert breakdown[0].net == Decimal("5.00") + + +# --------------------------------------------------------------------------- +# VAT-by-month report integration tests +# --------------------------------------------------------------------------- + + +def _make_order( + event: Event, + when: datetime, + product_data: dict[int, int], + status: PaymentStatus = PaymentStatus.PAID, +) -> UUID: + """ + Insert a tickets_v2_order row directly via SQL. + + Bypasses the ORM because tickets_v2_order.order_number is GENERATED ALWAYS + AS IDENTITY at the DB layer, so Django can't INSERT through the column; + the production code path goes through create_order.sql for the same reason. + Returns the order id. + """ + from django.db import connection + + order_id = uuid7(when) + with connection.cursor() as cursor: + cursor.execute( + """ + insert into tickets_v2_order + (id, event_id, cached_status, cached_price, language, product_data, + first_name, last_name, email, phone) + values + (%s, %s, %s, %s, %s, %s::jsonb, %s, %s, %s, %s) + """, + [ + str(order_id), + event.id, + status.value, + "0", + "en", + json.dumps({str(pid): qty for pid, qty in product_data.items()}), + "Test", + "Customer", + "test@example.com", + "", + ], + ) + return order_id + + +@pytest.fixture +def vat_report_event(db): + event, _ = Event.get_or_create_dummy(name="VAT report test event") + # Sanity-check the timezone the report relies on for month bucketing. + assert event.timezone_name == "Europe/Helsinki" + Order.ensure_partition(event) + return event + + +@pytest.mark.django_db +def test_vat_by_month_report_empty(vat_report_event: Event): + """No orders → no rows, but the report still renders with a month column.""" + report = VatByMonth.report(vat_report_event, "en") + assert report.slug == "vat_by_month" + assert report.rows == [] + assert [c.slug for c in report.columns] == ["month", "total"] + assert report.total_row is None # has_total_row is False when there are no rows + + +@pytest.mark.django_db +def test_vat_by_month_report_basic(vat_report_event: Event): + """ + Multiple VAT rates across multiple months, with an unpaid order and a + zero-VAT order included to verify both filters. + """ + # Prices chosen so VAT comes out to clean Decimals: + # - 125.50 @ 25.5% → VAT/unit = 125.50 * 25.5 / 125.5 = 25.50 + # - 114.00 @ 14% → VAT/unit = 114.00 * 14 / 114 = 14.00 + standard = Product.objects.create( + event=vat_report_event, + title="Standard product", + description="", + price=Decimal("125.50"), + vat_percentage=Decimal("25.50"), + ) + reduced = Product.objects.create( + event=vat_report_event, + title="Food product", + description="", + price=Decimal("114.00"), + vat_percentage=Decimal("14.00"), + ) + zero_rated = Product.objects.create( + event=vat_report_event, + title="Free product", + description="", + price=Decimal("5.00"), + vat_percentage=Decimal("0"), + ) + + # Jan 2026 (Helsinki): 1× standard → 25.5% column = 25.50 + _make_order(vat_report_event, datetime(2026, 1, 15, 12, 0, tzinfo=UTC), {standard.id: 1}) + # Feb 2026 (Helsinki): 1× standard + 2× reduced → 25.5% = 25.50, 14% = 28.00 + _make_order(vat_report_event, datetime(2026, 2, 10, 12, 0, tzinfo=UTC), {standard.id: 1, reduced.id: 2}) + # Unpaid Feb order (must be excluded) + _make_order( + vat_report_event, + datetime(2026, 2, 20, 12, 0, tzinfo=UTC), + {standard.id: 1}, + status=PaymentStatus.PENDING, + ) + # Paid zero-VAT Feb order (must be excluded) + _make_order(vat_report_event, datetime(2026, 2, 25, 12, 0, tzinfo=UTC), {zero_rated.id: 1}) + + report = VatByMonth.report(vat_report_event, "en") + + column_slugs = [c.slug for c in report.columns] + assert column_slugs == ["month", "vat_14.00", "vat_25.50", "total"] + + assert len(report.rows) == 2 + jan, feb = report.rows + assert jan == ["2026-01", 0.00, 25.50, 25.50] + assert feb == ["2026-02", 28.00, 25.50, 53.50] + + # Total row sums each column (skipping the month column, which is total_by=NONE). + assert report.total_row is not None + month_label, total_14, total_25_5, grand_total = report.total_row + assert month_label == "Total" + assert total_14 == pytest.approx(28.00) + assert total_25_5 == pytest.approx(51.00) + assert grand_total == pytest.approx(79.00) + + +@pytest.mark.django_db +def test_vat_by_month_report_timezone_boundary(vat_report_event: Event): + """ + An order at 23:30 UTC on Jan 31 falls on Feb 1 in Helsinki (UTC+2), + so the report should bucket it in February. + """ + product = Product.objects.create( + event=vat_report_event, + title="Standard", + description="", + price=Decimal("125.50"), + vat_percentage=Decimal("25.50"), + ) + # UTC moment that is already in February when projected into Europe/Helsinki. + _make_order(vat_report_event, datetime(2026, 1, 31, 23, 30, tzinfo=UTC), {product.id: 1}) + + report = VatByMonth.report(vat_report_event, "en") + assert [row[0] for row in report.rows] == ["2026-02"] + + +@pytest.mark.django_db +def test_vat_by_month_report_localized_titles(vat_report_event: Event): + """Column titles for VAT rates follow the requested locale's separator.""" + Product.objects.create( + event=vat_report_event, + title="Standard", + description="", + price=Decimal("125.50"), + vat_percentage=Decimal("25.50"), + ) + _make_order( + vat_report_event, + datetime(2026, 3, 15, 12, 0, tzinfo=UTC), + {vat_report_event.products.get().id: 1}, + ) + + en_report = VatByMonth.report(vat_report_event, "en") + fi_report = VatByMonth.report(vat_report_event, "fi") + # The Column.title is a dict that resolve_localized_field picks from at GraphQL + # serialization time; we just verify both locales are populated correctly. + vat_col_en = next(c for c in en_report.columns if c.slug == "vat_25.50") + vat_col_fi = next(c for c in fi_report.columns if c.slug == "vat_25.50") + assert vat_col_en.title["en"] == "25.5%" + assert vat_col_fi.title["fi"] == "25,5%"