feat: Add quantity support for product modules, multiply prices in order snapshot and pdf
Some checks failed
Staging Build / build (push) Has been cancelled

This commit is contained in:
DanielS
2026-06-25 13:04:37 +02:00
parent fe05e13d3f
commit 9e189f331a
8 changed files with 159 additions and 66 deletions

View File

@@ -50,6 +50,7 @@ function hashOrderSnapshot(snapshot: object): string {
*/
export async function submitOrder(params: {
selections: WizardSelections
moduleQuantities?: Record<string, number>
products?: Product[]
categories?: Category[]
customerProfile: Partial<Profile>
@@ -57,7 +58,7 @@ export async function submitOrder(params: {
endCustomer?: EndCustomer | null
billingInterval?: 'one_time' | 'monthly'
}): Promise<Order> {
const { selections, customerProfile, endCustomerId, endCustomer, billingInterval } = params
const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval } = params
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
@@ -106,7 +107,7 @@ export async function submitOrder(params: {
// 1. Snapshots aufbauen
// Wenn Endkunde vorhanden: dessen Daten einfrieren; sonst Partner-Profil (Fallback)
const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null)
const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval)
const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities)
// 2. Idempotenz-Guard: Prüfen ob identische Bestellung in den letzten 30s existiert
const orderHash = hashOrderSnapshot(orderSnapshot)

View File

@@ -89,6 +89,7 @@ export async function createProduct(
price: m.price,
requirements: m.requirements || [],
exclusions: m.exclusions || [],
has_quantity: m.has_quantity ?? false,
}))
const { error: modulesError } = await supabase
.from('product_modules')
@@ -125,6 +126,7 @@ export async function updateProduct(id: string, product: Partial<Product>, modul
price: m.price,
requirements: m.requirements || [],
exclusions: m.exclusions || [],
has_quantity: m.has_quantity ?? false,
}))
const { error: modulesError } = await supabase
.from('product_modules')

View File

@@ -85,7 +85,8 @@ export function buildOrderSnapshot(
selections: WizardSelections,
products: Product[],
categories: Category[],
billingInterval?: 'one_time' | 'monthly'
billingInterval?: 'one_time' | 'monthly',
moduleQuantities?: Record<string, number>
): OrderSnapshot {
const items: OrderItem[] = []
let subtotal = 0
@@ -103,13 +104,18 @@ export function buildOrderSnapshot(
const selectedModules = sel.moduleIds
.map((modId) => prod.modules?.find((m) => m.id === modId))
.filter((m): m is NonNullable<typeof m> => !!m)
.map((mod) => ({
module_id: mod.id,
module_name: mod.name,
price: mod.price,
}))
.map((mod) => {
const qty = moduleQuantities?.[mod.id] || 1
return {
module_id: mod.id,
module_name: mod.name,
price: mod.price,
quantity: qty,
total_price: mod.price * qty,
}
})
const moduleTotal = selectedModules.reduce((acc, m) => acc + m.price, 0)
const moduleTotal = selectedModules.reduce((acc, m) => acc + (m.total_price || m.price), 0)
const itemTotal = prod.base_price + moduleTotal
subtotal += itemTotal
@@ -157,7 +163,8 @@ export function transformToLicenseBundle(
selections: WizardSelections,
products: Product[],
categories: Category[],
customer: Partial<Profile>
customer: Partial<Profile>,
moduleQuantities?: Record<string, number>
): LicenseBundle {
const options: LicenseOption[] = []
let monthlyTotal = 0
@@ -186,6 +193,7 @@ export function transformToLicenseBundle(
for (const modId of sel.moduleIds) {
const mod = prod.modules?.find((m) => m.id === modId)
if (!mod) continue
const qty = moduleQuantities?.[mod.id] || 1
options.push({
key: `${prodKey}__${slugify(mod.name)}`,
@@ -194,10 +202,11 @@ export function transformToLicenseBundle(
// Module erben das Billing-Intervall des übergeordneten Produkts
billing: prod.billing_interval,
price_net: mod.price,
quantity: qty,
})
if (prod.billing_interval === 'monthly') monthlyTotal += mod.price
else oneTimeTotal += mod.price
if (prod.billing_interval === 'monthly') monthlyTotal += mod.price * qty
else oneTimeTotal += mod.price * qty
}
}
@@ -238,16 +247,19 @@ export function licenseBundleFromSnapshot(
else oneTimeTotal += item.base_price
for (const mod of item.selected_modules) {
const qty = mod.quantity || 1
const modPrice = mod.total_price || (mod.price * qty)
options.push({
key: `${prodKey}__${slugify(mod.module_name)}`,
label: mod.module_name,
active: true,
billing: item.billing_interval,
price_net: mod.price,
quantity: qty,
})
if (item.billing_interval === 'monthly') monthlyTotal += mod.price
else oneTimeTotal += mod.price
if (item.billing_interval === 'monthly') monthlyTotal += modPrice
else oneTimeTotal += modPrice
}
}

View File

@@ -36,6 +36,7 @@ export type ProductModule = {
requirements: string[] // UUID[] anderer Module, die aktiv sein müssen
exclusions: string[] // UUID[] von Modulen, die nicht gleichzeitig aktiv sein dürfen
created_at: string
has_quantity?: boolean
}
export type Profile = {
@@ -103,6 +104,8 @@ export type OrderModuleSnapshot = {
module_id: string
module_name: string
price: number
quantity?: number
total_price?: number
}
export type OrderItem = {
@@ -154,6 +157,7 @@ export type LicenseOption = {
active: boolean
billing: BillingInterval
price_net: number
quantity?: number
}
export type LicenseBundle = {