'use server' import { createClient } from '@/lib/supabase/server'; import { createHash } from 'crypto'; import { buildCustomerSnapshot, buildOrderSnapshot } from '@/lib/license-transform'; import { getProducts, getCategories } from '@/lib/actions/products'; function generateOrderNumber(prefix: 'BE' | 'AE' = 'AE'): string { const year = new Date().getFullYear(); const rand = Math.floor(10000 + Math.random() * 90000); return `${prefix}-${year}-${rand}`; } function hashOrderPayload(endCustomerId: string, items: any[]): string { const str = endCustomerId + JSON.stringify(items); return createHash('sha256').update(str).digest('hex'); } export async function checkoutAction(params: { items: any[]; endCustomerId: string; customerProfile?: any; }) { const { items, endCustomerId, customerProfile } = params; const supabase = await createClient(); // 1. Session & Auth Check const { data: { user } } = await supabase.auth.getUser(); if (!user) { throw new Error('Not authenticated'); } const { data: dbUser } = await supabase .from('users') .select('role, company_id') .eq('id', user.id) .single(); const isAdminUser = dbUser?.role === 'admin'; if (!isAdminUser && !dbUser?.company_id) { throw new Error('Kein Unternehmen zugewiesen. Zugriff verweigert.'); } // 2. Idempotency Guard const orderHash = hashOrderPayload(endCustomerId, items); const fiveMinutesAgo = new Date(Date.now() - 5 * 60_000).toISOString(); const { data: existingOrders } = await supabase .from('orders') .select('id') .eq('user_id', user.id) .eq('order_hash', orderHash) .gte('created_at', fiveMinutesAgo); if (existingOrders && existingOrders.length > 0) { return { success: true, orderIds: existingOrders.map(o => o.id), duplicated: true }; } // Fetch End Customer Stammdaten const { data: endCustomer, error: customerErr } = await supabase .from('end_customers') .select('*') .eq('id', endCustomerId) .single(); if (customerErr || !endCustomer) { throw new Error('Endkunde nicht gefunden.'); } const customerSnapshot = buildCustomerSnapshot(customerProfile || {}, endCustomer); const products = await getProducts(); const categories = await getCategories(); // 3. Checkout Split const purchaseItems = items.filter( (item: any) => item.billingInterval === 'one_time' || item.billingType === 'purchase' ); const subscriptionItems = items.filter( (item: any) => item.billingInterval === 'monthly' || item.billingType === 'subscription' ); const orderIds: string[] = []; const createOrderGroup = async (groupItems: any[], type: 'purchase' | 'subscription', paymentMethod: string) => { if (groupItems.length === 0) return; const orderItemsList: any[] = []; let total = 0; for (const item of groupItems) { const itemSnapshot = buildOrderSnapshot( item.selections, products, categories, type === 'purchase' ? 'one_time' : 'monthly', item.moduleQuantities ); orderItemsList.push(...itemSnapshot.items); total += itemSnapshot.total; } const orderSnapshot = { schema_version: 1, billing_cycle: type === 'purchase' ? 'one_time' : 'monthly', items: orderItemsList, total: total, tax_rate: 19, tax_amount: Math.round(total * 0.19 * 100) / 100, subtotal: Math.round((total / 1.19) * 100) / 100 }; const orderNumber = generateOrderNumber(type === 'purchase' ? 'AE' : 'BE'); const { data: order, error: orderError } = await supabase .from('orders') .insert([{ user_id: user.id, company_id: dbUser?.company_id || null, order_number: orderNumber, order_hash: orderHash, end_customer_id: endCustomerId, total_price: total, customer_data: customerSnapshot, order_data: orderSnapshot, type: type, payment_method: paymentMethod, status: 'pending' }]) .select() .single(); if (orderError) throw orderError; orderIds.push(order.id); }; await createOrderGroup(purchaseItems, 'purchase', 'invoice'); await createOrderGroup(subscriptionItems, 'subscription', 'sepa'); return { success: true, orderIds, duplicated: false }; }