From 321bedcb61a231d3dd8ecba8623d1ee9d632b9b7 Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 23 Feb 2026 15:44:23 +0300 Subject: [PATCH] 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. --- src/pages/Subscription.tsx | 401 +++++++++++++++++-------------------- 1 file changed, 181 insertions(+), 220 deletions(-) diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index eaf5a1b..3f5f5cd 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -89,24 +89,47 @@ export default function Subscription() { // Helper to format price from kopeks 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 = ( priceKopeks: number, - hasExistingDiscount: boolean = false, + existingOriginalPrice?: number | null, ): { price: number; original: 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 - if (!activeDiscount?.is_active || !activeDiscount.discount_percent || hasExistingDiscount) { - return { price: priceKopeks, original: null, percent: null }; + const hasExisting = (existingOriginalPrice ?? 0) > priceKopeks; + const hasPromo = !!activeDiscount?.is_active && !!activeDiscount.discount_percent; + + 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 { + price: finalPrice, + original: existingOriginalPrice!, + percent: combinedPercent, + isPromoGroup: true, + }; + } + + // Only promo offer discount (no promo group) return { - price: discountedPrice, + price: finalPrice, original: priceKopeks, - percent: activeDiscount.discount_percent, + percent: activeDiscount!.discount_percent!, + isPromoGroup: false, }; }; @@ -2416,41 +2439,34 @@ export default function Subscription() { const dailyPrice = tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0; const originalDailyPrice = tariff.original_daily_price_kopeks || 0; - const hasExistingDailyDiscount = originalDailyPrice > dailyPrice; if (dailyPrice > 0) { - // Apply promo discount if no existing discount const promoDaily = applyPromoDiscount( dailyPrice, - hasExistingDailyDiscount, + originalDailyPrice > dailyPrice ? originalDailyPrice : undefined, ); return ( {formatPrice(promoDaily.price)} - {/* Show original price from promo group or promo offer */} - {(hasExistingDailyDiscount || promoDaily.original) && ( - - {formatPrice( - hasExistingDailyDiscount - ? originalDailyPrice - : promoDaily.original!, - )} - - )} + {promoDaily.original && + promoDaily.original > promoDaily.price && ( + + {formatPrice(promoDaily.original)} + + )} {t('subscription.tariff.perDay')} {/* Show discount badge */} - {tariff.daily_discount_percent && - tariff.daily_discount_percent > 0 ? ( - - -{tariff.daily_discount_percent}% + {promoDaily.percent && promoDaily.percent > 0 && ( + + -{promoDaily.percent}% - ) : ( - promoDaily.percent && ( - - -{promoDaily.percent}% - - ) )} ); @@ -2458,14 +2474,9 @@ export default function Subscription() { // Period-based price if (tariff.periods.length > 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( firstPeriod?.price_kopeks || 0, - hasExistingDiscount, + firstPeriod?.original_price_kopeks, ); return ( @@ -2473,27 +2484,22 @@ export default function Subscription() { {formatPrice(promoPeriod.price)} - {/* Show original price from promo group or promo offer */} - {(hasExistingDiscount || promoPeriod.original) && ( - - {formatPrice( - hasExistingDiscount - ? firstPeriod.original_price_kopeks! - : promoPeriod.original!, - )} - - )} - {/* Show discount badge */} - {hasExistingDiscount && firstPeriod.discount_percent ? ( - - -{firstPeriod.discount_percent}% - - ) : ( - promoPeriod.percent && ( - - -{promoPeriod.percent}% + {promoPeriod.original && + promoPeriod.original > promoPeriod.price && ( + + {formatPrice(promoPeriod.original)} - ) + )} + {promoPeriod.percent && promoPeriod.percent > 0 && ( + + -{promoPeriod.percent}% + )} ); @@ -2702,24 +2708,17 @@ export default function Subscription() { {selectedTariff.periods.length > 0 && !useCustomDays && (
{selectedTariff.periods.map((period) => { - const hasExistingDiscount = !!( - period.original_price_kopeks && - period.original_price_kopeks > period.price_kopeks - ); const promoPeriod = applyPromoDiscount( 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; + const displayDiscount = promoPeriod.percent; + const displayOriginal = promoPeriod.original; const displayPrice = promoPeriod.price; - const displayPerMonth = hasExistingDiscount - ? period.price_per_month_kopeks - : Math.round(promoPeriod.price / (period.days / 30)); + const displayPerMonth = + displayPrice !== period.price_kopeks + ? Math.round(displayPrice / Math.max(1, period.days / 30)) + : period.price_per_month_kopeks; return (
) : ( @@ -3035,16 +3049,12 @@ export default function Subscription() {
{formatPrice(promoPeriod.price)} - {(hasExistingPeriodDiscount || - promoPeriod.original) && ( - - {formatPrice( - hasExistingPeriodDiscount - ? selectedTariffPeriod.original_price_kopeks! - : promoPeriod.original!, - )} - - )} + {promoPeriod.original && + promoPeriod.original > promoPeriod.price && ( + + {formatPrice(promoPeriod.original)} + + )}
)} @@ -3177,19 +3187,10 @@ export default function Subscription() { {currentStep === 'period' && classicOptions && (
{classicOptions.periods.map((period) => { - const hasExistingDiscount = !!( - period.discount_percent && period.discount_percent > 0 - ); const promoPeriod = applyPromoDiscount( 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 (
@@ -3249,12 +3250,9 @@ export default function Subscription() { {currentStep === 'traffic' && selectedPeriod?.traffic.options && (
{selectedPeriod.traffic.options.map((option) => { - const hasExistingDiscount = !!( - option.discount_percent && option.discount_percent > 0 - ); const promoTraffic = applyPromoDiscount( option.price_kopeks, - hasExistingDiscount, + option.original_price_kopeks, ); return ( @@ -3268,41 +3266,24 @@ export default function Subscription() { : '' } ${!option.is_available ? 'cursor-not-allowed opacity-50' : ''}`} > - {(() => { - const trafficDisplayDiscount = hasExistingDiscount - ? option.discount_percent - : promoTraffic.percent; - const trafficDisplayOriginal = hasExistingDiscount - ? option.original_price_kopeks - : promoTraffic.original; - return ( - <> - {trafficDisplayDiscount && trafficDisplayDiscount > 0 && ( -
- -{trafficDisplayDiscount}% -
- )} -
- {option.label} -
-
- - {formatPrice(promoTraffic.price)} - - {trafficDisplayOriginal && - trafficDisplayOriginal > promoTraffic.price && ( - - {formatPrice(trafficDisplayOriginal)} - - )} -
- - ); - })()} + {promoTraffic.percent && promoTraffic.percent > 0 && ( +
+ -{promoTraffic.percent}% +
+ )} +
{option.label}
+
+ {formatPrice(promoTraffic.price)} + {promoTraffic.original && promoTraffic.original > promoTraffic.price && ( + + {formatPrice(promoTraffic.original)} + + )} +
); })} @@ -3322,12 +3303,9 @@ export default function Subscription() { return true; }) .map((server) => { - const hasExistingDiscount = !!( - server.discount_percent && server.discount_percent > 0 - ); const promoServer = applyPromoDiscount( server.price_kopeks, - hasExistingDiscount, + server.original_price_kopeks, ); return ( @@ -3343,20 +3321,15 @@ export default function Subscription() { : 'cursor-not-allowed border-dark-800/30 bg-dark-900/30 opacity-50' }`} > - {(() => { - const serverDisplayDiscount = hasExistingDiscount - ? server.discount_percent - : promoServer.percent; - return serverDisplayDiscount && serverDisplayDiscount > 0 ? ( -
- -{serverDisplayDiscount}% -
- ) : null; - })()} + {promoServer.percent && promoServer.percent > 0 ? ( +
+ -{promoServer.percent}% +
+ ) : null}
- {(() => { - const serverOriginal = hasExistingDiscount - ? server.original_price_kopeks - : promoServer.original; - return serverOriginal && serverOriginal > promoServer.price ? ( - - {formatPrice(serverOriginal)} - - ) : null; - })()} + {promoServer.original && + promoServer.original > promoServer.price ? ( + + {formatPrice(promoServer.original)} + + ) : null}
@@ -3448,28 +3417,26 @@ export default function Subscription() { ) : preview ? (
{/* Active promo discount banner */} - {activeDiscount?.is_active && - activeDiscount.discount_percent && - !preview.original_price_kopeks && ( -
- - - - - {t('promo.discountApplied')} -{activeDiscount.discount_percent}% - -
- )} + {activeDiscount?.is_active && activeDiscount.discount_percent && ( +
+ + + + + {t('promo.discountApplied')} -{activeDiscount.discount_percent}% + +
+ )} {preview.breakdown.map((item, idx) => (
@@ -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( preview.total_price_kopeks, - hasServerDiscount, + preview.original_price_kopeks, ); - const displayTotal = promoTotal.price; - const displayOriginal = hasServerDiscount - ? preview.original_price_kopeks - : promoTotal.original; return (
@@ -3497,11 +3458,11 @@ export default function Subscription() {
- {formatPrice(displayTotal)} + {formatPrice(promoTotal.price)}
- {displayOriginal && ( + {promoTotal.original && promoTotal.original > promoTotal.price && (
- {formatPrice(displayOriginal)} + {formatPrice(promoTotal.original)}
)}