refactor(shop): extract wizard validation and email templates into helpers
All checks were successful
Staging Build / build (push) Successful in 2m48s
All checks were successful
Staging Build / build (push) Successful in 2m48s
This commit is contained in:
87
shop/lib/actions/email-templates.ts
Normal file
87
shop/lib/actions/email-templates.ts
Normal file
@@ -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 = `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||
<h2 style="color: #0f172a; margin-bottom: 16px;">${title}</h2>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">${intro}</p>
|
||||
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
||||
<h3 style="color: #0f172a; margin-top: 0; margin-bottom: 12px;">${isUpdate ? 'Neue Anfrage-Details' : 'Anfrage-Details'}</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; width: 140px;">Anfrage-Nummer:</td>
|
||||
<td style="padding: 4px 0;">${details.orderNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Datum:</td>
|
||||
<td style="padding: 4px 0;">${details.formattedDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Endkunde:</td>
|
||||
<td style="padding: 4px 0;">${details.customerCompanyName}</td>
|
||||
</tr>
|
||||
${details.totalDetailsHtml}
|
||||
</table>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Im Anhang dieser E-Mail finden Sie Ihre ${isUpdate ? 'aktualisierte' : ''} Anfragebestätigung als PDF-Dokument.</p>
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
<a href="${siteUrl}" style="display: inline-block; background-color: #3b82f6; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 6px; font-weight: bold; font-size: 14px;">📄 PDF herunterladen</a>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5; margin-top: 24px;">Mit freundlichen Grüßen,<br>Ihr CASPOS Shop-Team</p>
|
||||
${!isUpdate ? '<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 24px 0;"><p style="color: #94a3b8; font-size: 12px; text-align: center;">Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>' : ''}
|
||||
</div>
|
||||
`
|
||||
|
||||
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 = `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||
<h2 style="color: #0f172a; margin-bottom: 16px;">Status Ihrer Bestellung hat sich geändert</h2>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">der Status Ihrer Bestellung mit der Nummer <strong>${orderNumber}</strong> wurde aktualisiert.</p>
|
||||
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; width: 140px;">Bestellnummer:</td>
|
||||
<td style="padding: 4px 0;">${orderNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Alter Status:</td>
|
||||
<td style="padding: 4px 0; text-decoration: line-through; color: #94a3b8;">${oldLabel}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Neuer Status:</td>
|
||||
<td style="padding: 4px 0; font-weight: bold; color: #3b82f6;">${newLabel}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Viele Grüße,<br>Ihr CASPOS Shop-Team</p>
|
||||
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 24px 0;">
|
||||
<p style="color: #94a3b8; font-size: 12px; text-align: center;">Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
return { text, html }
|
||||
}
|
||||
@@ -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: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||
<h2 style="color: #0f172a; margin-bottom: 16px;">Vielen Dank für Ihre Anfrage!</h2>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">wir freuen uns sehr über Ihre Anfrage bei CASPOS Shop. Ihre Anfrage wurde erfolgreich entgegengenommen und wird nun verarbeitet.</p>
|
||||
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
||||
<h3 style="color: #0f172a; margin-top: 0; margin-bottom: 12px;">Anfrage-Details</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; width: 140px;">Anfrage-Nummer:</td>
|
||||
<td style="padding: 4px 0;">${orderNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Datum:</td>
|
||||
<td style="padding: 4px 0;">${formattedDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Endkunde:</td>
|
||||
<td style="padding: 4px 0;">${customerSnapshot.company_name}</td>
|
||||
</tr>
|
||||
${totalDetailsHtml}
|
||||
</table>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Im Anhang dieser E-Mail finden Sie Ihre Anfragebestätigung als PDF-Dokument.</p>
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
<a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}/api/orders/${order.id}/download" style="display: inline-block; background-color: #3b82f6; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 6px; font-weight: bold; font-size: 14px;">📄 PDF herunterladen</a>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5; margin-top: 24px;">Mit freundlichen Grüßen,<br>Ihr CASPOS Shop-Team</p>
|
||||
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 24px 0;">
|
||||
<p style="color: #94a3b8; font-size: 12px; text-align: center;">Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
|
||||
</div>
|
||||
`,
|
||||
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: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||
<h2 style="color: #0f172a; margin-bottom: 16px;">Status Ihrer Bestellung hat sich geändert</h2>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">der Status Ihrer Bestellung mit der Nummer <strong>${orderNumber}</strong> wurde aktualisiert.</p>
|
||||
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; width: 140px;">Bestellnummer:</td>
|
||||
<td style="padding: 4px 0;">${orderNumber}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Alter Status:</td>
|
||||
<td style="padding: 4px 0; text-decoration: line-through; color: #94a3b8;">${oldLabel}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Neuer Status:</td>
|
||||
<td style="padding: 4px 0; font-weight: bold; color: #3b82f6;">${newLabel}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Viele Grüße,<br>Ihr CASPOS Shop-Team</p>
|
||||
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 24px 0;">
|
||||
<p style="color: #94a3b8; font-size: 12px; text-align: center;">Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht darauf.</p>
|
||||
</div>
|
||||
`
|
||||
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: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||
<h2 style="color: #0f172a; margin-bottom: 16px;">Ihre Anfrage wurde aktualisiert!</h2>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Hallo,</p>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Ihr Anfrage mit der Nummer ${order.order_number} wurde erfolgreich aktualisiert.</p>
|
||||
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 24px 0;">
|
||||
<h3 style="color: #0f172a; margin-top: 0; margin-bottom: 12px;">Neue Anfrage-Details</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px; color: #475569;">
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold; width: 140px;">Anfrage-Nummer:</td>
|
||||
<td style="padding: 4px 0;">${order.order_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Datum:</td>
|
||||
<td style="padding: 4px 0;">${formattedDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 0; font-weight: bold;">Endkunde:</td>
|
||||
<td style="padding: 4px 0;">${customerSnapshot.company_name}</td>
|
||||
</tr>
|
||||
${totalDetailsHtml}
|
||||
</table>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5;">Im Anhang dieser E-Mail finden Sie Ihre aktualisierte Anfragebestätigung als PDF-Dokument.</p>
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
<a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://staging.hephex.de'}/api/orders/${order.id}/download" style="display: inline-block; background-color: #3b82f6; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 6px; font-weight: bold; font-size: 14px;">📄 PDF herunterladen</a>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 16px; line-height: 1.5; margin-top: 24px;">Mit freundlichen Grüßen,<br>Ihr CASPOS Shop-Team</p>
|
||||
</div>
|
||||
`,
|
||||
text: emailTemplate.text,
|
||||
html: emailTemplate.html,
|
||||
attachments: [
|
||||
{
|
||||
filename: `Anfragebestaetigung_${order.order_number}.pdf`,
|
||||
|
||||
74
shop/lib/actions/validation.ts
Normal file
74
shop/lib/actions/validation.ts
Normal file
@@ -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}.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user