Compare commits
2 Commits
cb12a6ced4
...
0621a0be16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0621a0be16 | ||
|
|
926650d3b6 |
@@ -43,15 +43,17 @@ graph TD
|
|||||||
---
|
---
|
||||||
|
|
||||||
### E. Multi-Kassen-Warenkorb & Checkout-Split
|
### E. Multi-Kassen-Warenkorb & Checkout-Split
|
||||||
- **Endpunkt**: `POST /api/orders/checkout`
|
- **Endpunkt / Server Action**: `POST /api/orders/checkout` sowie Server Action `checkoutAction` in `app/actions/checkout.ts`
|
||||||
- **Funktionsweise**:
|
- **Funktionsweise**:
|
||||||
- Empfängt ein Array von `items` (Kassenkonfigurationen).
|
- Empfängt ein Array von `items` (Kassenkonfigurationen).
|
||||||
|
- Validiert Auth und Partner-Firma Zuweisung.
|
||||||
|
- Generiert einen `order_hash` zur Vermeidung von Doppelübermittlungen (Idempotenz-Guard).
|
||||||
- Teilt die Konfigurationen nach `billingInterval` / `billingType` auf (Kauf vs. Abo).
|
- Teilt die Konfigurationen nach `billingInterval` / `billingType` auf (Kauf vs. Abo).
|
||||||
- Erstellt separate Orders in der Datenbank:
|
- Erstellt separate Orders in der Datenbank:
|
||||||
- Typ `purchase`: Zahlungsart `invoice` (Rechnung).
|
- Typ `purchase`: Zahlungsart `invoice` (Rechnung).
|
||||||
- Typ `subscription`: Zahlungsart `sepa`.
|
- Typ `subscription`: Zahlungsart `sepa`.
|
||||||
- Friert für jede Order einen konsolidierten `order_snapshot` (JSONB) und `customer_snapshot` (JSONB) ein.
|
- Friert für jede Order einen konsolidierten `order_snapshot` (JSONB) und `customer_snapshot` (JSONB) ein.
|
||||||
- Triggert die PDF-Rechnungserstellung und den E-Mail-Versand unabhängig für jede generierte Order.
|
- Triggert die PDF-Rechnungserstellung und den E-Mail-Versand unabhängig für jede generierte Order (gibt Order-IDs für Post-Processing zurück).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
141
shop/app/actions/checkout.ts
Normal file
141
shop/app/actions/checkout.ts
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user