Honor Products.noShippingCost across cart, checkout, and product pages
- api/checkout/route.ts: authoritative shipping charge is 0 whenever every cart line opts out via noShippingCost, regardless of the free-shipping threshold. - Cart/checkout order summaries: the whole "Versand" line (cost, free- shipping note, delivery time) is hidden entirely rather than showing "Kostenlos" — that's a different state from hitting the threshold. - ProductSpotlight/Pricing/TodoKartenHero: "zzgl. Versand" and delivery- time hints drop for an exempted product's own page. - /versand + its shared modal: one clarifying sentence that digital products are exempt. - Widerrufsformular link: opens inline in a new tab (no forced download), arrow icon changed from a download glyph to a plain right arrow to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ const product = (overrides: Partial<RawProduct> = {}): RawProduct => ({
|
||||
active: true,
|
||||
image: null,
|
||||
taxRatePercent: null,
|
||||
noShippingCost: false,
|
||||
bundleItems: null,
|
||||
variants: null,
|
||||
trackInventory: false,
|
||||
|
||||
@@ -22,6 +22,7 @@ const product = (overrides: Partial<Product> = {}): Product => ({
|
||||
lowStock: false,
|
||||
maxQty: null,
|
||||
taxRatePercent: null,
|
||||
noShippingCost: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,16 @@ export function computeSubtotal(items: CartLine[]): number {
|
||||
return items.reduce((sum, { entry, product }) => sum + entry.qty * effectivePrice(entry, product), 0);
|
||||
}
|
||||
|
||||
// A cart only needs a shipping line at all if at least one item doesn't
|
||||
// opt out via Products.noShippingCost (e.g. a purely digital download) —
|
||||
// mirrors api/checkout/route.ts's own hasShippableItem check, which is
|
||||
// the actual charged amount; this is only the storefront's estimate/
|
||||
// display before that. A single non-exempt item still triggers normal
|
||||
// shipping for the whole cart, this never partially discounts it.
|
||||
export function cartHasShippableItem(items: CartLine[]): boolean {
|
||||
return items.some(({ product }) => !product.noShippingCost);
|
||||
}
|
||||
|
||||
export type CartTotals = {
|
||||
subtotal: number;
|
||||
/** compareAtPrice-based per-product savings — already excluded from
|
||||
|
||||
@@ -207,6 +207,13 @@ export type Product = {
|
||||
// storefront; the actual rate used for order totals is resolved and
|
||||
// snapshotted server-side at checkout (api/checkout/route.ts).
|
||||
taxRatePercent: number | null;
|
||||
// No shipping cost for this product at all (e.g. a digital download) —
|
||||
// never shows "zzgl. Versand" on its own product page, and doesn't count
|
||||
// toward "does this cart need a shipping line" (lib/cartTotals.ts's
|
||||
// cartHasShippableItem()). A cart with even one item that does NOT have
|
||||
// this set still gets charged/shown the normal shipping cost — this only
|
||||
// exempts the individual product, not the whole cart.
|
||||
noShippingCost: boolean;
|
||||
variants: { name: string; priceOverride: number | null; outOfStock: boolean; lowStock: boolean; maxQty: number | null }[];
|
||||
};
|
||||
|
||||
@@ -231,6 +238,7 @@ type PayloadProduct = {
|
||||
allowBackorder: boolean;
|
||||
lowStockThreshold: number | null;
|
||||
taxRatePercent: number | null;
|
||||
noShippingCost: boolean;
|
||||
variants:
|
||||
| {
|
||||
name: string;
|
||||
@@ -291,6 +299,7 @@ export function mapPayloadProduct(product: PayloadProduct): Product {
|
||||
lowStock: isLowStock(product.trackInventory, product.stock, product.lowStockThreshold),
|
||||
maxQty: maxPurchasableQty(product.trackInventory, product.stock, product.allowBackorder),
|
||||
taxRatePercent: product.taxRatePercent ?? null,
|
||||
noShippingCost: product.noShippingCost,
|
||||
variants: (product.variants ?? []).map((v) => ({
|
||||
name: v.name,
|
||||
priceOverride: v.priceOverride,
|
||||
|
||||
@@ -24,6 +24,7 @@ export type RawProduct = {
|
||||
active: boolean;
|
||||
image: { url: string } | number | null;
|
||||
taxRatePercent: number | null;
|
||||
noShippingCost: boolean;
|
||||
bundleItems: { product: { id: number; name: string } | number; quantity: number }[] | null;
|
||||
variants: RawProductVariant[] | null;
|
||||
trackInventory: boolean;
|
||||
|
||||
Reference in New Issue
Block a user