Add optional Firma/USt-IdNr. fields to checkout and profile

B2B checkout fields, split out from the e-invoicing migration and
picked back up now that it's shipped. Both fields are independently
optional, format-validated (shared regex in lib/vatId.ts, mirrored
server-side in api/checkout and api/account/profile), persisted in
the checkout draft, and saved as a customer profile default that
pre-fills future checkouts. Order/customer snapshot fields land in a
companion Payload backend commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-23 17:53:21 +00:00
parent 2d88fb86a1
commit e48107470a
8 changed files with 119 additions and 1 deletions
+10 -1
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { getSessionCustomer, updateCustomerProfile } from "../../../lib/customerAuth";
import { normalizeVatId, isValidVatId } from "../../../lib/vatId";
export async function GET() {
const session = await getSessionCustomer();
@@ -12,7 +13,7 @@ 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 } = body ?? {};
const { firstName, lastName, deliveryMethod, street, packstationNumber, postNumber, zip, city, country, companyName, vatId } = body ?? {};
if (
typeof firstName !== "string" ||
!firstName ||
@@ -34,6 +35,12 @@ export async function PATCH(request: Request) {
if (deliveryMethod === "packstation" && (!packstationNumber || !postNumber)) {
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer angeben." }, { status: 400 });
}
// Both independently optional (see Customers.ts's own comment) — only
// format-checked when actually provided, same as the backend field itself.
const normalizedVatId = typeof vatId === "string" && vatId ? normalizeVatId(vatId) : undefined;
if (normalizedVatId && !isValidVatId(normalizedVatId)) {
return NextResponse.json({ ok: false, reason: "Ungültiges USt-IdNr.-Format (z. B. DE123456789)." }, { status: 400 });
}
const result = await updateCustomerProfile(session.token, session.customer.id, {
firstName,
@@ -45,6 +52,8 @@ export async function PATCH(request: Request) {
zip,
city,
country,
companyName: typeof companyName === "string" && companyName ? companyName : undefined,
vatId: normalizedVatId,
});
return NextResponse.json(result, { status: result.ok ? 200 : 400 });
}
+14
View File
@@ -8,6 +8,7 @@ import { fetchProductsBySlug } from "../../lib/productsServer";
import { describeBundleContents } from "../../lib/bundleContents";
import { sendCriticalAlert } from "../../lib/alertAdmin";
import { sendOrderConfirmationEmail } from "../../lib/orderEmail";
import { normalizeVatId, isValidVatId } from "../../lib/vatId";
// Plain float arithmetic on money (quantity × unitPrice summed across
// lines, a percent discount, subtracting/adding those together) drifts
@@ -30,6 +31,8 @@ type CheckoutBody = {
lastName: string;
email: string;
password?: string;
companyName?: string;
vatId?: string;
deliveryMethod: "address" | "packstation";
street?: string;
packstationNumber?: string;
@@ -85,6 +88,15 @@ export async function POST(request: Request) {
if (body.deliveryMethod === "packstation" && (!body.packstationNumber || !body.postNumber)) {
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer angeben." }, { status: 400 });
}
// Optional — only format-checked when actually provided, same "never
// trust the client" reasoning as every other checkout field re-validated
// here. Normalized the same way Orders.ts's own field does (uppercase +
// trim), so the snapshot on the order matches what would've been
// accepted directly through the Payload admin.
const normalizedVatId = body.vatId ? normalizeVatId(body.vatId) : undefined;
if (normalizedVatId && !isValidVatId(normalizedVatId)) {
return NextResponse.json({ ok: false, reason: "Ungültiges USt-IdNr.-Format (z. B. DE123456789)." }, { status: 400 });
}
if (body.hasDifferentShippingAddress) {
if (!body.shippingFirstName || !body.shippingLastName || !body.shippingZip || !body.shippingCity || !body.shippingCountry) {
return NextResponse.json({ ok: false, reason: "Bitte alle Felder der Lieferadresse ausfüllen." }, { status: 400 });
@@ -201,6 +213,8 @@ export async function POST(request: Request) {
customerFirstName: body.firstName,
customerLastName: body.lastName,
customerEmail: body.email,
companyName: body.companyName || undefined,
vatId: normalizedVatId,
deliveryMethod: body.deliveryMethod,
street: body.street,
packstationNumber: body.packstationNumber,