Implement dynamic SMTP setting loader, admin product dependency matrix UI, recursive deselect cascade, and backend catalog validation
All checks were successful
Staging Build / build (push) Successful in 2m19s
All checks were successful
Staging Build / build (push) Successful in 2m19s
This commit is contained in:
@@ -7,6 +7,7 @@ import { renderToBuffer } from '@react-pdf/renderer'
|
||||
import React from 'react'
|
||||
import { buildCustomerSnapshot, buildOrderSnapshot } from '@/lib/license-transform'
|
||||
import type { Category, EndCustomer, Order, Product, Profile, WizardSelections } from '@/lib/types'
|
||||
import { getProducts, getCategories } from '@/lib/actions/products'
|
||||
|
||||
// ─── Hilfsfunktionen ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -48,22 +49,62 @@ function hashOrderSnapshot(snapshot: object): string {
|
||||
*/
|
||||
export async function submitOrder(params: {
|
||||
selections: WizardSelections
|
||||
products: Product[]
|
||||
categories: Category[]
|
||||
products?: Product[]
|
||||
categories?: Category[]
|
||||
customerProfile: Partial<Profile>
|
||||
endCustomerId?: string | null
|
||||
endCustomer?: EndCustomer | null
|
||||
}): Promise<Order> {
|
||||
const { selections, products, categories, customerProfile, endCustomerId, endCustomer } = params
|
||||
const { selections, customerProfile, endCustomerId, endCustomer } = params
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) throw new Error('Not authenticated')
|
||||
|
||||
// Fetch catalog directly from DB to prevent client tampering
|
||||
const dbProducts = await getProducts()
|
||||
const dbCategories = await getCategories()
|
||||
|
||||
// ─── Constraint Validation ──────────────────────────────────────────────────
|
||||
for (const cat of dbCategories) {
|
||||
const sel = selections[cat.id]
|
||||
if (cat.is_required && (!sel || !sel.productId)) {
|
||||
throw new Error(`Die Kategorie "${cat.name}" ist ein Pflichtfeld, wurde aber nicht ausgewählt.`)
|
||||
}
|
||||
if (sel?.productId) {
|
||||
const prod = dbProducts.find(p => p.id === sel.productId)
|
||||
if (!prod) {
|
||||
throw new Error(`Das ausgewählte Produkt für Kategorie "${cat.name}" wurde nicht gefunden.`)
|
||||
}
|
||||
|
||||
// Validate selected modules
|
||||
for (const mId of sel.moduleIds) {
|
||||
const mod = prod.modules?.find(m => m.id === mId)
|
||||
if (!mod) {
|
||||
throw new Error(`Das Modul mit ID "${mId}" gehört nicht zum Produkt "${prod.name}".`)
|
||||
}
|
||||
// Requirements check
|
||||
if (mod.requirements && mod.requirements.length > 0) {
|
||||
const missing = mod.requirements.filter(reqId => !sel.moduleIds.includes(reqId))
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Das Modul "${mod.name}" setzt die Aktivierung anderer Module voraus.`)
|
||||
}
|
||||
}
|
||||
// Exclusions check
|
||||
if (mod.exclusions && mod.exclusions.length > 0) {
|
||||
const conflicting = mod.exclusions.filter(exId => sel.moduleIds.includes(exId))
|
||||
if (conflicting.length > 0) {
|
||||
throw new Error(`Das Modul "${mod.name}" schließt die Kombination mit anderen gewählten Modulen aus.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Snapshots aufbauen
|
||||
// Wenn Endkunde vorhanden: dessen Daten einfrieren; sonst Partner-Profil (Fallback)
|
||||
const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null)
|
||||
const orderSnapshot = buildOrderSnapshot(selections, products, categories)
|
||||
const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories)
|
||||
|
||||
// 2. Idempotenz-Guard: Prüfen ob identische Bestellung in den letzten 30s existiert
|
||||
const orderHash = hashOrderSnapshot(orderSnapshot)
|
||||
|
||||
Reference in New Issue
Block a user