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)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { Category, Product, ProductModule } from '../types'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
export async function getProducts() {
|
||||
const supabase = await createClient()
|
||||
@@ -64,7 +65,10 @@ export async function deleteCategory(id: string) {
|
||||
revalidatePath('/order')
|
||||
}
|
||||
|
||||
export async function createProduct(product: Omit<Product, 'id' | 'created_at' | 'updated_at'>, modules: Omit<ProductModule, 'id' | 'product_id' | 'created_at'>[]) {
|
||||
export async function createProduct(
|
||||
product: Omit<Product, 'id' | 'created_at' | 'updated_at'>,
|
||||
modules: Omit<ProductModule, 'product_id' | 'created_at'>[]
|
||||
) {
|
||||
const supabase = await createClient()
|
||||
|
||||
// Start a transaction-like approach (Supabase doesn't have cross-table transactions easily in one call via JS client without RPC)
|
||||
@@ -77,7 +81,15 @@ export async function createProduct(product: Omit<Product, 'id' | 'created_at' |
|
||||
if (productError) throw productError
|
||||
|
||||
if (modules.length > 0) {
|
||||
const modulesWithId = modules.map(m => ({ ...m, product_id: newProduct.id }))
|
||||
const modulesWithId = modules.map(m => ({
|
||||
id: m.id || randomUUID(),
|
||||
product_id: newProduct.id,
|
||||
name: m.name,
|
||||
description: m.description || null,
|
||||
price: m.price,
|
||||
requirements: m.requirements || [],
|
||||
exclusions: m.exclusions || [],
|
||||
}))
|
||||
const { error: modulesError } = await supabase
|
||||
.from('product_modules')
|
||||
.insert(modulesWithId)
|
||||
@@ -106,12 +118,13 @@ export async function updateProduct(id: string, product: Partial<Product>, modul
|
||||
|
||||
if (modules.length > 0) {
|
||||
const modulesWithId = modules.map(m => ({
|
||||
id: m.id || randomUUID(),
|
||||
product_id: id,
|
||||
name: m.name,
|
||||
description: m.description || null,
|
||||
price: m.price,
|
||||
requirements: m.requirements || [],
|
||||
exclusions: m.exclusions || [],
|
||||
product_id: id
|
||||
}))
|
||||
const { error: modulesError } = await supabase
|
||||
.from('product_modules')
|
||||
@@ -120,6 +133,7 @@ export async function updateProduct(id: string, product: Partial<Product>, modul
|
||||
if (modulesError) throw modulesError
|
||||
}
|
||||
|
||||
|
||||
revalidatePath('/admin/products')
|
||||
revalidatePath('/order')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user