fix: stack promo offer discounts with promo group discounts

Redesign applyPromoDiscount to apply promo offer on top of promo group
prices instead of treating them as mutually exclusive. Calculate combined
discount percent from the deepest original price. Show promo banner in
confirm step regardless of existing server discounts.
This commit is contained in:
Fringg
2026-02-23 15:44:23 +03:00
parent 91d567f9cc
commit 321bedcb61

View File

@@ -89,24 +89,47 @@ export default function Subscription() {
// Helper to format price from kopeks // Helper to format price from kopeks
const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}`; const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}`;
// Helper to apply promo discount to a price // Helper to apply promo discount to a price, stacking with existing promo group discount
const applyPromoDiscount = ( const applyPromoDiscount = (
priceKopeks: number, priceKopeks: number,
hasExistingDiscount: boolean = false, existingOriginalPrice?: number | null,
): { ): {
price: number; price: number;
original: number | null; original: number | null;
percent: number | null; percent: number | null;
isPromoGroup: boolean;
} => { } => {
// Only apply promo discount if no existing discount (from promo group) and we have an active promo discount const hasExisting = (existingOriginalPrice ?? 0) > priceKopeks;
if (!activeDiscount?.is_active || !activeDiscount.discount_percent || hasExistingDiscount) { const hasPromo = !!activeDiscount?.is_active && !!activeDiscount.discount_percent;
return { price: priceKopeks, original: null, percent: null };
if (!hasExisting && !hasPromo) {
return { price: priceKopeks, original: null, percent: null, isPromoGroup: false };
} }
const discountedPrice = Math.round(priceKopeks * (1 - activeDiscount.discount_percent / 100));
let finalPrice = priceKopeks;
if (hasPromo) {
finalPrice = Math.round(priceKopeks * (1 - activeDiscount!.discount_percent! / 100));
}
if (hasExisting) {
// Promo group discount exists — calculate combined percent from deepest original
const combinedPercent = hasPromo
? Math.round((1 - finalPrice / existingOriginalPrice!) * 100)
: Math.round((1 - priceKopeks / existingOriginalPrice!) * 100);
return { return {
price: discountedPrice, price: finalPrice,
original: existingOriginalPrice!,
percent: combinedPercent,
isPromoGroup: true,
};
}
// Only promo offer discount (no promo group)
return {
price: finalPrice,
original: priceKopeks, original: priceKopeks,
percent: activeDiscount.discount_percent, percent: activeDiscount!.discount_percent!,
isPromoGroup: false,
}; };
}; };
@@ -2416,41 +2439,34 @@ export default function Subscription() {
const dailyPrice = const dailyPrice =
tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0; tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0;
const originalDailyPrice = tariff.original_daily_price_kopeks || 0; const originalDailyPrice = tariff.original_daily_price_kopeks || 0;
const hasExistingDailyDiscount = originalDailyPrice > dailyPrice;
if (dailyPrice > 0) { if (dailyPrice > 0) {
// Apply promo discount if no existing discount
const promoDaily = applyPromoDiscount( const promoDaily = applyPromoDiscount(
dailyPrice, dailyPrice,
hasExistingDailyDiscount, originalDailyPrice > dailyPrice ? originalDailyPrice : undefined,
); );
return ( return (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className="font-medium text-accent-400"> <span className="font-medium text-accent-400">
{formatPrice(promoDaily.price)} {formatPrice(promoDaily.price)}
</span> </span>
{/* Show original price from promo group or promo offer */} {promoDaily.original &&
{(hasExistingDailyDiscount || promoDaily.original) && ( promoDaily.original > promoDaily.price && (
<span className="text-xs text-dark-500 line-through"> <span className="text-xs text-dark-500 line-through">
{formatPrice( {formatPrice(promoDaily.original)}
hasExistingDailyDiscount
? originalDailyPrice
: promoDaily.original!,
)}
</span> </span>
)} )}
<span>{t('subscription.tariff.perDay')}</span> <span>{t('subscription.tariff.perDay')}</span>
{/* Show discount badge */} {/* Show discount badge */}
{tariff.daily_discount_percent && {promoDaily.percent && promoDaily.percent > 0 && (
tariff.daily_discount_percent > 0 ? ( <span
<span className="rounded bg-success-500/20 px-1.5 py-0.5 text-xs text-success-400"> className={`rounded px-1.5 py-0.5 text-xs ${
-{tariff.daily_discount_percent}% promoDaily.isPromoGroup
</span> ? 'bg-success-500/20 text-success-400'
) : ( : 'bg-orange-500/20 text-orange-400'
promoDaily.percent && ( }`}
<span className="rounded bg-orange-500/20 px-1.5 py-0.5 text-xs text-orange-400"> >
-{promoDaily.percent}% -{promoDaily.percent}%
</span> </span>
)
)} )}
</span> </span>
); );
@@ -2458,14 +2474,9 @@ export default function Subscription() {
// Period-based price // Period-based price
if (tariff.periods.length > 0) { if (tariff.periods.length > 0) {
const firstPeriod = tariff.periods[0]; const firstPeriod = tariff.periods[0];
const hasExistingDiscount = !!(
firstPeriod?.original_price_kopeks &&
firstPeriod.original_price_kopeks > firstPeriod.price_kopeks
);
// Apply promo discount if no existing discount
const promoPeriod = applyPromoDiscount( const promoPeriod = applyPromoDiscount(
firstPeriod?.price_kopeks || 0, firstPeriod?.price_kopeks || 0,
hasExistingDiscount, firstPeriod?.original_price_kopeks,
); );
return ( return (
<span className="flex flex-wrap items-center gap-2"> <span className="flex flex-wrap items-center gap-2">
@@ -2473,27 +2484,22 @@ export default function Subscription() {
<span className="font-medium text-accent-400"> <span className="font-medium text-accent-400">
{formatPrice(promoPeriod.price)} {formatPrice(promoPeriod.price)}
</span> </span>
{/* Show original price from promo group or promo offer */} {promoPeriod.original &&
{(hasExistingDiscount || promoPeriod.original) && ( promoPeriod.original > promoPeriod.price && (
<span className="text-xs text-dark-500 line-through"> <span className="text-xs text-dark-500 line-through">
{formatPrice( {formatPrice(promoPeriod.original)}
hasExistingDiscount
? firstPeriod.original_price_kopeks!
: promoPeriod.original!,
)}
</span> </span>
)} )}
{/* Show discount badge */} {promoPeriod.percent && promoPeriod.percent > 0 && (
{hasExistingDiscount && firstPeriod.discount_percent ? ( <span
<span className="rounded bg-success-500/20 px-1.5 py-0.5 text-xs text-success-400"> className={`rounded px-1.5 py-0.5 text-xs ${
-{firstPeriod.discount_percent}% promoPeriod.isPromoGroup
</span> ? 'bg-success-500/20 text-success-400'
) : ( : 'bg-orange-500/20 text-orange-400'
promoPeriod.percent && ( }`}
<span className="rounded bg-orange-500/20 px-1.5 py-0.5 text-xs text-orange-400"> >
-{promoPeriod.percent}% -{promoPeriod.percent}%
</span> </span>
)
)} )}
</span> </span>
); );
@@ -2702,24 +2708,17 @@ export default function Subscription() {
{selectedTariff.periods.length > 0 && !useCustomDays && ( {selectedTariff.periods.length > 0 && !useCustomDays && (
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-3"> <div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
{selectedTariff.periods.map((period) => { {selectedTariff.periods.map((period) => {
const hasExistingDiscount = !!(
period.original_price_kopeks &&
period.original_price_kopeks > period.price_kopeks
);
const promoPeriod = applyPromoDiscount( const promoPeriod = applyPromoDiscount(
period.price_kopeks, period.price_kopeks,
hasExistingDiscount, period.original_price_kopeks,
); );
const displayDiscount = hasExistingDiscount const displayDiscount = promoPeriod.percent;
? period.discount_percent const displayOriginal = promoPeriod.original;
: promoPeriod.percent;
const displayOriginal = hasExistingDiscount
? period.original_price_kopeks
: promoPeriod.original;
const displayPrice = promoPeriod.price; const displayPrice = promoPeriod.price;
const displayPerMonth = hasExistingDiscount const displayPerMonth =
? period.price_per_month_kopeks displayPrice !== period.price_kopeks
: Math.round(promoPeriod.price / (period.days / 30)); ? Math.round(displayPrice / Math.max(1, period.days / 30))
: period.price_per_month_kopeks;
return ( return (
<button <button
@@ -2737,7 +2736,7 @@ export default function Subscription() {
{displayDiscount && displayDiscount > 0 && ( {displayDiscount && displayDiscount > 0 && (
<div <div
className={`absolute -right-2 -top-2 rounded-full px-2 py-0.5 text-xs font-medium text-white ${ className={`absolute -right-2 -top-2 rounded-full px-2 py-0.5 text-xs font-medium text-white ${
hasExistingDiscount ? 'bg-success-500' : 'bg-orange-500' promoPeriod.isPromoGroup ? 'bg-success-500' : 'bg-orange-500'
}`} }`}
> >
-{displayDiscount}% -{displayDiscount}%
@@ -2821,14 +2820,15 @@ export default function Subscription() {
{(() => { {(() => {
const basePrice = const basePrice =
customDays * (selectedTariff.price_per_day_kopeks ?? 0); customDays * (selectedTariff.price_per_day_kopeks ?? 0);
const hasExistingDiscount = !!( const existingOriginal =
selectedTariff.original_price_per_day_kopeks && selectedTariff.original_price_per_day_kopeks &&
selectedTariff.original_price_per_day_kopeks > selectedTariff.original_price_per_day_kopeks >
(selectedTariff.price_per_day_kopeks ?? 0) (selectedTariff.price_per_day_kopeks ?? 0)
); ? customDays * selectedTariff.original_price_per_day_kopeks
: undefined;
const promoCustom = applyPromoDiscount( const promoCustom = applyPromoDiscount(
basePrice, basePrice,
hasExistingDiscount, existingOriginal,
); );
return ( return (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
@@ -2841,12 +2841,19 @@ export default function Subscription() {
<span className="font-medium text-accent-400"> <span className="font-medium text-accent-400">
{formatPrice(promoCustom.price)} {formatPrice(promoCustom.price)}
</span> </span>
{promoCustom.original && ( {promoCustom.original &&
promoCustom.original > promoCustom.price && (
<> <>
<span className="text-xs text-dark-500 line-through"> <span className="text-xs text-dark-500 line-through">
{formatPrice(promoCustom.original)} {formatPrice(promoCustom.original)}
</span> </span>
<span className="rounded bg-orange-500/20 px-1.5 py-0.5 text-xs text-orange-400"> <span
className={`rounded px-1.5 py-0.5 text-xs ${
promoCustom.isPromoGroup
? 'bg-success-500/20 text-success-400'
: 'bg-orange-500/20 text-orange-400'
}`}
>
-{promoCustom.percent}% -{promoCustom.percent}%
</span> </span>
</> </>
@@ -2956,14 +2963,20 @@ export default function Subscription() {
const basePeriodPrice = useCustomDays const basePeriodPrice = useCustomDays
? customDays * (selectedTariff.price_per_day_kopeks ?? 0) ? customDays * (selectedTariff.price_per_day_kopeks ?? 0)
: selectedTariffPeriod?.price_kopeks || 0; : selectedTariffPeriod?.price_kopeks || 0;
const hasExistingPeriodDiscount = const existingPeriodOriginal = useCustomDays
!useCustomDays && selectedTariffPeriod?.original_price_kopeks ? selectedTariff.original_price_per_day_kopeks &&
? selectedTariffPeriod.original_price_kopeks > selectedTariff.original_price_per_day_kopeks >
(selectedTariff.price_per_day_kopeks ?? 0)
? customDays * selectedTariff.original_price_per_day_kopeks
: undefined
: selectedTariffPeriod?.original_price_kopeks &&
selectedTariffPeriod.original_price_kopeks >
selectedTariffPeriod.price_kopeks selectedTariffPeriod.price_kopeks
: false; ? selectedTariffPeriod.original_price_kopeks
: undefined;
const promoPeriod = applyPromoDiscount( const promoPeriod = applyPromoDiscount(
basePeriodPrice, basePeriodPrice,
hasExistingPeriodDiscount, existingPeriodOriginal,
); );
const trafficPrice = const trafficPrice =
@@ -2988,7 +3001,8 @@ export default function Subscription() {
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{formatPrice(promoPeriod.price)}</span> <span>{formatPrice(promoPeriod.price)}</span>
{promoPeriod.original && ( {promoPeriod.original &&
promoPeriod.original > promoPeriod.price && (
<span className="text-xs text-dark-500 line-through"> <span className="text-xs text-dark-500 line-through">
{formatPrice(promoPeriod.original)} {formatPrice(promoPeriod.original)}
</span> </span>
@@ -3035,14 +3049,10 @@ export default function Subscription() {
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{formatPrice(promoPeriod.price)}</span> <span>{formatPrice(promoPeriod.price)}</span>
{(hasExistingPeriodDiscount || {promoPeriod.original &&
promoPeriod.original) && ( promoPeriod.original > promoPeriod.price && (
<span className="text-xs text-dark-500 line-through"> <span className="text-xs text-dark-500 line-through">
{formatPrice( {formatPrice(promoPeriod.original)}
hasExistingPeriodDiscount
? selectedTariffPeriod.original_price_kopeks!
: promoPeriod.original!,
)}
</span> </span>
)} )}
</div> </div>
@@ -3177,19 +3187,10 @@ export default function Subscription() {
{currentStep === 'period' && classicOptions && ( {currentStep === 'period' && classicOptions && (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
{classicOptions.periods.map((period) => { {classicOptions.periods.map((period) => {
const hasExistingDiscount = !!(
period.discount_percent && period.discount_percent > 0
);
const promoPeriod = applyPromoDiscount( const promoPeriod = applyPromoDiscount(
period.price_kopeks, period.price_kopeks,
hasExistingDiscount, period.original_price_kopeks,
); );
const displayDiscount = hasExistingDiscount
? period.discount_percent
: promoPeriod.percent;
const displayOriginal = hasExistingDiscount
? period.original_price_kopeks
: promoPeriod.original;
return ( return (
<button <button
@@ -3219,13 +3220,13 @@ export default function Subscription() {
: '' : ''
}`} }`}
> >
{displayDiscount && displayDiscount > 0 && ( {promoPeriod.percent && promoPeriod.percent > 0 && (
<div <div
className={`absolute right-2 top-2 z-10 rounded-full px-2 py-0.5 text-xs font-medium text-white shadow-sm ${ className={`absolute right-2 top-2 z-10 rounded-full px-2 py-0.5 text-xs font-medium text-white shadow-sm ${
hasExistingDiscount ? 'bg-success-500' : 'bg-orange-500' promoPeriod.isPromoGroup ? 'bg-success-500' : 'bg-orange-500'
}`} }`}
> >
-{displayDiscount}% -{promoPeriod.percent}%
</div> </div>
)} )}
<div className="text-lg font-semibold text-dark-100">{period.label}</div> <div className="text-lg font-semibold text-dark-100">{period.label}</div>
@@ -3233,9 +3234,9 @@ export default function Subscription() {
<span className="font-medium text-accent-400"> <span className="font-medium text-accent-400">
{formatPrice(promoPeriod.price)} {formatPrice(promoPeriod.price)}
</span> </span>
{displayOriginal && displayOriginal > promoPeriod.price && ( {promoPeriod.original && promoPeriod.original > promoPeriod.price && (
<span className="text-sm text-dark-500 line-through"> <span className="text-sm text-dark-500 line-through">
{formatPrice(displayOriginal)} {formatPrice(promoPeriod.original)}
</span> </span>
)} )}
</div> </div>
@@ -3249,12 +3250,9 @@ export default function Subscription() {
{currentStep === 'traffic' && selectedPeriod?.traffic.options && ( {currentStep === 'traffic' && selectedPeriod?.traffic.options && (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{selectedPeriod.traffic.options.map((option) => { {selectedPeriod.traffic.options.map((option) => {
const hasExistingDiscount = !!(
option.discount_percent && option.discount_percent > 0
);
const promoTraffic = applyPromoDiscount( const promoTraffic = applyPromoDiscount(
option.price_kopeks, option.price_kopeks,
hasExistingDiscount, option.original_price_kopeks,
); );
return ( return (
@@ -3268,41 +3266,24 @@ export default function Subscription() {
: '' : ''
} ${!option.is_available ? 'cursor-not-allowed opacity-50' : ''}`} } ${!option.is_available ? 'cursor-not-allowed opacity-50' : ''}`}
> >
{(() => { {promoTraffic.percent && promoTraffic.percent > 0 && (
const trafficDisplayDiscount = hasExistingDiscount
? option.discount_percent
: promoTraffic.percent;
const trafficDisplayOriginal = hasExistingDiscount
? option.original_price_kopeks
: promoTraffic.original;
return (
<>
{trafficDisplayDiscount && trafficDisplayDiscount > 0 && (
<div <div
className={`absolute right-2 top-2 z-10 rounded-full px-2 py-0.5 text-xs font-medium text-white shadow-sm ${ className={`absolute right-2 top-2 z-10 rounded-full px-2 py-0.5 text-xs font-medium text-white shadow-sm ${
hasExistingDiscount ? 'bg-success-500' : 'bg-orange-500' promoTraffic.isPromoGroup ? 'bg-success-500' : 'bg-orange-500'
}`} }`}
> >
-{trafficDisplayDiscount}% -{promoTraffic.percent}%
</div> </div>
)} )}
<div className="text-lg font-semibold text-dark-100"> <div className="text-lg font-semibold text-dark-100">{option.label}</div>
{option.label}
</div>
<div className="mt-1 flex flex-wrap items-center justify-center gap-x-2 gap-y-1"> <div className="mt-1 flex flex-wrap items-center justify-center gap-x-2 gap-y-1">
<span className="text-accent-400"> <span className="text-accent-400">{formatPrice(promoTraffic.price)}</span>
{formatPrice(promoTraffic.price)} {promoTraffic.original && promoTraffic.original > promoTraffic.price && (
</span>
{trafficDisplayOriginal &&
trafficDisplayOriginal > promoTraffic.price && (
<span className="text-xs text-dark-500 line-through"> <span className="text-xs text-dark-500 line-through">
{formatPrice(trafficDisplayOriginal)} {formatPrice(promoTraffic.original)}
</span> </span>
)} )}
</div> </div>
</>
);
})()}
</button> </button>
); );
})} })}
@@ -3322,12 +3303,9 @@ export default function Subscription() {
return true; return true;
}) })
.map((server) => { .map((server) => {
const hasExistingDiscount = !!(
server.discount_percent && server.discount_percent > 0
);
const promoServer = applyPromoDiscount( const promoServer = applyPromoDiscount(
server.price_kopeks, server.price_kopeks,
hasExistingDiscount, server.original_price_kopeks,
); );
return ( return (
@@ -3343,20 +3321,15 @@ export default function Subscription() {
: 'cursor-not-allowed border-dark-800/30 bg-dark-900/30 opacity-50' : 'cursor-not-allowed border-dark-800/30 bg-dark-900/30 opacity-50'
}`} }`}
> >
{(() => { {promoServer.percent && promoServer.percent > 0 ? (
const serverDisplayDiscount = hasExistingDiscount
? server.discount_percent
: promoServer.percent;
return serverDisplayDiscount && serverDisplayDiscount > 0 ? (
<div <div
className={`absolute right-2 top-2 z-10 rounded-full px-2 py-0.5 text-xs font-medium text-white shadow-sm ${ className={`absolute right-2 top-2 z-10 rounded-full px-2 py-0.5 text-xs font-medium text-white shadow-sm ${
hasExistingDiscount ? 'bg-success-500' : 'bg-orange-500' promoServer.isPromoGroup ? 'bg-success-500' : 'bg-orange-500'
}`} }`}
> >
-{serverDisplayDiscount}% -{promoServer.percent}%
</div> </div>
) : null; ) : null}
})()}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div <div
className={`flex h-5 w-5 flex-shrink-0 items-center justify-center rounded border-2 ${ className={`flex h-5 w-5 flex-shrink-0 items-center justify-center rounded border-2 ${
@@ -3374,16 +3347,12 @@ export default function Subscription() {
{formatPrice(promoServer.price)} {formatPrice(promoServer.price)}
{t('subscription.perMonth')} {t('subscription.perMonth')}
</span> </span>
{(() => { {promoServer.original &&
const serverOriginal = hasExistingDiscount promoServer.original > promoServer.price ? (
? server.original_price_kopeks
: promoServer.original;
return serverOriginal && serverOriginal > promoServer.price ? (
<span className="text-xs text-dark-500 line-through"> <span className="text-xs text-dark-500 line-through">
{formatPrice(serverOriginal)} {formatPrice(promoServer.original)}
</span> </span>
) : null; ) : null}
})()}
</div> </div>
</div> </div>
</div> </div>
@@ -3448,9 +3417,7 @@ export default function Subscription() {
) : preview ? ( ) : preview ? (
<div className="space-y-4 rounded-xl bg-dark-800/50 p-5"> <div className="space-y-4 rounded-xl bg-dark-800/50 p-5">
{/* Active promo discount banner */} {/* Active promo discount banner */}
{activeDiscount?.is_active && {activeDiscount?.is_active && activeDiscount.discount_percent && (
activeDiscount.discount_percent &&
!preview.original_price_kopeks && (
<div className="flex items-center justify-center gap-2 rounded-lg border border-orange-500/30 bg-orange-500/10 p-3"> <div className="flex items-center justify-center gap-2 rounded-lg border border-orange-500/30 bg-orange-500/10 p-3">
<svg <svg
className="h-4 w-4 text-orange-400" className="h-4 w-4 text-orange-400"
@@ -3479,16 +3446,10 @@ export default function Subscription() {
))} ))}
{(() => { {(() => {
// Apply promo discount if not already applied by server
const hasServerDiscount = !!preview.original_price_kopeks;
const promoTotal = applyPromoDiscount( const promoTotal = applyPromoDiscount(
preview.total_price_kopeks, preview.total_price_kopeks,
hasServerDiscount, preview.original_price_kopeks,
); );
const displayTotal = promoTotal.price;
const displayOriginal = hasServerDiscount
? preview.original_price_kopeks
: promoTotal.original;
return ( return (
<div className="flex items-center justify-between border-t border-dark-700/50 pt-4"> <div className="flex items-center justify-between border-t border-dark-700/50 pt-4">
@@ -3497,11 +3458,11 @@ export default function Subscription() {
</span> </span>
<div className="text-right"> <div className="text-right">
<div className="text-2xl font-bold text-accent-400"> <div className="text-2xl font-bold text-accent-400">
{formatPrice(displayTotal)} {formatPrice(promoTotal.price)}
</div> </div>
{displayOriginal && ( {promoTotal.original && promoTotal.original > promoTotal.price && (
<div className="text-sm text-dark-500 line-through"> <div className="text-sm text-dark-500 line-through">
{formatPrice(displayOriginal)} {formatPrice(promoTotal.original)}
</div> </div>
)} )}
</div> </div>