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
+5
View File
@@ -14,6 +14,11 @@ export type CheckoutDraft = {
firstName: string;
lastName: string;
email: string;
// Optional B2B fields — see CheckoutContent.tsx's own comment on why
// they sit here (right next to the Rechnungsadresse fields, not a
// separate persisted concept).
companyName: string;
vatId: string;
// Rechnungsadresse is always a plain street address now — no
// deliveryMethod/packstationNumber/postNumber here, only on the
// shipping* override fields below (see CheckoutContent.tsx).
+10
View File
@@ -199,6 +199,10 @@ export type CustomerAddress = {
zip: string | null;
city: string | null;
country: string | null;
// Optional B2B profile default — see Customers.ts's own comment. Prefills
// /checkout's Firma/USt-IdNr. fields for a returning customer.
companyName: string | null;
vatId: string | null;
};
export type CustomerProfile = CustomerSummary & CustomerAddress;
@@ -217,6 +221,8 @@ type PayloadCustomerMe = {
zip: string | null;
city: string | null;
country: string | null;
companyName: string | null;
vatId: string | null;
cart: { product: number; productSlug: string; quantity: number; variantName: string | null }[] | null;
};
@@ -243,6 +249,8 @@ export async function getCustomerProfile(token: string): Promise<CustomerProfile
zip: u.zip,
city: u.city,
country: u.country,
companyName: u.companyName,
vatId: u.vatId,
};
}
@@ -259,6 +267,8 @@ export async function updateCustomerProfile(
zip: string;
city: string;
country: string;
companyName?: string;
vatId?: string;
},
): Promise<{ ok: true } | { ok: false; reason: string }> {
const res = await fetch(`${PAYLOAD_URL}/api/customers/${customerId}`, {
+6
View File
@@ -34,6 +34,10 @@ export type CreateOrderInput = {
customerFirstName: string;
customerLastName: string;
customerEmail: string;
// Optional B2B snapshot fields — see Orders.ts's own comment on why both
// are independently optional.
companyName?: string;
vatId?: string;
deliveryMethod: "address" | "packstation";
street?: string;
packstationNumber?: string;
@@ -87,6 +91,8 @@ export async function createOrder(input: CreateOrderInput): Promise<CreatedOrder
customerFirstName: input.customerFirstName,
customerLastName: input.customerLastName,
customerEmail: input.customerEmail,
companyName: input.companyName,
vatId: input.vatId,
deliveryMethod: input.deliveryMethod,
street: input.street,
packstationNumber: input.packstationNumber,
+15
View File
@@ -0,0 +1,15 @@
// Mirrors the backend's own USt-IdNr. validation exactly (Orders.ts/
// Customers.ts/CompanySettings.ts in the Payload repo) — kept as a plain
// client+server-safe helper here since this repo's frontend needs the same
// check twice (checkout's instant client-side pattern + api/checkout's own
// server-side re-validation, same "never trust the client" reasoning as
// every other checkout field).
const VAT_ID_PATTERN = /^[A-Z]{2}[A-Z0-9]{2,12}$/;
export function normalizeVatId(value: string): string {
return value.toUpperCase().trim();
}
export function isValidVatId(value: string): boolean {
return VAT_ID_PATTERN.test(value);
}