diff --git a/shop/lib/actions/email-templates.ts b/shop/lib/actions/email-templates.ts
new file mode 100644
index 0000000..e547ddd
--- /dev/null
+++ b/shop/lib/actions/email-templates.ts
@@ -0,0 +1,87 @@
+interface EmailDetails {
+ orderNumber: string
+ formattedDate: string
+ customerCompanyName: string
+ totalDetailsText: string
+ totalDetailsHtml: string
+}
+
+export function getOrderEmailTemplate(
+ details: EmailDetails,
+ siteUrl: string,
+ isUpdate: boolean = false
+) {
+ const title = isUpdate ? 'Ihre Anfrage wurde aktualisiert!' : 'Vielen Dank für Ihre Anfrage!'
+ const intro = isUpdate
+ ? `Ihr Anfrage mit der Nummer ${details.orderNumber} wurde erfolgreich aktualisiert.`
+ : 'wir freuen uns sehr über Ihre Anfrage bei CASPOS Shop. Ihre Anfrage wurde erfolgreich entgegengenommen und wird nun verarbeitet.'
+
+ const text = `Hallo,\n\n${isUpdate ? 'ihre Anfrage wurde erfolgreich aktualisiert!' : 'vielen Dank für Ihre Anfrage bei CASPOS Shop!'}\n\n${isUpdate ? 'Neue Anfragedetails' : 'Anfragedetails'}:\n- Anfrage-Nummer: ${details.orderNumber}\n- Datum: ${details.formattedDate}\n- Endkunde: ${details.customerCompanyName}\n${details.totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihre ${isUpdate ? 'aktualisierte' : ''} Anfragebestätigung als PDF-Dokument.\n\nPDF herunterladen: ${siteUrl}/api/orders/${details.orderNumber}/download\n\nViele Grüße,\nIhr CASPOS Shop-Team`
+
+ const html = `
+
+
${title}
+
Hallo,
+
${intro}
+
+
${isUpdate ? 'Neue Anfrage-Details' : 'Anfrage-Details'}
+
+
+ | Anfrage-Nummer: |
+ ${details.orderNumber} |
+
+
+ | Datum: |
+ ${details.formattedDate} |
+
+
+ | Endkunde: |
+ ${details.customerCompanyName} |
+
+ ${details.totalDetailsHtml}
+
+
+
Im Anhang dieser E-Mail finden Sie Ihre ${isUpdate ? 'aktualisierte' : ''} Anfragebestätigung als PDF-Dokument.
+
+
Mit freundlichen Grüßen,
Ihr CASPOS Shop-Team
+ ${!isUpdate ? '
Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.
' : ''}
+
+ `
+
+ return { text, html }
+}
+
+export function getStatusEmailTemplate(orderNumber: string, oldLabel: string, newLabel: string) {
+ const text = `Hallo,\n\nder Status Ihrer Bestellung mit der Nummer ${orderNumber} hat sich geändert.\n\nNeuer Status: ${newLabel} (vorher: ${oldLabel})\n\nViele Grüße,\nIhr CASPOS Shop-Team`
+
+ const html = `
+
+
Status Ihrer Bestellung hat sich geändert
+
Hallo,
+
der Status Ihrer Bestellung mit der Nummer ${orderNumber} wurde aktualisiert.
+
+
+
+ | Bestellnummer: |
+ ${orderNumber} |
+
+
+ | Alter Status: |
+ ${oldLabel} |
+
+
+ | Neuer Status: |
+ ${newLabel} |
+
+
+
+
Viele Grüße,
Ihr CASPOS Shop-Team
+
+
Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.
+
+ `
+
+ return { text, html }
+}
diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts
index 48fc5ac..f261343 100644
--- a/shop/lib/actions/orders.ts
+++ b/shop/lib/actions/orders.ts
@@ -11,6 +11,8 @@ 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'
+import { validateWizardSelections } from '@/lib/actions/validation'
+import { getOrderEmailTemplate, getStatusEmailTemplate } from '@/lib/actions/email-templates'
// ─── Hilfsfunktionen ─────────────────────────────────────────────────────────
@@ -78,72 +80,7 @@ export async function submitOrder(params: {
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 hasMetRequirement = mod.requirements.some(reqId => sel.moduleIds.includes(reqId))
- if (!hasMetRequirement) {
- throw new Error(`Das Modul "${mod.name}" setzt die Aktivierung mindestens eines der folgenden Module voraus: ${mod.requirements.join(', ')}.`)
- }
- }
- // 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.`)
- }
- }
- }
- }
- }
-
- // Product-level constraints validation
- const selectedProductIds = Object.values(selections)
- .map(s => s.productId)
- .filter((id): id is string => !!id)
-
- for (const prodId of selectedProductIds) {
- const prod = dbProducts.find(p => p.id === prodId)
- if (!prod) continue
-
- // Product requirements check
- if (prod.requirements && prod.requirements.length > 0) {
- const hasMetRequirement = prod.requirements.some(reqId => selectedProductIds.includes(reqId))
- if (!hasMetRequirement) {
- const reqNames = prod.requirements
- .map(reqId => dbProducts.find(p => p.id === reqId)?.name || reqId)
- .join(' oder ')
- throw new Error(`Das Produkt "${prod.name}" erfordert die Auswahl von mindestens einem dieser Produkte: ${reqNames}.`)
- }
- }
-
- // Product exclusions check
- if (prod.exclusions && prod.exclusions.length > 0) {
- const conflicting = prod.exclusions.filter(exId => selectedProductIds.includes(exId))
- if (conflicting.length > 0) {
- const conflictingNames = conflicting
- .map(exId => dbProducts.find(p => p.id === exId)?.name || exId)
- .join(', ')
- throw new Error(`Das Produkt "${prod.name}" schließt die gleichzeitige Auswahl von folgenden Produkten aus: ${conflictingNames}.`)
- }
- }
- }
+ validateWizardSelections(selections, dbProducts, dbCategories)
// 1. Snapshots aufbauen
// Wenn Endkunde vorhanden: dessen Daten einfrieren; sonst Partner-Profil (Fallback)
@@ -319,42 +256,19 @@ export async function submitOrder(params: {
`
}
+ const emailTemplate = getOrderEmailTemplate({
+ orderNumber,
+ formattedDate,
+ customerCompanyName: customerSnapshot.company_name,
+ totalDetailsText,
+ totalDetailsHtml
+ }, `${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}`, false)
+
await sendMail({
to: user.email,
subject: `Anfragebestätigung ${orderNumber}`,
- text: `Hallo,\n\nvielen Dank für Ihre Anfrage bei CASPOS Shop!\n\nAnfragedetails:\n- Anfrage-Nummer: ${orderNumber}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n${totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihre Anfragebestätigung als PDF-Dokument.\n\nPDF herunterladen: ${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}/api/orders/${order.id}/download\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
- html: `
-
-
Vielen Dank für Ihre Anfrage!
-
Hallo,
-
wir freuen uns sehr über Ihre Anfrage bei CASPOS Shop. Ihre Anfrage wurde erfolgreich entgegengenommen und wird nun verarbeitet.
-
-
Anfrage-Details
-
-
- | Anfrage-Nummer: |
- ${orderNumber} |
-
-
- | Datum: |
- ${formattedDate} |
-
-
- | Endkunde: |
- ${customerSnapshot.company_name} |
-
- ${totalDetailsHtml}
-
-
-
Im Anhang dieser E-Mail finden Sie Ihre Anfragebestätigung als PDF-Dokument.
-
-
Mit freundlichen Grüßen,
Ihr CASPOS Shop-Team
-
-
Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.
-
- `,
+ text: emailTemplate.text,
+ html: emailTemplate.html,
attachments: [
{
filename: `Anfragebestaetigung_${orderNumber}.pdf`,
@@ -427,36 +341,13 @@ export async function updateOrderStatus(
const oldLabel = statusLabelMap[oldStatus] || oldStatus
const newLabel = statusLabelMap[newStatus] || newStatus
+ const statusEmail = getStatusEmailTemplate(orderNumber, oldLabel, newLabel)
+
await sendMail({
to: user.email,
subject: `Statusänderung Ihrer Bestellung ${orderNumber}`,
- text: `Hallo,\n\nder Status Ihrer Bestellung mit der Nummer ${orderNumber} hat sich geändert.\n\nNeuer Status: ${newLabel} (vorher: ${oldLabel})\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
- html: `
-
-
Status Ihrer Bestellung hat sich geändert
-
Hallo,
-
der Status Ihrer Bestellung mit der Nummer ${orderNumber} wurde aktualisiert.
-
-
-
- | Bestellnummer: |
- ${orderNumber} |
-
-
- | Alter Status: |
- ${oldLabel} |
-
-
- | Neuer Status: |
- ${newLabel} |
-
-
-
-
Viele Grüße,
Ihr CASPOS Shop-Team
-
-
Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.
-
- `
+ text: statusEmail.text,
+ html: statusEmail.html
})
}
} catch (mailError) {
@@ -538,72 +429,7 @@ export async function updateOrder(
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 hasMetRequirement = mod.requirements.some(reqId => sel.moduleIds.includes(reqId))
- if (!hasMetRequirement) {
- throw new Error(`Das Modul "${mod.name}" setzt die Aktivierung mindestens eines der folgenden Module voraus: ${mod.requirements.join(', ')}.`)
- }
- }
- // 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.`)
- }
- }
- }
- }
- }
-
- // Product-level constraints validation
- const selectedProductIds = Object.values(selections)
- .map(s => s.productId)
- .filter((id): id is string => !!id)
-
- for (const prodId of selectedProductIds) {
- const prod = dbProducts.find(p => p.id === prodId)
- if (!prod) continue
-
- // Product requirements check
- if (prod.requirements && prod.requirements.length > 0) {
- const hasMetRequirement = prod.requirements.some(reqId => selectedProductIds.includes(reqId))
- if (!hasMetRequirement) {
- const reqNames = prod.requirements
- .map(reqId => dbProducts.find(p => p.id === reqId)?.name || reqId)
- .join(' oder ')
- throw new Error(`Das Produkt "${prod.name}" erfordert die Auswahl von mindestens einem dieser Produkte: ${reqNames}.`)
- }
- }
-
- // Product exclusions check
- if (prod.exclusions && prod.exclusions.length > 0) {
- const conflicting = prod.exclusions.filter(exId => selectedProductIds.includes(exId))
- if (conflicting.length > 0) {
- const conflictingNames = conflicting
- .map(exId => dbProducts.find(p => p.id === exId)?.name || exId)
- .join(', ')
- throw new Error(`Das Produkt "${prod.name}" schließt die gleichzeitige Auswahl von folgenden Produkten aus: ${conflictingNames}.`)
- }
- }
- }
+ validateWizardSelections(selections, dbProducts, dbCategories)
// 1. Snapshots aufbauen
const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null)
@@ -750,40 +576,19 @@ export async function updateOrder(
`
}
+ const emailTemplate = getOrderEmailTemplate({
+ orderNumber: order.order_number,
+ formattedDate,
+ customerCompanyName: customerSnapshot.company_name,
+ totalDetailsText,
+ totalDetailsHtml
+ }, `${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}`, true)
+
await sendMail({
to: orderUserEmail,
subject: `Anfrageänderung ${order.order_number}`,
- text: `Hallo,\n\nihre Anfrage wurde erfolgreich aktualisiert!\n\nNeue Anfragedetails:\n- Anfrage-Nummer: ${order.order_number}\n- Datum: ${formattedDate}\n- Endkunde: ${customerSnapshot.company_name}\n${totalDetailsText}\n\nIm Anhang dieser E-Mail finden Sie Ihre aktualisierte Anfrage als PDF-Dokument.\n\nPDF herunterladen: ${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}/api/orders/${order.id}/download\n\nViele Grüße,\nIhr CASPOS Shop-Team`,
- html: `
-
-
Ihre Anfrage wurde aktualisiert!
-
Hallo,
-
Ihr Anfrage mit der Nummer ${order.order_number} wurde erfolgreich aktualisiert.
-
-
Neue Anfrage-Details
-
-
- | Anfrage-Nummer: |
- ${order.order_number} |
-
-
- | Datum: |
- ${formattedDate} |
-
-
- | Endkunde: |
- ${customerSnapshot.company_name} |
-
- ${totalDetailsHtml}
-
-
-
Im Anhang dieser E-Mail finden Sie Ihre aktualisierte Anfragebestätigung als PDF-Dokument.
-
-
Mit freundlichen Grüßen,
Ihr CASPOS Shop-Team
-
- `,
+ text: emailTemplate.text,
+ html: emailTemplate.html,
attachments: [
{
filename: `Anfragebestaetigung_${order.order_number}.pdf`,
diff --git a/shop/lib/actions/validation.ts b/shop/lib/actions/validation.ts
new file mode 100644
index 0000000..884648c
--- /dev/null
+++ b/shop/lib/actions/validation.ts
@@ -0,0 +1,74 @@
+import { WizardSelections, Product, Category } from '@/lib/types'
+
+export function validateWizardSelections(
+ selections: WizardSelections,
+ dbProducts: Product[],
+ dbCategories: Category[]
+): void {
+ 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 hasMetRequirement = mod.requirements.some(reqId => sel.moduleIds.includes(reqId))
+ if (!hasMetRequirement) {
+ throw new Error(`Das Modul "${mod.name}" setzt die Aktivierung mindestens eines der folgenden Module voraus: ${mod.requirements.join(', ')}.`)
+ }
+ }
+ // 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.`)
+ }
+ }
+ }
+ }
+ }
+
+ // Product-level constraints validation
+ const selectedProductIds = Object.values(selections)
+ .map(s => s.productId)
+ .filter((id): id is string => !!id)
+
+ for (const prodId of selectedProductIds) {
+ const prod = dbProducts.find(p => p.id === prodId)
+ if (!prod) continue
+
+ // Product requirements check
+ if (prod.requirements && prod.requirements.length > 0) {
+ const hasMetRequirement = prod.requirements.some(reqId => selectedProductIds.includes(reqId))
+ if (!hasMetRequirement) {
+ const reqNames = prod.requirements
+ .map(reqId => dbProducts.find(p => p.id === reqId)?.name || reqId)
+ .join(' oder ')
+ throw new Error(`Das Produkt "${prod.name}" erfordert die Auswahl von mindestens einem dieser Produkte: ${reqNames}.`)
+ }
+ }
+
+ // Product exclusions check
+ if (prod.exclusions && prod.exclusions.length > 0) {
+ const conflicting = prod.exclusions.filter(exId => selectedProductIds.includes(exId))
+ if (conflicting.length > 0) {
+ const conflictingNames = conflicting
+ .map(exId => dbProducts.find(p => p.id === exId)?.name || exId)
+ .join(', ')
+ throw new Error(`Das Produkt "${prod.name}" schließt die gleichzeitige Auswahl von folgenden Produkten aus: ${conflictingNames}.`)
+ }
+ }
+ }
+}