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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user