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:
@@ -12,6 +12,7 @@ Customer Context: get_current_customer_api returns Customer directly
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -19,7 +20,10 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_customer_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import VendorNotFoundException
|
||||
from app.services.cart_service import cart_service
|
||||
from app.services.email_service import EmailService
|
||||
from app.services.order_service import order_service
|
||||
from app.utils.money import cents_to_euros
|
||||
from models.database.customer import Customer
|
||||
from models.schema.order import (
|
||||
OrderCreate,
|
||||
@@ -81,9 +85,53 @@ def place_order(
|
||||
},
|
||||
)
|
||||
|
||||
# TODO: Update customer stats
|
||||
# TODO: Clear cart
|
||||
# TODO: Send order confirmation email
|
||||
# Update customer stats
|
||||
customer.total_orders = (customer.total_orders or 0) + 1
|
||||
customer.total_spent = (customer.total_spent or 0) + order.total_amount
|
||||
customer.last_order_date = datetime.now(UTC)
|
||||
db.flush()
|
||||
|
||||
logger.debug(
|
||||
f"Updated customer stats: total_orders={customer.total_orders}, "
|
||||
f"total_spent={customer.total_spent}"
|
||||
)
|
||||
|
||||
# Clear cart (get session_id from request cookies or headers)
|
||||
session_id = request.cookies.get("cart_session_id") or request.headers.get(
|
||||
"X-Cart-Session-Id"
|
||||
)
|
||||
if session_id:
|
||||
try:
|
||||
cart_service.clear_cart(db, vendor.id, session_id)
|
||||
logger.debug(f"Cleared cart for session {session_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to clear cart: {e}")
|
||||
|
||||
# Send order confirmation email
|
||||
try:
|
||||
email_service = EmailService(db)
|
||||
email_service.send_template(
|
||||
template_code="order_confirmation",
|
||||
to_email=customer.email,
|
||||
to_name=customer.full_name,
|
||||
language=customer.preferred_language or "en",
|
||||
variables={
|
||||
"customer_name": customer.first_name or customer.full_name,
|
||||
"order_number": order.order_number,
|
||||
"order_total": f"€{order.total_amount:.2f}",
|
||||
"order_items_count": len(order.items),
|
||||
"order_date": order.order_date.strftime("%d.%m.%Y")
|
||||
if order.order_date
|
||||
else "",
|
||||
"shipping_address": f"{order.ship_address_line_1}, {order.ship_postal_code} {order.ship_city}",
|
||||
},
|
||||
vendor_id=vendor.id,
|
||||
related_type="order",
|
||||
related_id=order.id,
|
||||
)
|
||||
logger.info(f"Sent order confirmation email to {customer.email}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send order confirmation email: {e}")
|
||||
|
||||
return OrderResponse.model_validate(order)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
322
app/templates/shop/account/order-detail.html
Normal file
322
app/templates/shop/account/order-detail.html
Normal file
@@ -0,0 +1,322 @@
|
||||
{# app/templates/shop/account/order-detail.html #}
|
||||
{% extends "shop/base.html" %}
|
||||
|
||||
{% block title %}Order Details - {{ vendor.name }}{% endblock %}
|
||||
|
||||
{% block alpine_data %}shopOrderDetailPage(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-6" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<li>
|
||||
<a href="{{ base_url }}shop/account/dashboard" class="hover:text-primary">My Account</a>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<svg class="h-4 w-4 mx-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<a href="{{ base_url }}shop/account/orders" class="hover:text-primary">Orders</a>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<svg class="h-4 w-4 mx-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="text-gray-900 dark:text-white" x-text="order?.order_number || 'Order Details'"></span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary" style="border-color: var(--color-primary)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div x-show="error && !loading" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6">
|
||||
<div class="flex">
|
||||
<svg class="h-6 w-6 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-lg font-medium text-red-800 dark:text-red-200">Error loading order</h3>
|
||||
<p class="mt-1 text-sm text-red-700 dark:text-red-300" x-text="error"></p>
|
||||
<a href="{{ base_url }}shop/account/orders"
|
||||
class="mt-4 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700">
|
||||
Back to Orders
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Content -->
|
||||
<div x-show="!loading && !error && order" x-cloak>
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white" x-text="'Order ' + order.order_number"></h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
||||
Placed on <span x-text="formatDateTime(order.order_date || order.created_at)"></span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||
:class="getStatusClass(order.status)">
|
||||
<span x-text="getStatusLabel(order.status)"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Main Content (Left Column - 2/3) -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
|
||||
<!-- Order Items -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Order Items</h2>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<template x-for="item in order.items" :key="item.id">
|
||||
<div class="px-6 py-4 flex items-center gap-4">
|
||||
<!-- Product Image Placeholder -->
|
||||
<div class="flex-shrink-0 w-16 h-16 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
||||
<svg class="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Product Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white" x-text="item.product_name"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
SKU: <span x-text="item.product_sku || '-'"></span>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Qty: <span x-text="item.quantity"></span>
|
||||
×
|
||||
<span x-text="formatPrice(item.unit_price)"></span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Line Total -->
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-white" x-text="formatPrice(item.total_price)"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Addresses -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Shipping Address -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<svg class="h-5 w-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Shipping Address
|
||||
</h3>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||
<p class="font-medium" x-text="(order.ship_first_name || '') + ' ' + (order.ship_last_name || '')"></p>
|
||||
<p x-show="order.ship_company" x-text="order.ship_company"></p>
|
||||
<p x-text="order.ship_address_line_1"></p>
|
||||
<p x-show="order.ship_address_line_2" x-text="order.ship_address_line_2"></p>
|
||||
<p x-text="(order.ship_postal_code || '') + ' ' + (order.ship_city || '')"></p>
|
||||
<p x-text="order.ship_country_iso"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Address -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<svg class="h-5 w-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Billing Address
|
||||
</h3>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||
<p class="font-medium" x-text="(order.bill_first_name || '') + ' ' + (order.bill_last_name || '')"></p>
|
||||
<p x-show="order.bill_company" x-text="order.bill_company"></p>
|
||||
<p x-text="order.bill_address_line_1"></p>
|
||||
<p x-show="order.bill_address_line_2" x-text="order.bill_address_line_2"></p>
|
||||
<p x-text="(order.bill_postal_code || '') + ' ' + (order.bill_city || '')"></p>
|
||||
<p x-text="order.bill_country_iso"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Notes -->
|
||||
<div x-show="order.customer_notes" class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<svg class="h-5 w-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
||||
</svg>
|
||||
Order Notes
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300" x-text="order.customer_notes"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar (Right Column - 1/3) -->
|
||||
<div class="space-y-6">
|
||||
<!-- Order Summary -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Order Summary</h3>
|
||||
<dl class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<dt class="text-gray-500 dark:text-gray-400">Subtotal</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white" x-text="formatPrice(order.subtotal)"></dd>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<dt class="text-gray-500 dark:text-gray-400">Shipping</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white" x-text="formatPrice(order.shipping_amount)"></dd>
|
||||
</div>
|
||||
<div x-show="order.tax_amount > 0" class="flex justify-between text-sm">
|
||||
<dt class="text-gray-500 dark:text-gray-400">Tax</dt>
|
||||
<dd class="font-medium text-gray-900 dark:text-white" x-text="formatPrice(order.tax_amount)"></dd>
|
||||
</div>
|
||||
<div x-show="order.discount_amount > 0" class="flex justify-between text-sm">
|
||||
<dt class="text-gray-500 dark:text-gray-400">Discount</dt>
|
||||
<dd class="font-medium text-green-600 dark:text-green-400" x-text="'-' + formatPrice(order.discount_amount)"></dd>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-3 flex justify-between">
|
||||
<dt class="text-base font-semibold text-gray-900 dark:text-white">Total</dt>
|
||||
<dd class="text-base font-bold text-gray-900 dark:text-white" x-text="formatPrice(order.total_amount)"></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Info -->
|
||||
<div x-show="order.shipping_method || order.tracking_number" class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||||
<svg class="h-5 w-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
Shipping
|
||||
</h3>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div x-show="order.shipping_method">
|
||||
<p class="text-gray-500 dark:text-gray-400">Method</p>
|
||||
<p class="font-medium text-gray-900 dark:text-white" x-text="order.shipping_method"></p>
|
||||
</div>
|
||||
<div x-show="order.tracking_number">
|
||||
<p class="text-gray-500 dark:text-gray-400">Tracking Number</p>
|
||||
<p class="font-medium text-gray-900 dark:text-white" x-text="order.tracking_number"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Need Help? -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Need Help?</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||
If you have any questions about your order, please contact us.
|
||||
</p>
|
||||
<a href="{{ base_url }}shop/account/messages"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors"
|
||||
style="background-color: var(--color-primary)">
|
||||
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
Contact Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="mt-8">
|
||||
<a href="{{ base_url }}shop/account/orders"
|
||||
class="inline-flex items-center text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-primary">
|
||||
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Orders
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function shopOrderDetailPage() {
|
||||
return {
|
||||
...shopLayoutData(),
|
||||
|
||||
// State
|
||||
order: null,
|
||||
loading: true,
|
||||
error: '',
|
||||
orderId: {{ order_id }},
|
||||
|
||||
// Status mapping
|
||||
statuses: {
|
||||
pending: { label: 'Pending', class: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
|
||||
processing: { label: 'Processing', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
|
||||
partially_shipped: { label: 'Partially Shipped', class: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200' },
|
||||
shipped: { label: 'Shipped', class: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200' },
|
||||
delivered: { label: 'Delivered', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||
completed: { label: 'Completed', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||
cancelled: { label: 'Cancelled', class: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
||||
refunded: { label: 'Refunded', class: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' }
|
||||
},
|
||||
|
||||
async init() {
|
||||
await this.loadOrder();
|
||||
},
|
||||
|
||||
async loadOrder() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/shop/orders/${this.orderId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
throw new Error('Failed to load order details');
|
||||
}
|
||||
|
||||
this.order = await response.json();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading order:', err);
|
||||
this.error = err.message || 'Failed to load order';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
getStatusLabel(status) {
|
||||
return this.statuses[status]?.label || status;
|
||||
},
|
||||
|
||||
getStatusClass(status) {
|
||||
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
|
||||
},
|
||||
|
||||
formatPrice(amount) {
|
||||
if (!amount && amount !== 0) return '-';
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
formatDateTime(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,15 +1,223 @@
|
||||
{# app/templates/shop/account/orders.html #}
|
||||
{% extends "shop/base.html" %}
|
||||
|
||||
{% block title %}Order History{% endblock %}
|
||||
{% block title %}Order History - {{ vendor.name }}{% endblock %}
|
||||
|
||||
{% block alpine_data %}shopOrdersPage(){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">Order History</h1>
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-6" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<li>
|
||||
<a href="{{ base_url }}shop/account/dashboard" class="hover:text-primary">My Account</a>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<svg class="h-4 w-4 mx-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span class="text-gray-900 dark:text-white">Order History</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{# TODO: Implement order history #}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<p class="text-gray-600 dark:text-gray-400">Order history coming soon...</p>
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Order History</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">View and track your orders</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary" style="border-color: var(--color-primary)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div x-show="error && !loading" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
|
||||
<div class="flex">
|
||||
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<p class="ml-3 text-sm text-red-700 dark:text-red-300" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div x-show="!loading && !error && orders.length === 0"
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-12 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No orders yet</h3>
|
||||
<p class="mt-2 text-gray-500 dark:text-gray-400">Start shopping to see your orders here.</p>
|
||||
<a href="{{ base_url }}shop/products"
|
||||
class="mt-6 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark transition-colors"
|
||||
style="background-color: var(--color-primary)">
|
||||
Browse Products
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Orders List -->
|
||||
<div x-show="!loading && !error && orders.length > 0" class="space-y-4">
|
||||
<template x-for="order in orders" :key="order.id">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<!-- Order Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex flex-wrap items-center gap-6">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Order Number</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white" x-text="order.order_number"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Date</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white" x-text="formatDate(order.order_date || order.created_at)"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Total</p>
|
||||
<p class="text-sm font-bold text-gray-900 dark:text-white" x-text="formatPrice(order.total_amount)"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Status Badge -->
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="getStatusClass(order.status)">
|
||||
<span x-text="getStatusLabel(order.status)"></span>
|
||||
</span>
|
||||
<!-- View Details Button -->
|
||||
<a :href="'{{ base_url }}shop/account/orders/' + order.id"
|
||||
class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
|
||||
View Details
|
||||
<svg class="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items Preview -->
|
||||
<div class="px-6 py-4">
|
||||
<template x-for="item in order.items.slice(0, 3)" :key="item.id">
|
||||
<div class="flex items-center py-2 border-b border-gray-100 dark:border-gray-700 last:border-0">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white truncate" x-text="item.product_name"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Qty: <span x-text="item.quantity"></span> ×
|
||||
<span x-text="formatPrice(item.unit_price)"></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="ml-4 text-sm font-medium text-gray-900 dark:text-white" x-text="formatPrice(item.total_price)"></p>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="order.items.length > 3"
|
||||
class="text-sm text-gray-500 dark:text-gray-400 mt-2"
|
||||
x-text="'+ ' + (order.items.length - 3) + ' more item(s)'"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div x-show="totalPages > 1" class="flex justify-center mt-8">
|
||||
<nav class="flex items-center space-x-2">
|
||||
<button @click="loadOrders(currentPage - 1)"
|
||||
:disabled="currentPage === 1"
|
||||
class="px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Previous
|
||||
</button>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
Page <span x-text="currentPage"></span> of <span x-text="totalPages"></span>
|
||||
</span>
|
||||
<button @click="loadOrders(currentPage + 1)"
|
||||
:disabled="currentPage === totalPages"
|
||||
class="px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Next
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function shopOrdersPage() {
|
||||
return {
|
||||
...shopLayoutData(),
|
||||
|
||||
// State
|
||||
orders: [],
|
||||
loading: true,
|
||||
error: '',
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
perPage: 10,
|
||||
|
||||
// Status mapping
|
||||
statuses: {
|
||||
pending: { label: 'Pending', class: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
|
||||
processing: { label: 'Processing', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
|
||||
partially_shipped: { label: 'Partially Shipped', class: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200' },
|
||||
shipped: { label: 'Shipped', class: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200' },
|
||||
delivered: { label: 'Delivered', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||
completed: { label: 'Completed', class: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||
cancelled: { label: 'Cancelled', class: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
||||
refunded: { label: 'Refunded', class: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' }
|
||||
},
|
||||
|
||||
async init() {
|
||||
await this.loadOrders(1);
|
||||
},
|
||||
|
||||
async loadOrders(page = 1) {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const skip = (page - 1) * this.perPage;
|
||||
const response = await fetch(`/api/v1/shop/orders?skip=${skip}&limit=${this.perPage}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load orders');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.orders = data.orders || [];
|
||||
this.currentPage = page;
|
||||
this.totalPages = Math.ceil((data.total || 0) / this.perPage);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading orders:', err);
|
||||
this.error = err.message || 'Failed to load orders';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
getStatusLabel(status) {
|
||||
return this.statuses[status]?.label || status;
|
||||
},
|
||||
|
||||
getStatusClass(status) {
|
||||
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
|
||||
},
|
||||
|
||||
formatPrice(amount) {
|
||||
if (!amount && amount !== 0) return '-';
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -366,6 +366,293 @@ Fir unzefänken:
|
||||
|
||||
Mat beschte Gréiss,
|
||||
D'Wizamart Team
|
||||
""",
|
||||
},
|
||||
# -------------------------------------------------------------------------
|
||||
# ORDER CONFIRMATION
|
||||
# -------------------------------------------------------------------------
|
||||
{
|
||||
"code": "order_confirmation",
|
||||
"language": "en",
|
||||
"name": "Order Confirmation",
|
||||
"description": "Sent to customers after placing an order",
|
||||
"category": EmailCategory.ORDERS.value,
|
||||
"variables": json.dumps([
|
||||
"customer_name", "order_number", "order_total",
|
||||
"order_items_count", "order_date", "shipping_address"
|
||||
]),
|
||||
"subject": "Order Confirmation - {{ order_number }}",
|
||||
"body_html": """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 28px;">Order Confirmed!</h1>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">Hi {{ customer_name }},</p>
|
||||
|
||||
<p>Thank you for your order! We've received your order and it's being processed.</p>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981;">
|
||||
<h3 style="margin-top: 0; color: #10b981;">Order Details</h3>
|
||||
<p style="margin: 5px 0;"><strong>Order Number:</strong> {{ order_number }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Date:</strong> {{ order_date }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Items:</strong> {{ order_items_count }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Total:</strong> {{ order_total }}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0; color: #374151;">Shipping Address</h3>
|
||||
<p style="margin: 5px 0;">{{ shipping_address }}</p>
|
||||
</div>
|
||||
|
||||
<p>You will receive another email when your order ships.</p>
|
||||
|
||||
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
|
||||
If you have any questions about your order, please contact us.
|
||||
</p>
|
||||
|
||||
<p>Thank you for shopping with us!<br><strong>The Team</strong></p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding: 20px; color: #9ca3af; font-size: 12px;">
|
||||
<p>This is an automated email. Please do not reply directly.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>""",
|
||||
"body_text": """Order Confirmed!
|
||||
|
||||
Hi {{ customer_name }},
|
||||
|
||||
Thank you for your order! We've received your order and it's being processed.
|
||||
|
||||
Order Details:
|
||||
- Order Number: {{ order_number }}
|
||||
- Date: {{ order_date }}
|
||||
- Items: {{ order_items_count }}
|
||||
- Total: {{ order_total }}
|
||||
|
||||
Shipping Address:
|
||||
{{ shipping_address }}
|
||||
|
||||
You will receive another email when your order ships.
|
||||
|
||||
Thank you for shopping with us!
|
||||
""",
|
||||
},
|
||||
{
|
||||
"code": "order_confirmation",
|
||||
"language": "fr",
|
||||
"name": "Confirmation de commande",
|
||||
"description": "Envoyé aux clients après avoir passé une commande",
|
||||
"category": EmailCategory.ORDERS.value,
|
||||
"variables": json.dumps([
|
||||
"customer_name", "order_number", "order_total",
|
||||
"order_items_count", "order_date", "shipping_address"
|
||||
]),
|
||||
"subject": "Confirmation de commande - {{ order_number }}",
|
||||
"body_html": """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 28px;">Commande confirmée !</h1>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">Bonjour {{ customer_name }},</p>
|
||||
|
||||
<p>Merci pour votre commande ! Nous l'avons bien reçue et elle est en cours de traitement.</p>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981;">
|
||||
<h3 style="margin-top: 0; color: #10b981;">Détails de la commande</h3>
|
||||
<p style="margin: 5px 0;"><strong>Numéro de commande :</strong> {{ order_number }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Date :</strong> {{ order_date }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Articles :</strong> {{ order_items_count }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Total :</strong> {{ order_total }}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0; color: #374151;">Adresse de livraison</h3>
|
||||
<p style="margin: 5px 0;">{{ shipping_address }}</p>
|
||||
</div>
|
||||
|
||||
<p>Vous recevrez un autre email lors de l'expédition de votre commande.</p>
|
||||
|
||||
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
|
||||
Si vous avez des questions concernant votre commande, n'hésitez pas à nous contacter.
|
||||
</p>
|
||||
|
||||
<p>Merci pour votre achat !<br><strong>L'équipe</strong></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>""",
|
||||
"body_text": """Commande confirmée !
|
||||
|
||||
Bonjour {{ customer_name }},
|
||||
|
||||
Merci pour votre commande ! Nous l'avons bien reçue et elle est en cours de traitement.
|
||||
|
||||
Détails de la commande :
|
||||
- Numéro de commande : {{ order_number }}
|
||||
- Date : {{ order_date }}
|
||||
- Articles : {{ order_items_count }}
|
||||
- Total : {{ order_total }}
|
||||
|
||||
Adresse de livraison :
|
||||
{{ shipping_address }}
|
||||
|
||||
Vous recevrez un autre email lors de l'expédition de votre commande.
|
||||
|
||||
Merci pour votre achat !
|
||||
""",
|
||||
},
|
||||
{
|
||||
"code": "order_confirmation",
|
||||
"language": "de",
|
||||
"name": "Bestellbestätigung",
|
||||
"description": "An Kunden nach einer Bestellung gesendet",
|
||||
"category": EmailCategory.ORDERS.value,
|
||||
"variables": json.dumps([
|
||||
"customer_name", "order_number", "order_total",
|
||||
"order_items_count", "order_date", "shipping_address"
|
||||
]),
|
||||
"subject": "Bestellbestätigung - {{ order_number }}",
|
||||
"body_html": """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 28px;">Bestellung bestätigt!</h1>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">Hallo {{ customer_name }},</p>
|
||||
|
||||
<p>Vielen Dank für Ihre Bestellung! Wir haben Ihre Bestellung erhalten und sie wird bearbeitet.</p>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981;">
|
||||
<h3 style="margin-top: 0; color: #10b981;">Bestelldetails</h3>
|
||||
<p style="margin: 5px 0;"><strong>Bestellnummer:</strong> {{ order_number }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Datum:</strong> {{ order_date }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Artikel:</strong> {{ order_items_count }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Summe:</strong> {{ order_total }}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0; color: #374151;">Lieferadresse</h3>
|
||||
<p style="margin: 5px 0;">{{ shipping_address }}</p>
|
||||
</div>
|
||||
|
||||
<p>Sie erhalten eine weitere E-Mail, sobald Ihre Bestellung versandt wird.</p>
|
||||
|
||||
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
|
||||
Bei Fragen zu Ihrer Bestellung kontaktieren Sie uns bitte.
|
||||
</p>
|
||||
|
||||
<p>Vielen Dank für Ihren Einkauf!<br><strong>Das Team</strong></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>""",
|
||||
"body_text": """Bestellung bestätigt!
|
||||
|
||||
Hallo {{ customer_name }},
|
||||
|
||||
Vielen Dank für Ihre Bestellung! Wir haben Ihre Bestellung erhalten und sie wird bearbeitet.
|
||||
|
||||
Bestelldetails:
|
||||
- Bestellnummer: {{ order_number }}
|
||||
- Datum: {{ order_date }}
|
||||
- Artikel: {{ order_items_count }}
|
||||
- Summe: {{ order_total }}
|
||||
|
||||
Lieferadresse:
|
||||
{{ shipping_address }}
|
||||
|
||||
Sie erhalten eine weitere E-Mail, sobald Ihre Bestellung versandt wird.
|
||||
|
||||
Vielen Dank für Ihren Einkauf!
|
||||
""",
|
||||
},
|
||||
{
|
||||
"code": "order_confirmation",
|
||||
"language": "lb",
|
||||
"name": "Bestellung Konfirmatioun",
|
||||
"description": "Un Clienten no enger Bestellung geschéckt",
|
||||
"category": EmailCategory.ORDERS.value,
|
||||
"variables": json.dumps([
|
||||
"customer_name", "order_number", "order_total",
|
||||
"order_items_count", "order_date", "shipping_address"
|
||||
]),
|
||||
"subject": "Bestellung Konfirmatioun - {{ order_number }}",
|
||||
"body_html": """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="color: white; margin: 0; font-size: 28px;">Bestellung confirméiert!</h1>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">Moien {{ customer_name }},</p>
|
||||
|
||||
<p>Merci fir Är Bestellung! Mir hunn Är Bestellung kritt an si gëtt beaarbecht.</p>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981;">
|
||||
<h3 style="margin-top: 0; color: #10b981;">Bestelldetailer</h3>
|
||||
<p style="margin: 5px 0;"><strong>Bestellnummer:</strong> {{ order_number }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Datum:</strong> {{ order_date }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Artikelen:</strong> {{ order_items_count }}</p>
|
||||
<p style="margin: 5px 0;"><strong>Total:</strong> {{ order_total }}</p>
|
||||
</div>
|
||||
|
||||
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
||||
<h3 style="margin-top: 0; color: #374151;">Liwweradress</h3>
|
||||
<p style="margin: 5px 0;">{{ shipping_address }}</p>
|
||||
</div>
|
||||
|
||||
<p>Dir kritt eng weider E-Mail wann Är Bestellung verschéckt gëtt.</p>
|
||||
|
||||
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
|
||||
Wann Dir Froen iwwer Är Bestellung hutt, kontaktéiert eis w.e.g.
|
||||
</p>
|
||||
|
||||
<p>Merci fir Ären Akaf!<br><strong>D'Team</strong></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>""",
|
||||
"body_text": """Bestellung confirméiert!
|
||||
|
||||
Moien {{ customer_name }},
|
||||
|
||||
Merci fir Är Bestellung! Mir hunn Är Bestellung kritt an si gëtt beaarbecht.
|
||||
|
||||
Bestelldetailer:
|
||||
- Bestellnummer: {{ order_number }}
|
||||
- Datum: {{ order_date }}
|
||||
- Artikelen: {{ order_items_count }}
|
||||
- Total: {{ order_total }}
|
||||
|
||||
Liwweradress:
|
||||
{{ shipping_address }}
|
||||
|
||||
Dir kritt eng weider E-Mail wann Är Bestellung verschéckt gëtt.
|
||||
|
||||
Merci fir Ären Akaf!
|
||||
""",
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user