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

This commit is contained in:
DanielS
2026-06-23 04:42:50 +02:00
parent de1d228fb2
commit 0729dd21be
5 changed files with 286 additions and 122 deletions

View File

@@ -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)