Blog categories (hasMany), shipping-address contact fields, product SKU on invoices
- Posts.categories is now hasMany — blog list/detail/live-preview render a comma-joined list instead of a single category. - Checkout's "Abweichende Lieferadresse" gains optional Firma + Kontakt- E-Mail/Telefon fields (handed to the shipping carrier, not used for customer communication). - Customer profile can now store its own shipping address (mirroring Customers.ts's new "Lieferadresse" tab), prefilling the checkout override instead of always starting blank. - Product-level optional SKU (previously only on variants) snapshots onto each order item and shows up on invoices (visual + EN16931 XML), the confirmation email, and the account order detail page.
This commit is contained in:
@@ -13,7 +13,32 @@ export async function PATCH(request: Request) {
|
||||
if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 });
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const { firstName, lastName, deliveryMethod, street, packstationNumber, postNumber, zip, city, country, companyName, vatId } = body ?? {};
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
deliveryMethod,
|
||||
street,
|
||||
packstationNumber,
|
||||
postNumber,
|
||||
zip,
|
||||
city,
|
||||
country,
|
||||
companyName,
|
||||
vatId,
|
||||
hasDifferentShippingAddress,
|
||||
shippingFirstName,
|
||||
shippingLastName,
|
||||
shippingCompanyName,
|
||||
shippingDeliveryMethod,
|
||||
shippingStreet,
|
||||
shippingPackstationNumber,
|
||||
shippingPostNumber,
|
||||
shippingZip,
|
||||
shippingCity,
|
||||
shippingCountry,
|
||||
shippingContactEmail,
|
||||
shippingContactPhone,
|
||||
} = body ?? {};
|
||||
if (
|
||||
typeof firstName !== "string" ||
|
||||
!firstName ||
|
||||
@@ -42,6 +67,33 @@ export async function PATCH(request: Request) {
|
||||
return NextResponse.json({ ok: false, reason: "Ungültiges USt-IdNr.-Format (z. B. DE123456789)." }, { status: 400 });
|
||||
}
|
||||
|
||||
// Same shape as the checkout's own shipping-address-override validation
|
||||
// (api/checkout/route.ts) — required fields only apply when the toggle
|
||||
// is actually on, since this whole block is optional otherwise.
|
||||
if (hasDifferentShippingAddress) {
|
||||
if (
|
||||
typeof shippingFirstName !== "string" ||
|
||||
!shippingFirstName ||
|
||||
typeof shippingLastName !== "string" ||
|
||||
!shippingLastName ||
|
||||
(shippingDeliveryMethod !== "address" && shippingDeliveryMethod !== "packstation") ||
|
||||
typeof shippingZip !== "string" ||
|
||||
!shippingZip ||
|
||||
typeof shippingCity !== "string" ||
|
||||
!shippingCity ||
|
||||
typeof shippingCountry !== "string" ||
|
||||
!shippingCountry
|
||||
) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte alle Pflichtfelder der Lieferadresse ausfüllen." }, { status: 400 });
|
||||
}
|
||||
if (shippingDeliveryMethod === "address" && !shippingStreet) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer der Lieferadresse angeben." }, { status: 400 });
|
||||
}
|
||||
if (shippingDeliveryMethod === "packstation" && (!shippingPackstationNumber || !shippingPostNumber)) {
|
||||
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer der Lieferadresse angeben." }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await updateCustomerProfile(session.token, session.customer.id, {
|
||||
firstName,
|
||||
lastName,
|
||||
@@ -54,6 +106,19 @@ export async function PATCH(request: Request) {
|
||||
country,
|
||||
companyName: typeof companyName === "string" && companyName ? companyName : undefined,
|
||||
vatId: normalizedVatId,
|
||||
hasDifferentShippingAddress: Boolean(hasDifferentShippingAddress),
|
||||
shippingFirstName: hasDifferentShippingAddress ? shippingFirstName : undefined,
|
||||
shippingLastName: hasDifferentShippingAddress ? shippingLastName : undefined,
|
||||
shippingCompanyName: hasDifferentShippingAddress && shippingCompanyName ? shippingCompanyName : undefined,
|
||||
shippingDeliveryMethod: hasDifferentShippingAddress ? shippingDeliveryMethod : undefined,
|
||||
shippingStreet: hasDifferentShippingAddress ? shippingStreet : undefined,
|
||||
shippingPackstationNumber: hasDifferentShippingAddress ? shippingPackstationNumber : undefined,
|
||||
shippingPostNumber: hasDifferentShippingAddress ? shippingPostNumber : undefined,
|
||||
shippingZip: hasDifferentShippingAddress ? shippingZip : undefined,
|
||||
shippingCity: hasDifferentShippingAddress ? shippingCity : undefined,
|
||||
shippingCountry: hasDifferentShippingAddress ? shippingCountry : undefined,
|
||||
shippingContactEmail: hasDifferentShippingAddress && shippingContactEmail ? shippingContactEmail : undefined,
|
||||
shippingContactPhone: hasDifferentShippingAddress && shippingContactPhone ? shippingContactPhone : undefined,
|
||||
});
|
||||
return NextResponse.json(result, { status: result.ok ? 200 : 400 });
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ type CheckoutBody = {
|
||||
shippingZip?: string;
|
||||
shippingCity?: string;
|
||||
shippingCountry?: string;
|
||||
shippingCompanyName?: string;
|
||||
shippingContactEmail?: string;
|
||||
shippingContactPhone?: string;
|
||||
newsletterOptIn: boolean;
|
||||
};
|
||||
|
||||
@@ -160,6 +163,7 @@ export async function POST(request: Request) {
|
||||
taxRatePercent: number;
|
||||
bundleContents: string | null;
|
||||
variantName: string | null;
|
||||
sku: string | null;
|
||||
}[] = [];
|
||||
for (const line of body.cart) {
|
||||
const product = productsBySlug.get(line.id);
|
||||
@@ -168,7 +172,7 @@ export async function POST(request: Request) {
|
||||
// requested variant that no longer exists on this product (removed,
|
||||
// or never existed — a tampered request) fails the whole checkout
|
||||
// rather than silently falling back to the base product/price.
|
||||
let variant: { name: string; priceOverride: number | null } | null = null;
|
||||
let variant: { name: string; priceOverride: number | null; sku: string | null } | null = null;
|
||||
if (line.variant) {
|
||||
variant = product.variants?.find((v) => v.name === line.variant) ?? null;
|
||||
if (!variant) return NextResponse.json({ ok: false, reason: "Eine gewählte Variante ist nicht mehr verfügbar." }, { status: 400 });
|
||||
@@ -195,6 +199,9 @@ export async function POST(request: Request) {
|
||||
taxRatePercent: kleinunternehmer ? 0 : (product.taxRatePercent ?? defaultTaxRate),
|
||||
bundleContents: describeBundleContents(product),
|
||||
variantName: variant?.name ?? null,
|
||||
// Variant sku takes precedence over the product's own — same
|
||||
// "variant overrides product" precedence unitPrice already uses.
|
||||
sku: variant?.sku ?? product.sku ?? null,
|
||||
});
|
||||
}
|
||||
const subtotal = roundMoney(items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0));
|
||||
@@ -345,11 +352,15 @@ export async function POST(request: Request) {
|
||||
shippingZip: body.shippingZip,
|
||||
shippingCity: body.shippingCity,
|
||||
shippingCountry: body.shippingCountry,
|
||||
shippingCompanyName: Boolean(body.hasDifferentShippingAddress) ? body.shippingCompanyName || undefined : undefined,
|
||||
shippingContactEmail: Boolean(body.hasDifferentShippingAddress) ? body.shippingContactEmail || undefined : undefined,
|
||||
shippingContactPhone: Boolean(body.hasDifferentShippingAddress) ? body.shippingContactPhone || undefined : undefined,
|
||||
newsletterOptIn: Boolean(body.newsletterOptIn),
|
||||
items,
|
||||
subtotal: finalSubtotal,
|
||||
shippingCost: finalShippingCost,
|
||||
shippingMethodTitle: shippingMethod.title,
|
||||
shippingMethod: shippingMethod.id,
|
||||
// The checkout UI collapses Kreditkarte/PayPal into one "Online-
|
||||
// Zahlung" pre-selection (see groupPaymentMethodsForCheckout) — the
|
||||
// customer hasn't actually chosen an instrument yet at this point,
|
||||
@@ -429,6 +440,7 @@ export async function POST(request: Request) {
|
||||
hasDifferentShippingAddress: Boolean(body.hasDifferentShippingAddress),
|
||||
shippingFirstName: body.shippingFirstName,
|
||||
shippingLastName: body.shippingLastName,
|
||||
shippingCompanyName: body.hasDifferentShippingAddress ? body.shippingCompanyName : undefined,
|
||||
shippingDeliveryMethod: body.shippingDeliveryMethod,
|
||||
shippingStreet: body.shippingStreet,
|
||||
shippingPackstationNumber: body.shippingPackstationNumber,
|
||||
@@ -436,6 +448,8 @@ export async function POST(request: Request) {
|
||||
shippingZip: body.shippingZip,
|
||||
shippingCity: body.shippingCity,
|
||||
shippingCountry: body.shippingCountry,
|
||||
shippingContactEmail: body.hasDifferentShippingAddress ? body.shippingContactEmail : undefined,
|
||||
shippingContactPhone: body.hasDifferentShippingAddress ? body.shippingContactPhone : undefined,
|
||||
paymentMethodTitle: paymentMethod.title,
|
||||
items: items.map((i) => ({
|
||||
productName: i.productName,
|
||||
@@ -445,6 +459,7 @@ export async function POST(request: Request) {
|
||||
taxRatePercent: i.taxRatePercent,
|
||||
bundleContents: i.bundleContents,
|
||||
variantName: i.variantName,
|
||||
sku: i.sku,
|
||||
})),
|
||||
subtotal: finalSubtotal,
|
||||
shippingCost: finalShippingCost,
|
||||
|
||||
Reference in New Issue
Block a user