diff --git a/shop/app/order/success/page.tsx b/shop/app/order/success/page.tsx index 8dda4be..6b73fce 100644 --- a/shop/app/order/success/page.tsx +++ b/shop/app/order/success/page.tsx @@ -1,7 +1,7 @@ import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' import Link from 'next/link' -import { CheckCircle2, Download, ExternalLink, ShoppingBag, ArrowRight } from 'lucide-react' +import { CheckCircle2, Download, ExternalLink, ShoppingBag, ArrowRight, Calendar } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' @@ -110,6 +110,33 @@ export default async function OrderSuccessPage({ + {o.order_data?.price_multiplier !== null && o.order_data?.price_multiplier !== undefined && ( +
+

Update-Rabatt angewendet:

+

+ {o.order_data.price_multiplier === 0 + ? "Update (0-12 Monate): 100% Rabatt (inklusive)" + : o.order_data.price_multiplier === 0.15 + ? "Update (13-14 Monate): 15% vom Listenpreis" + : o.order_data.price_multiplier === 0.30 + ? "Update (15-25 Monate): 30% vom Listenpreis" + : o.order_data.price_multiplier === 0.45 + ? "Update (26-37 Monate): 45% vom Listenpreis" + : o.order_data.price_multiplier === 0.60 + ? "Update (38-49 Monate): 60% vom Listenpreis" + : o.order_data.price_multiplier === 0.90 + ? "Neukauf (>= 50 Monate): 10% Rabatt" + : `Gebühr: ${o.order_data.price_multiplier * 100}%`} +

+ {o.order_data.last_license_date && ( +

+ + Letzte Lizenz vom: {new Date(o.order_data.last_license_date).toLocaleDateString('de-DE')} +

+ )} +
+ )} + {/* Kundendaten */} diff --git a/shop/components/invoice-pdf.tsx b/shop/components/invoice-pdf.tsx index a44e4cc..3475278 100644 --- a/shop/components/invoice-pdf.tsx +++ b/shop/components/invoice-pdf.tsx @@ -134,6 +134,32 @@ export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => { Datum: {new Date(order.created_at || Date.now()).toLocaleDateString('de-DE')} + {orderSnapshot?.price_multiplier !== null && orderSnapshot?.price_multiplier !== undefined && ( + + Update-Konditionen + + {orderSnapshot.price_multiplier === 0 + ? "Update (0-12 Monate): 100% Rabatt (inklusive)" + : orderSnapshot.price_multiplier === 0.15 + ? "Update (13-14 Monate): 15% vom Listenpreis" + : orderSnapshot.price_multiplier === 0.30 + ? "Update (15-25 Monate): 30% vom Listenpreis" + : orderSnapshot.price_multiplier === 0.45 + ? "Update (26-37 Monate): 45% vom Listenpreis" + : orderSnapshot.price_multiplier === 0.60 + ? "Update (38-49 Monate): 60% vom Listenpreis" + : orderSnapshot.price_multiplier === 0.90 + ? "Neukauf (>= 50 Monate): 10% Rabatt auf Neukauf" + : `Gebühr: ${orderSnapshot.price_multiplier * 100}%`} + + {orderSnapshot.last_license_date && ( + + Letzte Lizenz vom: {new Date(orderSnapshot.last_license_date).toLocaleDateString('de-DE')} + + )} + + )} + Beschreibung diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index a6b52f2..8445589 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -113,6 +113,37 @@ export function OrderWizard({ // Modulmengen (moduleId -> Menge) const [moduleQuantities, setModuleQuantities] = useState>({}) + // Datum der letzten Lizenz (Bestandskunden-Update-Logik) + const [lastLicenseDate, setLastLicenseDate] = useState("") + + const lastLicenseMonths = useMemo(() => { + if (!lastLicenseDate || customerMode !== 'select' || !selectedEndCustomerId) return null + const lastDate = new Date(lastLicenseDate) + const today = new Date() + const yearsDiff = today.getFullYear() - lastDate.getFullYear() + const monthsDiff = today.getMonth() - lastDate.getMonth() + return yearsDiff * 12 + monthsDiff + }, [lastLicenseDate, customerMode, selectedEndCustomerId]) + + const updatePriceModifier = useMemo(() => { + if (lastLicenseMonths === null || selectedBillingInterval === 'monthly') { + return { multiplier: 1, discount: 0, label: null } + } + if (lastLicenseMonths <= 12) { + return { multiplier: 0, discount: 1, label: "Update (0-12 Mon.): 100% Rabatt (inklusive)" } + } else if (lastLicenseMonths <= 14) { + return { multiplier: 0.15, discount: 0.85, label: "Update (13-14 Mon.): 15% vom Listenpreis" } + } else if (lastLicenseMonths <= 25) { + return { multiplier: 0.30, discount: 0.70, label: "Update (15-25 Mon.): 30% vom Listenpreis" } + } else if (lastLicenseMonths <= 37) { + return { multiplier: 0.45, discount: 0.55, label: "Update (26-37 Mon.): 45% vom Listenpreis" } + } else if (lastLicenseMonths <= 49) { + return { multiplier: 0.60, discount: 0.40, label: "Update (38-49 Mon.): 60% vom Listenpreis" } + } else { + return { multiplier: 0.90, discount: 0.10, label: "Neukauf (>= 50 Mon.): 10% Rabatt" } + } + }, [lastLicenseMonths, selectedBillingInterval]) + // Per-category selection map: categoryId -> { productId, moduleIds } const [selections, setSelections] = useState>(() => { const init: Record = {} @@ -203,8 +234,12 @@ export function OrderWizard({ monthly += totalItem } } + // Apply update modifier to oneTime total + if (updatePriceModifier.multiplier !== 1) { + oneTime = oneTime * updatePriceModifier.multiplier + } return { monthlyTotal: monthly, oneTimeTotal: oneTime } - }, [selections, products, categories, moduleQuantities]) + }, [selections, products, categories, moduleQuantities, updatePriceModifier]) // Helper: update product selection for a category (resets modules) function selectProduct(catId: string, productId: string) { @@ -280,6 +315,7 @@ export function OrderWizard({ endCustomerId: selectedEndCustomerId, endCustomer: selectedEndCustomer, billingInterval: selectedBillingInterval, + lastLicenseDate: lastLicenseDate || null, }) router.push(`/order/success?id=${order.id}`) } catch (error) { @@ -401,6 +437,24 @@ export function OrderWizard({ ))} )} + {selectedEndCustomerId && ( +
+ + setLastLicenseDate(e.target.value)} + className="bg-[#0b1329] border-white/10 text-white focus:border-primary" + /> +

+ Falls dieser Kunde bereits Lizenzen besitzt, tragen Sie das Datum der letzten Lizenzierung ein. Damit werden die korrekten Update-Gebühren ermittelt. +

+
+ )} )} @@ -819,6 +873,15 @@ export function OrderWizard({ )} + {updatePriceModifier.label && ( +
+

+ + Update-Rabatt aktiv +

+

{updatePriceModifier.label}

+
+ )} {!allCategoriesFilled && (

@@ -920,6 +983,12 @@ export function OrderWizard({

{[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}

{[selectedEndCustomer.street, selectedEndCustomer.zip, selectedEndCustomer.city].filter(Boolean).join(', ')}

Endkunde Ihres Partners

+ {lastLicenseDate && ( +

+ + Letzte Lizenz vom: {new Date(lastLicenseDate).toLocaleDateString('de-DE')} +

+ )} ) : ( <> @@ -938,6 +1007,11 @@ export function OrderWizard({ {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)} + {updatePriceModifier.label && ( +
+ {updatePriceModifier.label} +
+ )}
Monatlicher Gesamtbetrag: @@ -947,11 +1021,18 @@ export function OrderWizard({
) : oneTimeTotal > 0 ? ( -
- Gesamtbetrag: - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)} - +
+
+ Gesamtbetrag: + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)} + +
+ {updatePriceModifier.label && ( +
+ {updatePriceModifier.label} +
+ )}
) : (
diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts index 41668f6..dcb509d 100644 --- a/shop/lib/actions/orders.ts +++ b/shop/lib/actions/orders.ts @@ -57,8 +57,9 @@ export async function submitOrder(params: { endCustomerId?: string | null endCustomer?: EndCustomer | null billingInterval?: 'one_time' | 'monthly' + lastLicenseDate?: string | null }): Promise { - const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval } = params + const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval, lastLicenseDate } = params const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() @@ -139,7 +140,7 @@ export async function submitOrder(params: { // 1. Snapshots aufbauen // Wenn Endkunde vorhanden: dessen Daten einfrieren; sonst Partner-Profil (Fallback) const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null) - const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities) + const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities, lastLicenseDate) // 2. Idempotenz-Guard: Prüfen ob identische Bestellung in den letzten 30s existiert const orderHash = hashOrderSnapshot(orderSnapshot) diff --git a/shop/lib/license-transform.ts b/shop/lib/license-transform.ts index 41f601f..43af559 100644 --- a/shop/lib/license-transform.ts +++ b/shop/lib/license-transform.ts @@ -86,7 +86,8 @@ export function buildOrderSnapshot( products: Product[], categories: Category[], billingInterval?: 'one_time' | 'monthly', - moduleQuantities?: Record + moduleQuantities?: Record, + lastLicenseDate?: string | null ): OrderSnapshot { const items: OrderItem[] = [] let subtotal = 0 @@ -131,14 +132,43 @@ export function buildOrderSnapshot( }) } - const taxAmount = Math.round(subtotal * (dominantTaxRate / 100) * 100) / 100 - const total = Math.round((subtotal + taxAmount) * 100) / 100 - // Wenn billingInterval übergeben, dieses nutzen; sonst automatisch ermitteln const billingCycle = billingInterval || (items.every((i) => i.billing_interval === 'one_time') ? 'one_time' : 'monthly') + let multiplier = 1 + if (lastLicenseDate && billingCycle === 'one_time') { + const lastDate = new Date(lastLicenseDate) + const today = new Date() + const yearsDiff = today.getFullYear() - lastDate.getFullYear() + const monthsDiff = today.getMonth() - lastDate.getMonth() + const months = yearsDiff * 12 + monthsDiff + + if (months <= 12) { + multiplier = 0 + } else if (months <= 14) { + multiplier = 0.15 + } else if (months <= 25) { + multiplier = 0.30 + } else if (months <= 37) { + multiplier = 0.45 + } else if (months <= 49) { + multiplier = 0.60 + } else { + multiplier = 0.90 + } + + // Multiply item_total in items by multiplier + items.forEach(item => { + item.item_total = item.item_total * multiplier + }) + subtotal = subtotal * multiplier + } + + const taxAmount = Math.round(subtotal * (dominantTaxRate / 100) * 100) / 100 + const total = Math.round((subtotal + taxAmount) * 100) / 100 + return { schema_version: 1, billing_cycle: billingCycle, @@ -147,6 +177,8 @@ export function buildOrderSnapshot( tax_rate: dominantTaxRate, tax_amount: taxAmount, total, + last_license_date: lastLicenseDate || null, + price_multiplier: lastLicenseDate && billingCycle === 'one_time' ? multiplier : null, } } diff --git a/shop/lib/types.ts b/shop/lib/types.ts index d51fb05..7ab8c82 100644 --- a/shop/lib/types.ts +++ b/shop/lib/types.ts @@ -132,6 +132,8 @@ export type OrderSnapshot = { tax_rate: number tax_amount: number total: number + last_license_date?: string | null + price_multiplier?: number | null } // ─── DB-Zeile (orders-Tabelle) ────────────────────────────────────────────────