diff --git a/app/api/checkout/route.ts b/app/api/checkout/route.ts index e689437..a2b4e19 100644 --- a/app/api/checkout/route.ts +++ b/app/api/checkout/route.ts @@ -198,12 +198,19 @@ export async function POST(request: Request) { }); } const subtotal = roundMoney(items.reduce((sum, i) => sum + i.quantity * i.unitPrice, 0)); + // Products.noShippingCost — a cart made up entirely of items that opt + // out of shipping costs (e.g. purely digital downloads) never gets + // charged shipping at all, regardless of the free-shipping threshold. + // A single item WITHOUT the flag still triggers normal shipping for the + // whole order — this only exempts a product, never the whole cart just + // because it contains an exempt item. + const hasShippableItem = body.cart.some((line) => !productsBySlug.get(line.id)?.noShippingCost); const shippingMethods = await getShippingMethods(); const shippingMethod = shippingMethods.find((m) => m.id === body.shippingMethodId); if (!shippingMethod) return NextResponse.json({ ok: false, reason: "Versandart ist ungültig." }, { status: 400 }); const freeShipping = shippingMethod.freeShippingThreshold != null && subtotal >= shippingMethod.freeShippingThreshold; - const shippingCost = freeShipping ? 0 : shippingMethod.price; + const shippingCost = !hasShippableItem || freeShipping ? 0 : shippingMethod.price; const paymentMethods = await getPaymentMethods(); const paymentMethod = paymentMethods.find((m) => m.id === body.paymentMethodId); diff --git a/app/cart/components/CartContent.tsx b/app/cart/components/CartContent.tsx index a82ea0b..a5dac29 100644 --- a/app/cart/components/CartContent.tsx +++ b/app/cart/components/CartContent.tsx @@ -7,7 +7,7 @@ import Image from "next/image"; import { useCart, removeFromCart, setQuantity } from "../../lib/cart"; import { useProducts } from "../../lib/products"; import { useDiscount, applyDiscount, clearDiscount } from "../../lib/discount"; -import { computeSubtotal, computeCartTotals, effectivePrice, effectiveTaxRate } from "../../lib/cartTotals"; +import { computeSubtotal, computeCartTotals, effectivePrice, effectiveTaxRate, cartHasShippableItem } from "../../lib/cartTotals"; import { computeTaxBreakdown } from "@einfach-produktiv/invoicing"; import { formatPrice, discountPercent } from "../../lib/format"; import { Reveal } from "../../components/Reveal"; @@ -77,7 +77,7 @@ export function CartContent({ const subtotal = computeSubtotal(items); const shipping = - items.length === 0 || (freeShippingThreshold !== null && subtotal >= freeShippingThreshold) + items.length === 0 || !cartHasShippableItem(items) || (freeShippingThreshold !== null && subtotal >= freeShippingThreshold) ? 0 : shippingCost; const { totalSavings, discountAmount, total } = computeCartTotals(items, shipping, discount); @@ -392,31 +392,40 @@ export function CartContent({ )}
- {shipping === 0 && freeShippingThreshold !== null - ? `ab ${formatPrice(freeShippingThreshold)} innerhalb Deutschlands` - : "innerhalb Deutschlands"} -
-- Lieferzeit {shippingSettings.totalDays.min}–{shippingSettings.totalDays.max} Werktage -
+ {/* Hidden entirely (not just "Kostenlos") when nothing in + the cart actually triggers shipping at all — that's a + different state from hitting the free-shipping + threshold, which is still a real promotional message + worth showing. */} + {cartHasShippableItem(items) && ( + <> ++ {shipping === 0 && freeShippingThreshold !== null + ? `ab ${formatPrice(freeShippingThreshold)} innerhalb Deutschlands` + : "innerhalb Deutschlands"} +
++ Lieferzeit {shippingSettings.totalDays.min}–{shippingSettings.totalDays.max} Werktage +
+ > + )}+ {shipping === 0 && selectedShipping?.freeShippingThreshold != null + ? `ab ${formatPrice(selectedShipping.freeShippingThreshold)} innerhalb Deutschlands` + : (selectedShipping?.description ?? "innerhalb Deutschlands")} +
++ Lieferzeit {shippingSettings.totalDays.min}–{shippingSettings.totalDays.max} Werktage +
- {shipping === 0 && selectedShipping?.freeShippingThreshold != null - ? `ab ${formatPrice(selectedShipping.freeShippingThreshold)} innerhalb Deutschlands` - : (selectedShipping?.description ?? "innerhalb Deutschlands")} -
-- Lieferzeit {shippingSettings.totalDays.min}–{shippingSettings.totalDays.max} Werktage -
-{formatPrice(product.price)}
{kleinunternehmer ? "zzgl. Versand" : `inkl. ${taxRate}% MwSt. zzgl. Versand`}
-- Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands -
+ {(() => { + const priceHint = kleinunternehmer + ? product.noShippingCost + ? null + : "zzgl. Versand" + : `inkl. ${taxRate}% MwSt.${product.noShippingCost ? "" : " zzgl. Versand"}`; + return priceHint &&{priceHint}
; + })()} + {!product.noShippingCost && ( ++ Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands +
+ )} {/* Single product, no grid siblings to stay equal-height with (unlike ProductGrid.tsx/RelatedProducts.tsx), so this can be diff --git a/app/lib/__tests__/bundleContents.test.ts b/app/lib/__tests__/bundleContents.test.ts index 48dd115..11c0073 100644 --- a/app/lib/__tests__/bundleContents.test.ts +++ b/app/lib/__tests__/bundleContents.test.ts @@ -10,6 +10,7 @@ const product = (overrides: Partial{formatPrice(product.price)}
-{kleinunternehmer ? "zzgl. Versand" : `inkl. ${taxRate}% MwSt. zzgl. Versand`}
-- Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands -
+ {(() => { + const priceHint = kleinunternehmer + ? product.noShippingCost + ? null + : "zzgl. Versand" + : `inkl. ${taxRate}% MwSt.${product.noShippingCost ? "" : " zzgl. Versand"}`; + return priceHint &&{priceHint}
; + })()} + {!product.noShippingCost && ( ++ Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands +
+ )} {/* Single product, no grid siblings to stay equal-height with — plain conditional line, same reasoning as ProductSpotlight.tsx. */} diff --git a/app/todo-cards/components/TodoKartenHero.tsx b/app/todo-cards/components/TodoKartenHero.tsx index 34d1da6..d49532c 100644 --- a/app/todo-cards/components/TodoKartenHero.tsx +++ b/app/todo-cards/components/TodoKartenHero.tsx @@ -124,12 +124,21 @@ export async function TodoKartenHero() {{formatPrice(product.compareAtPrice!)}
)}{formatPrice(product.price)}
-{kleinunternehmer ? "zzgl. Versand" : `inkl. ${taxRate}% MwSt. zzgl. Versand`}
+ {(() => { + const priceHint = kleinunternehmer + ? product.noShippingCost + ? null + : "zzgl. Versand" + : `inkl. ${taxRate}% MwSt.${product.noShippingCost ? "" : " zzgl. Versand"}`; + return priceHint &&{priceHint}
; + })()} )} -- Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands -
+ {!product?.noShippingCost && ( ++ Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands +
+ )} {/* Single product, no grid siblings to stay equal-height with — plain conditional line, same reasoning as ProductSpotlight.tsx/Pricing.tsx. */} {anyLowStock &&Nur noch wenige verfügbar
} diff --git a/app/versand/components/VersandSections.tsx b/app/versand/components/VersandSections.tsx index 5231ad5..389adc6 100644 --- a/app/versand/components/VersandSections.tsx +++ b/app/versand/components/VersandSections.tsx @@ -77,6 +77,10 @@ export function VersandSections({ wir kostenlos.Alle angegebenen Preise verstehen sich inklusive der gesetzlichen Mehrwertsteuer.
++ Rein digitale Produkte (z.B. Downloads) verursachen keine Versandkosten und sind von den + oben genannten Beträgen ausgenommen. +