Skip to content

Commit 39292bd

Browse files
Musiker15claude
andcommitted
fix(sales): show original price and correct discount % for active sales
Tebex's Headless /packages API reports an active (user-specific) sale only with an authenticated basket ident, and in a counter-intuitive shape: base_price is the post-discount price, discount is the amount removed, total_price is the payable price. The original price is base_price + discount. The previous code treated base_price as the original and dropped discount, so the strikethrough equaled the sale price and the badge rendered 'SALE -0%'. Add a shared resolveDisplayPrice helper (lib/price.ts): original = base_price + discount, price = total_price, with a float guard against rounding-induced -0%. Thread discount through the sale-prices store and SalePriceFetcher, and use the helper in both PackagePrice and PackageCard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0957320 commit 39292bd

5 files changed

Lines changed: 63 additions & 25 deletions

File tree

components/SalePriceFetcher.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ export function SalePriceFetcher() {
1919
const res = await fetch(`/api/packages?ident=${ident}`, { cache: 'no-store' })
2020
if (!res.ok) return
2121
const data = await res.json()
22-
const packages: { id: number; base_price: number; total_price?: number }[] = data.data ?? []
22+
const packages: { id: number; base_price: number; total_price?: number; discount?: number }[] = data.data ?? []
2323

24-
const prices: Record<number, { base_price: number; total_price: number }> = {}
24+
const prices: Record<number, { base_price: number; total_price: number; discount: number }> = {}
2525
for (const pkg of packages) {
2626
prices[pkg.id] = {
2727
base_price: pkg.base_price ?? 0,
2828
total_price: pkg.total_price ?? pkg.base_price ?? 0,
29+
discount: pkg.discount ?? 0,
2930
}
3031
}
3132
setPrices(prices)

components/packages/PackageCard.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Link from 'next/link'
55
import { ShoppingCart, Loader2, LogIn } from 'lucide-react'
66
import { useCart } from '@/lib/useCart'
77
import { useSalePricesStore } from '@/store/salePrices'
8+
import { resolveDisplayPrice } from '@/lib/price'
89
import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/Card'
910
import { Badge, type BadgeVariant } from '@/components/ui/Badge'
1011
import { Button } from '@/components/ui/Button'
@@ -22,15 +23,9 @@ export function PackageCard({ pkg, tags, badges, description }: Props) {
2223
const { addPackage, isLoading, username } = useCart()
2324
const { prices } = useSalePricesStore()
2425

25-
const saleData = prices[pkg.id]
26-
const basePrice = saleData?.base_price ?? pkg.base_price ?? 0
27-
const totalPrice = saleData?.total_price ?? pkg.total_price ?? basePrice
28-
const hasDiscount = totalPrice < basePrice && basePrice > 0
29-
const isFree = basePrice === 0
26+
const { original, price, isFree, hasDiscount, discountPct } =
27+
resolveDisplayPrice(pkg.base_price ?? 0, pkg.total_price ?? pkg.base_price ?? 0, prices[pkg.id])
3028
const needsLogin = !username && !isFree
31-
const discountPct = hasDiscount && basePrice > 0
32-
? Math.round(((basePrice - totalPrice) / basePrice) * 100)
33-
: 0
3429

3530
return (
3631
<Card hoverLift className="group flex flex-col overflow-hidden">
@@ -96,11 +91,11 @@ export function PackageCard({ pkg, tags, badges, description }: Props) {
9691
<div className="flex flex-col leading-none">
9792
{hasDiscount && (
9893
<span className="font-mono text-xs text-[var(--color-muted-foreground)] line-through">
99-
{basePrice.toFixed(2)}
94+
{original.toFixed(2)}
10095
</span>
10196
)}
10297
<span className={`font-mono font-bold tracking-tight ${isFree ? 'text-xl text-[var(--color-muted-foreground)]' : 'text-2xl text-[var(--color-primary)]'}`}>
103-
{isFree ? 'Free' : `${totalPrice.toFixed(2)}€`}
98+
{isFree ? 'Free' : `${price.toFixed(2)}€`}
10499
</span>
105100
</div>
106101

components/packages/PackagePrice.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { useSalePricesStore } from '@/store/salePrices'
4+
import { resolveDisplayPrice } from '@/lib/price'
45
import { Badge } from '@/components/ui/Badge'
56

67
interface Props {
@@ -13,26 +14,20 @@ interface Props {
1314
export function PackagePrice({ packageId, basePrice, totalPrice, currency }: Props) {
1415
const { prices } = useSalePricesStore()
1516

16-
const saleData = prices[packageId]
17-
const effectiveBase = saleData?.base_price ?? basePrice
18-
const effectiveTotal = saleData?.total_price ?? totalPrice
19-
const hasDiscount = effectiveTotal < effectiveBase && effectiveBase > 0
20-
const isFree = effectiveBase === 0
21-
const discountPct = hasDiscount && effectiveBase > 0
22-
? Math.round(((effectiveBase - effectiveTotal) / effectiveBase) * 100)
23-
: 0
17+
const { original, price, isFree, hasDiscount, discountPct } =
18+
resolveDisplayPrice(basePrice, totalPrice, prices[packageId])
2419

2520
return (
2621
<div className="flex flex-wrap items-baseline gap-2">
2722
{hasDiscount && (
2823
<span className="font-mono text-sm text-[var(--color-muted-foreground)] line-through">
29-
{effectiveBase.toFixed(2)}
24+
{original.toFixed(2)}
3025
</span>
3126
)}
3227
<span
3328
className={`font-mono text-3xl font-bold tracking-tight ${isFree ? 'text-[var(--color-muted-foreground)]' : 'text-[var(--color-primary)]'}`}
3429
>
35-
{isFree ? 'Free' : `${effectiveTotal.toFixed(2)}€`}
30+
{isFree ? 'Free' : `${price.toFixed(2)}€`}
3631
</span>
3732
<span className="text-xs text-[var(--color-muted-foreground)]">{currency}</span>
3833
{hasDiscount && <Badge variant="sale">Sale −{discountPct}%</Badge>}

lib/price.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Zentrale Preis-/Sale-Auflösung für die Anzeige.
2+
//
3+
// Tebex meldet einen aktiven (ggf. user-spezifischen) Sale über die Headless-API
4+
// NUR mit authentifiziertem Basket-Ident – und in einer unintuitiven Form:
5+
// base_price = Preis NACH Rabatt (z. B. 17.994)
6+
// discount = Rabattbetrag in der Währung (z. B. 11.996)
7+
// total_price = zahlbarer Preis (z. B. 17.99)
8+
// Der ursprüngliche (Vor-Sale-)Preis ist also `base_price + discount`.
9+
//
10+
// Ohne Sale (bzw. ohne Basket-Kontext) gilt base_price == total_price und discount == 0.
11+
12+
export interface SaleData {
13+
base_price: number
14+
total_price: number
15+
discount: number
16+
}
17+
18+
export interface DisplayPrice {
19+
/** Ursprünglicher Katalogpreis (durchgestrichen, wenn rabattiert). */
20+
original: number
21+
/** Tatsächlich zu zahlender Preis. */
22+
price: number
23+
isFree: boolean
24+
hasDiscount: boolean
25+
/** Gerundeter Rabatt in Prozent. */
26+
discountPct: number
27+
}
28+
29+
export function resolveDisplayPrice(
30+
pkgBasePrice: number,
31+
pkgTotalPrice: number,
32+
sale?: SaleData,
33+
): DisplayPrice {
34+
// Mit aktivem Sale ist base_price bereits rabattiert → Original = base_price + discount.
35+
// Ohne Sale-Daten der Katalogpreis aus den Server-Props.
36+
const original = sale ? sale.base_price + sale.discount : pkgBasePrice
37+
const price = sale?.total_price ?? pkgTotalPrice
38+
const isFree = original === 0
39+
// Float-Guard: 17.994 vs. 17.99 darf NICHT als „Sale −0%" durchrutschen.
40+
const hasDiscount = !isFree && original > 0 && original - price > 0.005
41+
const discountPct = hasDiscount
42+
? Math.round(((original - price) / original) * 100)
43+
: 0
44+
45+
return { original, price, isFree, hasDiscount, discountPct }
46+
}

store/salePrices.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use client'
22

33
import { create } from 'zustand'
4+
import type { SaleData } from '@/lib/price'
45

56
// Stores sale prices fetched from Tebex with basket context
6-
// Key: package ID, Value: { base_price, total_price }
7+
// Key: package ID, Value: { base_price, total_price, discount }
78
interface SalePricesStore {
8-
prices: Record<number, { base_price: number; total_price: number }>
9-
setPrices: (prices: Record<number, { base_price: number; total_price: number }>) => void
9+
prices: Record<number, SaleData>
10+
setPrices: (prices: Record<number, SaleData>) => void
1011
clear: () => void
1112
}
1213

0 commit comments

Comments
 (0)