diff --git a/shop/app/actions/checkout.ts b/shop/app/actions/checkout.ts new file mode 100644 index 0000000..6ea1540 --- /dev/null +++ b/shop/app/actions/checkout.ts @@ -0,0 +1,141 @@ +'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 }; +}