feat: add launch readiness features for OMS

- Implement VAT tax calculation on order creation based on EU country rates
- Add post-order hooks: customer stats update, cart clear, email confirmation
- Create shop order history page with pagination and status badges
- Create shop order detail page with order items and addresses
- Add order_confirmation email templates in 4 languages (en, fr, de, lb)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 19:37:17 +01:00
parent 8fd8168ff4
commit 098f008dfd
5 changed files with 953 additions and 9 deletions

View File

@@ -18,6 +18,7 @@ See docs/architecture/money-handling.md for details.
import logging
import random
import string
from decimal import Decimal
from datetime import UTC, datetime
from typing import Any
@@ -37,6 +38,38 @@ from app.services.subscription_service import (
TierLimitExceededException,
)
from app.utils.money import Money, cents_to_euros, euros_to_cents
# EU VAT rates by country code (2024 standard rates)
# Duplicated from invoice_service to avoid circular imports
EU_VAT_RATES: dict[str, Decimal] = {
"AT": Decimal("20.00"), # Austria
"BE": Decimal("21.00"), # Belgium
"BG": Decimal("20.00"), # Bulgaria
"HR": Decimal("25.00"), # Croatia
"CY": Decimal("19.00"), # Cyprus
"CZ": Decimal("21.00"), # Czech Republic
"DK": Decimal("25.00"), # Denmark
"EE": Decimal("22.00"), # Estonia
"FI": Decimal("24.00"), # Finland
"FR": Decimal("20.00"), # France
"DE": Decimal("19.00"), # Germany
"GR": Decimal("24.00"), # Greece
"HU": Decimal("27.00"), # Hungary
"IE": Decimal("23.00"), # Ireland
"IT": Decimal("22.00"), # Italy
"LV": Decimal("21.00"), # Latvia
"LT": Decimal("21.00"), # Lithuania
"LU": Decimal("17.00"), # Luxembourg (standard)
"MT": Decimal("18.00"), # Malta
"NL": Decimal("21.00"), # Netherlands
"PL": Decimal("23.00"), # Poland
"PT": Decimal("23.00"), # Portugal
"RO": Decimal("19.00"), # Romania
"SK": Decimal("20.00"), # Slovakia
"SI": Decimal("22.00"), # Slovenia
"ES": Decimal("21.00"), # Spain
"SE": Decimal("25.00"), # Sweden
}
from models.database.customer import Customer
from models.database.marketplace_product import MarketplaceProduct
from models.database.marketplace_product_translation import MarketplaceProductTranslation
@@ -87,6 +120,50 @@ class OrderService:
return order_number
# =========================================================================
# Tax Calculation
# =========================================================================
def _calculate_tax_for_order(
self, subtotal_cents: int, shipping_country_iso: str
) -> tuple[int, Decimal]:
"""
Calculate tax amount for an order based on shipping destination.
Uses EU VAT rates based on destination country. For B2C orders,
the tax is included in the product price, but we need to calculate
the tax component for reporting.
For Luxembourg vendors selling to EU countries:
- LU domestic: 17% VAT
- Other EU countries: destination country VAT rate
- Non-EU: 0% (VAT exempt)
Args:
subtotal_cents: Order subtotal in cents (before tax)
shipping_country_iso: ISO 2-letter country code
Returns:
tuple: (tax_amount_cents, vat_rate)
"""
country = shipping_country_iso.upper() if shipping_country_iso else "LU"
# Get VAT rate for destination country (0% if non-EU)
vat_rate = EU_VAT_RATES.get(country, Decimal("0.00"))
if vat_rate == Decimal("0.00"):
return 0, vat_rate
# Calculate tax: tax = subtotal * (rate / 100)
# Using Decimal for precision, then converting to cents
subtotal_decimal = Decimal(str(subtotal_cents))
tax_decimal = subtotal_decimal * (vat_rate / Decimal("100"))
# Round to nearest cent
tax_amount_cents = int(round(tax_decimal))
return tax_amount_cents, vat_rate
# =========================================================================
# Placeholder Product Management
# =========================================================================
@@ -369,7 +446,9 @@ class OrderService:
)
# Calculate totals in cents
tax_amount_cents = 0 # TODO: Implement tax calculation
tax_amount_cents, vat_rate = self._calculate_tax_for_order(
subtotal_cents, order_data.shipping_address.country_iso
)
shipping_amount_cents = 599 if subtotal_cents < 5000 else 0 # €5.99 / €50
discount_amount_cents = 0
total_amount_cents = Money.calculate_order_total(