From 599fe0a17717c3ba0a1cf3c28871d66bcca10b73 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Wed, 22 May 2024 10:49:46 -0400 Subject: [PATCH] Checkout: Change plan upsell calculation to take volume and discounts into account (#90893) * Change savings calculation to take volume discounts into account * Add percentage and first_unit_only to cart item cost_overrides Also make it clear that `cost_overrides` is always set. * Only use undiscounted domain price in calc for first year discounts * Re-apply coupon code discount to price difference * Add cost_overrides to empty response cart --- .../cart/cart-free-user-plan-upsell.tsx | 50 +++++++++++++++++-- packages/shopping-cart/src/empty-carts.ts | 1 + packages/shopping-cart/src/types.ts | 4 +- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/client/my-sites/checkout/cart/cart-free-user-plan-upsell.tsx b/client/my-sites/checkout/cart/cart-free-user-plan-upsell.tsx index 0f867bc92f7ca..b34c15d369d0e 100644 --- a/client/my-sites/checkout/cart/cart-free-user-plan-upsell.tsx +++ b/client/my-sites/checkout/cart/cart-free-user-plan-upsell.tsx @@ -36,6 +36,10 @@ const isRegistrationOrTransfer = ( item: ResponseCartProduct ) => { return isDomainRegistration( item ) || isDomainTransfer( item ); }; +function applyPercentageDiscount( percentageDiscount: number, basePrice: number ): number { + return basePrice - basePrice * ( percentageDiscount / 100 ); +} + function UpgradeText( { planPrice, planName, @@ -46,9 +50,43 @@ function UpgradeText( { firstDomain: ResponseCartProduct; } ) { const translate = useTranslate(); + // Use the subtotal with discounts for the current cart item price if the + // item's discounts are only for the first year, but use the subtotal + // without discounts to calculate the price after the upsell because the + // upsell will remove other first year discounts. + // + // For example, if the domain has a sale coupon then it will be removed by + // the "first year free" bundle discount when a plan is added to the cart + // because the bundle discount of 100% is higher than the sale coupon + // discount. Since we are trying to compare the current cart price with the + // predicted cart price with the plan, we must consider the current cart + // with the sale and the predicted cart without the sale. + // + // This will not be accurate for all types of discounts and predicting what + // discounts will be applied is difficult, but this makes an attempt based + // on what discounts are currently applied. + const isDomainDiscountFromFirstYearOverride = firstDomain.cost_overrides.every( + ( override ) => override.first_unit_only + ); + // Coupon code discounts will apply before and after the upsell, so we must + // re-apply them to the price difference also. + const domainCouponCodeDiscount = firstDomain.cost_overrides.find( + ( override ) => override.override_code === 'coupon-discount' + ); + const domainCouponPercentageDiscount = domainCouponCodeDiscount?.percentage ?? 0; + const domainInCartPrice = isDomainDiscountFromFirstYearOverride + ? firstDomain.item_subtotal_integer + : firstDomain.item_original_subtotal_integer; + const domainInCartPricePerYear = firstDomain.item_original_subtotal_integer / firstDomain.volume; + const cartPriceWithDomainAndPlan = + domainInCartPricePerYear * ( firstDomain.volume - 1 ) + planPrice; - if ( planPrice > firstDomain.item_subtotal_integer ) { - const extraToPay = planPrice - firstDomain.item_subtotal_integer; + if ( cartPriceWithDomainAndPlan > domainInCartPrice ) { + // Re-apply any coupon code discount. + const extraToPay = applyPercentageDiscount( + domainCouponPercentageDiscount, + cartPriceWithDomainAndPlan - domainInCartPrice + ); return translate( 'Pay an {{strong}}extra %(extraToPay)s{{/strong}} for our %(planName)s plan, and get access to all its features, plus the first year of your domain for free.', { @@ -66,8 +104,12 @@ function UpgradeText( { ); } - if ( planPrice < firstDomain.item_subtotal_integer ) { - const savings = firstDomain.item_subtotal_integer - planPrice; + if ( cartPriceWithDomainAndPlan < domainInCartPrice ) { + // Re-apply any coupon code discount. + const savings = applyPercentageDiscount( + domainCouponPercentageDiscount, + domainInCartPrice - cartPriceWithDomainAndPlan + ); return translate( '{{strong}}Save %(savings)s{{/strong}} when you purchase a WordPress.com %(planName)s plan instead — your domain comes free for a year.', { diff --git a/packages/shopping-cart/src/empty-carts.ts b/packages/shopping-cart/src/empty-carts.ts index cfba68d247a28..08858ba885de1 100644 --- a/packages/shopping-cart/src/empty-carts.ts +++ b/packages/shopping-cart/src/empty-carts.ts @@ -58,5 +58,6 @@ export function getEmptyResponseCartProduct(): ResponseCartProduct { product_type: 'test', included_domain_purchase_amount: 0, product_variants: [], + cost_overrides: [], }; } diff --git a/packages/shopping-cart/src/types.ts b/packages/shopping-cart/src/types.ts index 91f1d7c1d05f3..598312a35361d 100644 --- a/packages/shopping-cart/src/types.ts +++ b/packages/shopping-cart/src/types.ts @@ -418,7 +418,7 @@ export interface ResponseCartProduct { * The override_code is a string that identifies the reason for the override. * When displaying the reason to the customer, use the human_readable_reason. */ - cost_overrides?: ResponseCartCostOverride[]; + cost_overrides: ResponseCartCostOverride[]; /** * If set, is used to transform the usage/quantity of units used to derive the number of units @@ -525,6 +525,8 @@ export interface ResponseCartCostOverride { old_subtotal_integer: number; override_code: string; does_override_original_cost: boolean; + percentage: number; + first_unit_only: boolean; } export type IntroductoryOfferUnit = 'day' | 'week' | 'month' | 'year' | 'indefinite';