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:
@@ -6,6 +6,7 @@ const product = (overrides: Partial<RawProduct> = {}): RawProduct => ({
|
||||
id: 1,
|
||||
slug: "starter-set",
|
||||
name: "Starter-Set",
|
||||
sku: null,
|
||||
price: 29.9,
|
||||
active: true,
|
||||
image: null,
|
||||
|
||||
@@ -36,6 +36,9 @@ export type CheckoutDraft = {
|
||||
shippingZip: string;
|
||||
shippingCity: string;
|
||||
shippingCountry: string;
|
||||
shippingCompanyName: string;
|
||||
shippingContactEmail: string;
|
||||
shippingContactPhone: string;
|
||||
newsletterOptIn: boolean;
|
||||
shippingMethodId: number | null;
|
||||
paymentMethodId: number | null;
|
||||
|
||||
@@ -203,6 +203,23 @@ export type CustomerAddress = {
|
||||
// /checkout's Firma/USt-IdNr. fields for a returning customer.
|
||||
companyName: string | null;
|
||||
vatId: string | null;
|
||||
// Optional second/shipping address — see Customers.ts's "Lieferadresse"
|
||||
// tab. Prefills /checkout's "Abweichende Lieferadresse" section once
|
||||
// hasDifferentShippingAddress is set here; still fully overwritable per
|
||||
// order (Orders keeps its own shipping* snapshot regardless).
|
||||
hasDifferentShippingAddress: boolean;
|
||||
shippingFirstName: string | null;
|
||||
shippingLastName: string | null;
|
||||
shippingCompanyName: string | null;
|
||||
shippingDeliveryMethod: "address" | "packstation" | null;
|
||||
shippingStreet: string | null;
|
||||
shippingPackstationNumber: string | null;
|
||||
shippingPostNumber: string | null;
|
||||
shippingZip: string | null;
|
||||
shippingCity: string | null;
|
||||
shippingCountry: string | null;
|
||||
shippingContactEmail: string | null;
|
||||
shippingContactPhone: string | null;
|
||||
};
|
||||
|
||||
export type CustomerProfile = CustomerSummary & CustomerAddress;
|
||||
@@ -223,6 +240,19 @@ type PayloadCustomerMe = {
|
||||
country: string | null;
|
||||
companyName: string | null;
|
||||
vatId: string | null;
|
||||
hasDifferentShippingAddress: boolean | null;
|
||||
shippingFirstName: string | null;
|
||||
shippingLastName: string | null;
|
||||
shippingCompanyName: string | null;
|
||||
shippingDeliveryMethod: "address" | "packstation" | null;
|
||||
shippingStreet: string | null;
|
||||
shippingPackstationNumber: string | null;
|
||||
shippingPostNumber: string | null;
|
||||
shippingZip: string | null;
|
||||
shippingCity: string | null;
|
||||
shippingCountry: string | null;
|
||||
shippingContactEmail: string | null;
|
||||
shippingContactPhone: string | null;
|
||||
cart: { product: number; productSlug: string; quantity: number; variantName: string | null }[] | null;
|
||||
};
|
||||
|
||||
@@ -251,6 +281,19 @@ export async function getCustomerProfile(token: string): Promise<CustomerProfile
|
||||
country: u.country,
|
||||
companyName: u.companyName,
|
||||
vatId: u.vatId,
|
||||
hasDifferentShippingAddress: Boolean(u.hasDifferentShippingAddress),
|
||||
shippingFirstName: u.shippingFirstName,
|
||||
shippingLastName: u.shippingLastName,
|
||||
shippingCompanyName: u.shippingCompanyName,
|
||||
shippingDeliveryMethod: u.shippingDeliveryMethod,
|
||||
shippingStreet: u.shippingStreet,
|
||||
shippingPackstationNumber: u.shippingPackstationNumber,
|
||||
shippingPostNumber: u.shippingPostNumber,
|
||||
shippingZip: u.shippingZip,
|
||||
shippingCity: u.shippingCity,
|
||||
shippingCountry: u.shippingCountry,
|
||||
shippingContactEmail: u.shippingContactEmail,
|
||||
shippingContactPhone: u.shippingContactPhone,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,6 +312,19 @@ export async function updateCustomerProfile(
|
||||
country: string;
|
||||
companyName?: string;
|
||||
vatId?: string;
|
||||
hasDifferentShippingAddress?: boolean;
|
||||
shippingFirstName?: string;
|
||||
shippingLastName?: string;
|
||||
shippingCompanyName?: string;
|
||||
shippingDeliveryMethod?: "address" | "packstation";
|
||||
shippingStreet?: string;
|
||||
shippingPackstationNumber?: string;
|
||||
shippingPostNumber?: string;
|
||||
shippingZip?: string;
|
||||
shippingCity?: string;
|
||||
shippingCountry?: string;
|
||||
shippingContactEmail?: string;
|
||||
shippingContactPhone?: string;
|
||||
},
|
||||
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/customers/${customerId}`, {
|
||||
@@ -495,6 +551,7 @@ export type CustomerOrderDetail = CustomerOrder & {
|
||||
hasDifferentShippingAddress: boolean;
|
||||
shippingFirstName: string | null;
|
||||
shippingLastName: string | null;
|
||||
shippingCompanyName: string | null;
|
||||
shippingDeliveryMethod: "address" | "packstation" | null;
|
||||
shippingStreet: string | null;
|
||||
shippingPackstationNumber: string | null;
|
||||
@@ -502,6 +559,8 @@ export type CustomerOrderDetail = CustomerOrder & {
|
||||
shippingZip: string | null;
|
||||
shippingCity: string | null;
|
||||
shippingCountry: string | null;
|
||||
shippingContactEmail: string | null;
|
||||
shippingContactPhone: string | null;
|
||||
subtotal: number;
|
||||
shippingCost: number;
|
||||
shippingMethodTitle: string;
|
||||
@@ -521,6 +580,7 @@ export type CustomerOrderItem = {
|
||||
bundleContents: string | null;
|
||||
variantName: string | null;
|
||||
returnQuantity: number;
|
||||
sku: string | null;
|
||||
};
|
||||
|
||||
// Access control (Orders.ts) already scopes a customer's own JWT to only
|
||||
|
||||
@@ -196,6 +196,7 @@ export type OrderConfirmationItem = {
|
||||
bundleContents?: string | null;
|
||||
variantName?: string | null;
|
||||
taxRatePercent: number;
|
||||
sku?: string | null;
|
||||
};
|
||||
export type OrderConfirmationData = {
|
||||
orderNumber: string;
|
||||
@@ -249,7 +250,7 @@ export function renderOrderConfirmationHtml(template: EmailTemplateContent, orde
|
||||
: `<div style="width:44px;height:44px;border-radius:6px;background:${BG_MUTED};"></div>`
|
||||
}
|
||||
</td>
|
||||
<td style="padding:10px 0 10px 12px;border-bottom:1px solid ${BORDER};font-size:14px;color:${TEXT_PRIMARY};">${escapeHtml(item.productName)}${item.variantName ? ` (${escapeHtml(item.variantName)})` : ""} <span style="color:${TEXT_MUTED};">× ${item.quantity}</span>${item.bundleContents ? `<br/><span style="font-size:12px;color:${TEXT_MUTED};">${escapeHtml(item.bundleContents)}</span>` : ""}</td>
|
||||
<td style="padding:10px 0 10px 12px;border-bottom:1px solid ${BORDER};font-size:14px;color:${TEXT_PRIMARY};">${escapeHtml(item.productName)}${item.variantName ? ` (${escapeHtml(item.variantName)})` : ""} <span style="color:${TEXT_MUTED};">× ${item.quantity}</span>${item.bundleContents ? `<br/><span style="font-size:12px;color:${TEXT_MUTED};">${escapeHtml(item.bundleContents)}</span>` : ""}${item.sku ? `<br/><span style="font-size:12px;color:${TEXT_MUTED};">Art.-Nr. ${escapeHtml(item.sku)}</span>` : ""}</td>
|
||||
<td style="padding:10px 0;border-bottom:1px solid ${BORDER};text-align:right;white-space:nowrap;font-size:14px;color:${TEXT_PRIMARY};">${formatPrice(item.quantity * item.unitPrice)}</td>
|
||||
</tr>`,
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ export type OrderConfirmationEmailData = OrderConfirmationData & {
|
||||
hasDifferentShippingAddress?: boolean;
|
||||
shippingFirstName?: string | null;
|
||||
shippingLastName?: string | null;
|
||||
shippingCompanyName?: string | null;
|
||||
shippingDeliveryMethod?: "address" | "packstation" | null;
|
||||
shippingStreet?: string | null;
|
||||
shippingPackstationNumber?: string | null;
|
||||
@@ -34,6 +35,8 @@ export type OrderConfirmationEmailData = OrderConfirmationData & {
|
||||
shippingZip?: string | null;
|
||||
shippingCity?: string | null;
|
||||
shippingCountry?: string | null;
|
||||
shippingContactEmail?: string | null;
|
||||
shippingContactPhone?: string | null;
|
||||
paymentMethodTitle: string;
|
||||
};
|
||||
|
||||
@@ -93,6 +96,7 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
|
||||
hasDifferentShippingAddress: order.hasDifferentShippingAddress ?? false,
|
||||
shippingFirstName: order.shippingFirstName,
|
||||
shippingLastName: order.shippingLastName,
|
||||
shippingCompanyName: order.shippingCompanyName,
|
||||
shippingDeliveryMethod: order.shippingDeliveryMethod,
|
||||
shippingStreet: order.shippingStreet,
|
||||
shippingPackstationNumber: order.shippingPackstationNumber,
|
||||
@@ -100,6 +104,8 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
|
||||
shippingZip: order.shippingZip,
|
||||
shippingCity: order.shippingCity,
|
||||
shippingCountry: order.shippingCountry,
|
||||
shippingContactEmail: order.shippingContactEmail,
|
||||
shippingContactPhone: order.shippingContactPhone,
|
||||
paymentMethodTitle: order.paymentMethodTitle,
|
||||
items: order.items.map((i) => ({
|
||||
productName: i.productName,
|
||||
@@ -109,6 +115,7 @@ export async function sendOrderConfirmationEmail(order: OrderConfirmationEmailDa
|
||||
bundleContents: i.bundleContents ?? null,
|
||||
variantName: i.variantName ?? null,
|
||||
imageUrl: i.imageUrl ?? null,
|
||||
sku: i.sku ?? null,
|
||||
})),
|
||||
subtotal: order.subtotal,
|
||||
shippingCost: order.shippingCost,
|
||||
|
||||
@@ -27,6 +27,11 @@ export type OrderItemInput = {
|
||||
taxRatePercent: number;
|
||||
bundleContents: string | null;
|
||||
variantName: string | null;
|
||||
// Resolved by the caller (api/checkout/route.ts): the variant's own sku
|
||||
// if one was selected, else the product's sku, else null — same
|
||||
// "variant overrides product" precedence as unitPrice/taxRatePercent
|
||||
// elsewhere in this checkout flow.
|
||||
sku: string | null;
|
||||
};
|
||||
|
||||
export type CreateOrderInput = {
|
||||
@@ -67,11 +72,23 @@ export type CreateOrderInput = {
|
||||
shippingZip?: string;
|
||||
shippingCity?: string;
|
||||
shippingCountry?: string;
|
||||
// Optional, mirrors companyName above — no shipping-side vatId (billing-
|
||||
// only concept). Contact email/phone have no billing-side equivalent:
|
||||
// they're handed to the shipping carrier, never used for customer
|
||||
// communication.
|
||||
shippingCompanyName?: string;
|
||||
shippingContactEmail?: string;
|
||||
shippingContactPhone?: string;
|
||||
newsletterOptIn: boolean;
|
||||
items: OrderItemInput[];
|
||||
subtotal: number;
|
||||
shippingCost: number;
|
||||
shippingMethodTitle: string;
|
||||
// Numeric ShippingMethod id, not the same as shippingMethodTitle's frozen
|
||||
// text snapshot — lets pollCarrierTracking.ts resolve order → shipping
|
||||
// method → carrier without a fragile title-text match. See Orders.ts's
|
||||
// own comment on why both fields exist side by side.
|
||||
shippingMethod: number;
|
||||
paymentMethodTitle: string;
|
||||
discountCode: string | null;
|
||||
discountAmount: number;
|
||||
@@ -140,6 +157,9 @@ export async function createOrder(input: CreateOrderInput): Promise<CreatedOrder
|
||||
shippingZip: input.shippingZip,
|
||||
shippingCity: input.shippingCity,
|
||||
shippingCountry: input.shippingCountry,
|
||||
shippingCompanyName: input.shippingCompanyName,
|
||||
shippingContactEmail: input.shippingContactEmail,
|
||||
shippingContactPhone: input.shippingContactPhone,
|
||||
newsletterOptIn: input.newsletterOptIn,
|
||||
items: input.items.map((i) => ({
|
||||
product: i.productId,
|
||||
@@ -149,10 +169,12 @@ export async function createOrder(input: CreateOrderInput): Promise<CreatedOrder
|
||||
taxRatePercent: i.taxRatePercent,
|
||||
bundleContents: i.bundleContents,
|
||||
variantName: i.variantName,
|
||||
sku: i.sku,
|
||||
})),
|
||||
subtotal: input.subtotal,
|
||||
shippingCost: input.shippingCost,
|
||||
shippingMethodTitle: input.shippingMethodTitle,
|
||||
shippingMethod: input.shippingMethod,
|
||||
paymentMethodTitle: input.paymentMethodTitle,
|
||||
discountCode: input.discountCode,
|
||||
discountAmount: input.discountAmount,
|
||||
|
||||
+8
-8
@@ -21,7 +21,7 @@ export type BlogPost = {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
category: string;
|
||||
categories: string[];
|
||||
readTime: number;
|
||||
excerpt: string;
|
||||
thumbnail: string | null;
|
||||
@@ -33,7 +33,7 @@ type PayloadPost = {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
category: { name: string } | number | null;
|
||||
categories: ({ name: string } | number)[];
|
||||
readTime: number;
|
||||
excerpt: string;
|
||||
thumbnail: { url: string } | number | null;
|
||||
@@ -73,10 +73,9 @@ export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
slug: post.slug,
|
||||
category:
|
||||
typeof post.category === "object" && post.category
|
||||
? post.category.name
|
||||
: "",
|
||||
categories: (post.categories ?? [])
|
||||
.map((c) => (typeof c === "object" && c ? c.name : null))
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
readTime: post.readTime,
|
||||
excerpt: post.excerpt,
|
||||
thumbnail:
|
||||
@@ -125,8 +124,9 @@ export function mapPayloadPost(doc: PayloadPostDetail): PostDetail {
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
slug: doc.slug,
|
||||
category:
|
||||
typeof doc.category === "object" && doc.category ? doc.category.name : "",
|
||||
categories: (doc.categories ?? [])
|
||||
.map((c) => (typeof c === "object" && c ? c.name : null))
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
readTime: doc.readTime,
|
||||
excerpt: doc.excerpt,
|
||||
thumbnail:
|
||||
|
||||
@@ -20,6 +20,7 @@ export type RawProduct = {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
sku: string | null;
|
||||
price: number;
|
||||
active: boolean;
|
||||
image: { url: string } | number | null;
|
||||
|
||||
Reference in New Issue
Block a user