From 131c5fa29c99ff4c157edf79e691f628b07df0a9 Mon Sep 17 00:00:00 2001 From: jhoffman-ms Date: Tue, 19 May 2026 10:45:52 -0300 Subject: [PATCH 1/8] feat(new-plan-selection): implement by-email plan scenarios --- .../NewPlanSelection/EmailsPlan/index.js | 412 ++++++++++++++++++ .../StickyPlanSummary/index.js | 21 +- .../BuyProcess/NewPlanSelection/index.js | 99 ++++- .../BuyProcess/NewPlanSelection/index.test.js | 177 ++++++++ .../EmailMarketingPlan/index.js | 4 +- src/i18n/en.js | 11 + src/i18n/es.js | 11 + 7 files changed, 714 insertions(+), 21 deletions(-) create mode 100644 src/components/BuyProcess/NewPlanSelection/EmailsPlan/index.js diff --git a/src/components/BuyProcess/NewPlanSelection/EmailsPlan/index.js b/src/components/BuyProcess/NewPlanSelection/EmailsPlan/index.js new file mode 100644 index 000000000..cb4a15cd2 --- /dev/null +++ b/src/components/BuyProcess/NewPlanSelection/EmailsPlan/index.js @@ -0,0 +1,412 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FormattedMessage, FormattedNumber, useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; +import { PLAN_TYPE } from '../../../../doppler-types'; +import { InjectAppServices } from '../../../../services/pure-di'; +import { amountByPlanType, thousandSeparatorNumber, unitPriceDecimals } from '../../../../utils'; +import { Promocode } from '../Promocode'; + +const numberFormatOptions = { + maximumFractionDigits: 2, + minimumFractionDigits: 0, +}; + +const getFormattedPriceOptions = (value) => ({ + ...numberFormatOptions, + minimumFractionDigits: Number.isInteger(value) ? 0 : 2, +}); + +const getSessionPromocodeApplied = (sessionPlan) => { + const promotion = sessionPlan?.plan?.promotion; + const sanitizeCode = (value) => + value === undefined || value === null ? '' : String(value).trim(); + const promocode = + sanitizeCode(promotion?.code) || + sanitizeCode(promotion?.promocode) || + sanitizeCode(promotion?.promoCode) || + sanitizeCode(sessionPlan?.plan?.promotionCode) || + sanitizeCode(sessionPlan?.plan?.promocode); + + if (!promocode) { + return null; + } + + return { + canApply: true, + promocode, + promotionApplied: { + discountPercentage: promotion?.discount ?? 0, + duration: promotion?.duration ?? 0, + }, + }; +}; + +const getCheckoutUrl = ({ search, selectedPlan, promocodeApplied }) => { + const params = new URLSearchParams(search); + params.set('selected-plan', selectedPlan.id); + params.delete('promo-code'); + params.delete('Promo-code'); + params.delete('PromoCode'); + params.delete('discountId'); + params.delete('monthPlan'); + + if (promocodeApplied?.canApply && promocodeApplied.promocode) { + params.set('PromoCode', promocodeApplied.promocode); + } + + return `/checkout/premium/${PLAN_TYPE.byEmail}?${params.toString()}&buyType=1`; +}; + +export const EmailsPlan = InjectAppServices( + ({ + plans, + selectedPlanIndex, + isLessThan100kSelected, + isMoreThan10mSelected, + onPlanChange, + onStickySummaryChange, + sessionPlan, + selectedPlan, + search, + dependencies: { dopplerAccountPlansApiClient }, + }) => { + const intl = useIntl(); + const _ = (id, values) => intl.formatMessage({ id }, values); + const isFreeAccount = sessionPlan?.plan?.isFreeAccount; + const isEqualPlan = sessionPlan?.plan?.idPlan === selectedPlan?.id; + const [amountDetailsData, setAmountDetailsData] = useState(null); + const [promocodeApplied, setPromocodeApplied] = useState(null); + const [defaultPromocodeDismissed, setDefaultPromocodeDismissed] = useState(false); + const clearPromocodeInputRef = useRef(null); + const sessionPromocodeApplied = useMemo( + () => getSessionPromocodeApplied(sessionPlan), + [sessionPlan], + ); + const effectivePromocodeApplied = + promocodeApplied ?? (!defaultPromocodeDismissed ? sessionPromocodeApplied : null); + + useEffect(() => { + const fetchAmountDetails = async () => { + try { + const amountDetails = await dopplerAccountPlansApiClient.getPlanBillingDetailsData( + selectedPlan.id, + 'Marketing', + 0, + effectivePromocodeApplied?.canApply ? effectivePromocodeApplied.promocode : '', + ); + setAmountDetailsData(amountDetails); + } catch (error) { + setAmountDetailsData(null); + } + }; + + if (selectedPlan?.id) { + fetchAmountDetails(); + } + }, [dopplerAccountPlansApiClient, effectivePromocodeApplied, selectedPlan]); + + const handlePromocodeApplied = useCallback((promotion) => { + setPromocodeApplied(promotion && typeof promotion === 'object' ? promotion : null); + }, []); + + const handleRemovePromocodeApplied = useCallback(() => { + clearPromocodeInputRef.current?.(); + setDefaultPromocodeDismissed(true); + setPromocodeApplied(null); + }, []); + + const handleManualPromocodeIntervention = useCallback(() => { + setDefaultPromocodeDismissed(true); + }, []); + + const registerClearPromocodeInput = useCallback((clearPromocodeInput) => { + clearPromocodeInputRef.current = clearPromocodeInput; + }, []); + + const promocodeDiscount = amountDetailsData?.value?.discountPromocode ?? null; + const promocodeDiscountPercentage = promocodeDiscount?.discountPercentage ?? 0; + const selectedPlanFee = selectedPlan?.fee ?? 0; + const hasPromocodeDiscount = Boolean( + effectivePromocodeApplied?.canApply && + promocodeDiscountPercentage > 0 && + !isLessThan100kSelected && + !isMoreThan10mSelected, + ); + const displayedMonthlyPrice = + hasPromocodeDiscount && typeof amountDetailsData?.value?.nextMonthTotal === 'number' + ? amountDetailsData.value.nextMonthTotal + : selectedPlanFee; + const canChoosePlan = selectedPlan && !isEqualPlan; + const checkoutUrl = selectedPlan + ? getCheckoutUrl({ + search, + selectedPlan, + promocodeApplied: effectivePromocodeApplied, + }) + : '#'; + const stickyEmailsLabel = selectedPlan + ? thousandSeparatorNumber(intl.defaultLocale, amountByPlanType(selectedPlan)) + : ''; + const currentSessionEmailPlan = plans.find((plan) => plan.id === sessionPlan?.plan?.idPlan) ?? null; + const currentSessionEmailCapacity = amountByPlanType(currentSessionEmailPlan ?? {}); + const selectedEmailCapacity = amountByPlanType(selectedPlan ?? {}); + const shouldShowDowngradeWarning = + !isMoreThan10mSelected && + (isLessThan100kSelected || + (!isFreeAccount && + sessionPlan?.plan?.planType === PLAN_TYPE.byEmail && + currentSessionEmailCapacity > 0 && + selectedEmailCapacity > 0 && + selectedEmailCapacity < currentSessionEmailCapacity)); + const shouldShowHighVolumeMessage = isMoreThan10mSelected; + const shouldUseAdvisorCta = shouldShowDowngradeWarning || shouldShowHighVolumeMessage; + const shouldShowPromocode = !shouldUseAdvisorCta; + const extraEmailPrice = selectedPlan?.extraEmailPrice ?? 0; + + const stickyDiscountSummary = useMemo(() => { + if (!shouldShowPromocode || !hasPromocodeDiscount) { + return null; + } + + return { + type: 'promocode', + percentage: promocodeDiscountPercentage, + months: 0, + }; + }, [hasPromocodeDiscount, promocodeDiscountPercentage, shouldShowPromocode]); + + const stickySummaryData = useMemo( + () => ({ + amountLabel: stickyEmailsLabel, + ctaHref: shouldUseAdvisorCta ? '/upgrade-suggestion-form' : checkoutUrl, + discountSummary: stickyDiscountSummary, + displayPrice: displayedMonthlyPrice, + isCustomPlan: false, + isDisabled: !shouldUseAdvisorCta && !canChoosePlan, + planType: PLAN_TYPE.byEmail, + useAdvisorCta: shouldUseAdvisorCta, + }), + [ + canChoosePlan, + checkoutUrl, + displayedMonthlyPrice, + shouldUseAdvisorCta, + stickyDiscountSummary, + stickyEmailsLabel, + ], + ); + + useEffect(() => { + onStickySummaryChange?.(stickySummaryData); + }, [onStickySummaryChange, stickySummaryData]); + + return ( +
+
+
+
+

+ +

+
+

+ }} + /> +

+
+
+ +
+
+
+
+

+ +

+ + +
+ + {shouldShowPromocode && ( +
+ +
+ )} + + {shouldShowHighVolumeMessage && ( +
+ +
+

+ , bold: (chunks) => {chunks} }} + /> +

+ + + +
+
+ )} + + {shouldShowDowngradeWarning && ( +
+ +
+

+ , bold: (chunks) => {chunks} }} + /> +

+ + + +
+
+ )} +
+
+ + +
+
+ ); + }, +); diff --git a/src/components/BuyProcess/NewPlanSelection/StickyPlanSummary/index.js b/src/components/BuyProcess/NewPlanSelection/StickyPlanSummary/index.js index b30650459..60aac0f0f 100644 --- a/src/components/BuyProcess/NewPlanSelection/StickyPlanSummary/index.js +++ b/src/components/BuyProcess/NewPlanSelection/StickyPlanSummary/index.js @@ -1,5 +1,6 @@ import { FormattedMessage, FormattedNumber, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; +import { PLAN_TYPE } from '../../../../doppler-types'; const numberFormatOptions = { maximumFractionDigits: 2, @@ -21,6 +22,17 @@ export const StickyPlanSummary = ({ summary }) => { return null; } + const planType = summary.planType || PLAN_TYPE.byContact; + const planTitleMessageId = + planType === PLAN_TYPE.byEmail + ? 'buy_process.new_plan_selection.emails_plan_title' + : 'buy_process.new_plan_selection.contacts_plan_title'; + const subtitleMessageId = + planType === PLAN_TYPE.byEmail + ? 'buy_process.new_plan_selection.sticky_emails_subtitle' + : 'buy_process.new_plan_selection.sticky_contacts_subtitle'; + const amountLabel = summary.amountLabel ?? summary.contactsLabel; + return (
@@ -30,7 +42,7 @@ export const StickyPlanSummary = ({ summary }) => { ) : ( <> - {' '} + {' '} US$ { ) : ( )}

diff --git a/src/components/BuyProcess/NewPlanSelection/index.js b/src/components/BuyProcess/NewPlanSelection/index.js index 5afbe6d70..49a8d047e 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.js +++ b/src/components/BuyProcess/NewPlanSelection/index.js @@ -8,6 +8,7 @@ import { GoBackButton } from './GoBackButton'; import { UnexpectedError } from '../UnexpectedError'; import { ContactsPlan } from './ContactsPlan'; import { CreditsPlan } from './CreditsPlan'; +import { EmailsPlan } from './EmailsPlan'; import { IncludedFeatures } from './IncludedFeatures'; import { StickyPlanSummary } from './StickyPlanSummary'; import { AddOnsSection } from './AddOnsSection'; @@ -15,6 +16,8 @@ import { FAQSection } from './FAQSection'; import { NewPlanSelectionStyled } from './index.styles'; const MORE_THAN_100K_OPTION_VALUE = 'more-than-100000'; +const LESS_THAN_100K_EMAILS_OPTION_VALUE = 'less-than-100000'; +const MORE_THAN_10M_EMAILS_OPTION_VALUE = 'more-than-10000000'; const getContactsPromocode = () => { const rawValue = process.env.REACT_APP_PROMOCODE_CONTACTS?.trim() || ''; if (!rawValue || rawValue === 'undefined' || rawValue === 'null') { @@ -54,11 +57,15 @@ export const NewPlanSelection = InjectAppServices( const { isFreeAccount } = sessionPlan.plan; const [plansByContact, setPlansByContact] = useState([]); const [plansByCredit, setPlansByCredit] = useState([]); + const [plansByEmail, setPlansByEmail] = useState([]); const [selectedContactPlanIndex, setSelectedContactPlanIndex] = useState(0); const [selectedCreditPlanIndex, setSelectedCreditPlanIndex] = useState(0); + const [selectedEmailPlanIndex, setSelectedEmailPlanIndex] = useState(0); const [loading, setLoading] = useState(true); const [hasError, setHasError] = useState(false); const [isMoreThan100kSelected, setIsMoreThan100kSelected] = useState(false); + const [isLessThan100kEmailsSelected, setIsLessThan100kEmailsSelected] = useState(false); + const [isMoreThan10mEmailsSelected, setIsMoreThan10mEmailsSelected] = useState(false); const [stickySummaryData, setStickySummaryData] = useState(null); useEffect(() => { @@ -87,12 +94,14 @@ export const NewPlanSelection = InjectAppServices( const fetchPlans = async () => { try { setLoading(true); - const [fetchedPlansByContact, fetchedPlansByCredit] = await Promise.all([ + const [fetchedPlansByContact, fetchedPlansByCredit, fetchedPlansByEmail] = await Promise.all([ planService.getPlansByType(PLAN_TYPE.byContact, { includeDowngrades: true }), planService.getPlansByType(PLAN_TYPE.byCredit), + planService.getPlansByType(PLAN_TYPE.byEmail, { includeDowngrades: true }), ]); setPlansByContact(fetchedPlansByContact); setPlansByCredit(fetchedPlansByCredit); + setPlansByEmail(fetchedPlansByEmail); setSelectedContactPlanIndex( fetchedPlansByContact.length ? getPlanIndexByQueryOrSession({ @@ -113,7 +122,19 @@ export const NewPlanSelection = InjectAppServices( }) : 0, ); + setSelectedEmailPlanIndex( + fetchedPlansByEmail.length + ? getPlanIndexByQueryOrSession({ + plans: fetchedPlansByEmail, + search, + sessionPlan, + planType: PLAN_TYPE.byEmail, + }) + : 0, + ); setIsMoreThan100kSelected(false); + setIsLessThan100kEmailsSelected(false); + setIsMoreThan10mEmailsSelected(false); setHasError(false); } catch (error) { setHasError(true); @@ -127,7 +148,10 @@ export const NewPlanSelection = InjectAppServices( const selectedContactPlan = plansByContact[selectedContactPlanIndex] ?? null; const selectedCreditPlan = plansByCredit[selectedCreditPlanIndex] ?? null; + const selectedEmailPlan = plansByEmail[selectedEmailPlanIndex] ?? null; const isCurrentPlanByCredit = sessionPlan?.plan?.planType === PLAN_TYPE.byCredit; + const isCurrentPlanByEmail = + sessionPlan?.plan?.planType === PLAN_TYPE.byEmail && !sessionPlan?.plan?.isFreeAccount; const handlePlanChange = (event) => { const { value } = event.target; @@ -148,6 +172,28 @@ export const NewPlanSelection = InjectAppServices( setSelectedCreditPlanIndex(parseInt(event.target.value, 10)); }; + const handleEmailsPlanChange = (event) => { + const { value } = event.target; + + if (value === LESS_THAN_100K_EMAILS_OPTION_VALUE) { + setIsLessThan100kEmailsSelected(true); + setIsMoreThan10mEmailsSelected(false); + setSelectedEmailPlanIndex(0); + return; + } + + if (value === MORE_THAN_10M_EMAILS_OPTION_VALUE) { + setIsLessThan100kEmailsSelected(false); + setIsMoreThan10mEmailsSelected(true); + setSelectedEmailPlanIndex(Math.max(0, plansByEmail.length - 1)); + return; + } + + setIsLessThan100kEmailsSelected(false); + setIsMoreThan10mEmailsSelected(false); + setSelectedEmailPlanIndex(parseInt(value, 10)); + }; + if (loading) { return ; } @@ -156,7 +202,10 @@ export const NewPlanSelection = InjectAppServices( return ; } - if (plansByContact.length === 0 && plansByCredit.length === 0) { + if ( + (!isCurrentPlanByEmail && plansByContact.length === 0 && plansByCredit.length === 0) || + (isCurrentPlanByEmail && plansByEmail.length === 0) + ) { return (
@@ -172,7 +221,13 @@ export const NewPlanSelection = InjectAppServices(
- +
@@ -212,22 +267,36 @@ export const NewPlanSelection = InjectAppServices(
- + {isCurrentPlanByEmail ? ( + + ) : ( + + )}
- {!isCurrentPlanByCredit && creditsPlanSection} + {!isCurrentPlanByCredit && !isCurrentPlanByEmail && creditsPlanSection}
diff --git a/src/components/BuyProcess/NewPlanSelection/index.test.js b/src/components/BuyProcess/NewPlanSelection/index.test.js index 77352827f..346dbeebb 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.test.js +++ b/src/components/BuyProcess/NewPlanSelection/index.test.js @@ -45,6 +45,25 @@ const contactPlans = [ }, ]; +const emailPlans = [ + { + type: PLAN_TYPE.byEmail, + id: 30222, + name: '300000-EMAILS', + emailsByMonth: 300000, + fee: 367, + extraEmailPrice: 0.0015, + }, + { + type: PLAN_TYPE.byEmail, + id: 30223, + name: '1500000-EMAILS', + emailsByMonth: 1500000, + fee: 920, + extraEmailPrice: 0.0012, + }, +]; + const creditPlans = [ { type: PLAN_TYPE.byCredit, @@ -179,10 +198,12 @@ let consoleErrorSpy; let previousCanBuyPushNotificationPlan; const getContactsPlanSection = () => screen.getByTestId('dp-contacts-plan'); +const getEmailsPlanSection = () => screen.getByTestId('dp-emails-plan'); const getCreditsPlanSection = () => screen.getByTestId('dp-credits-plan'); const getAddOnsSection = () => screen.getByTestId('dp-addons-section'); const getFaqSection = () => screen.getByTestId('dp-faq-section'); const getContactsSelect = () => within(getContactsPlanSection()).getByRole('combobox'); +const getEmailsSelect = () => within(getEmailsPlanSection()).getByRole('combobox'); const getCreditsSelect = () => within(getCreditsPlanSection()).getByRole('combobox'); const hasPriceInBold = (priceRegex) => (_content, node) => node?.tagName === 'B' && priceRegex.test(node?.textContent || ''); @@ -246,6 +267,10 @@ const createForcedServices = ({ return creditPlans; } + if (planType === PLAN_TYPE.byEmail) { + return emailPlans; + } + return []; }), }, @@ -594,6 +619,158 @@ describe('NewPlanSelection component', () => { expect(screen.getByText(/Hasta 1\.500 Contactos \+ Envios ilimitados/i)).toBeInTheDocument(); }); + it('should render emails plan variant for paid byEmail users', async () => { + await renderNewPlanSelection( + ['/new-plan-selection'], + { + appSessionUser: { + plan: { + idPlan: 30222, + planType: PLAN_TYPE.byEmail, + isFreeAccount: false, + planSubscription: 1, + }, + }, + }, + { useI18nKeysAsValues: true }, + ); + + expect(getEmailsPlanSection()).toBeInTheDocument(); + expect(getEmailsSelect()).toBeInTheDocument(); + expect(screen.queryByTestId('dp-contacts-plan')).not.toBeInTheDocument(); + expect(screen.queryByTestId('dp-credits-plan')).not.toBeInTheDocument(); + }); + + it('should render less-than-100k as the first option in emails dropdown', async () => { + await renderNewPlanSelection( + ['/new-plan-selection'], + { + appSessionUser: { + plan: { + idPlan: 30222, + planType: PLAN_TYPE.byEmail, + isFreeAccount: false, + planSubscription: 1, + }, + }, + }, + { useI18nKeysAsValues: true }, + ); + + const emailOptions = within(getEmailsSelect()).getAllByRole('option'); + expect(emailOptions[0]).toHaveTextContent( + 'buy_process.new_plan_selection.emails_option_less_than_100k', + ); + }); + + it('should show downgrade message and use advisor CTA when selecting less than 100k emails', async () => { + await renderNewPlanSelection( + ['/new-plan-selection'], + { + appSessionUser: { + plan: { + idPlan: 30223, + planType: PLAN_TYPE.byEmail, + isFreeAccount: false, + planSubscription: 1, + }, + }, + }, + { useI18nKeysAsValues: true }, + ); + + fireEvent.change(getEmailsSelect(), { + target: { value: 'less-than-100000' }, + }); + await settleAsyncState(); + + await waitFor(() => + expect(screen.getByTestId('dp-emails-downgrade-message')).toBeInTheDocument(), + ); + expect( + screen.getByText('buy_process.new_plan_selection.emails_downgrade_warning_message'), + ).toBeInTheDocument(); + expect( + screen.getByRole('link', { + name: 'buy_process.new_plan_selection.contact_advisor_cta', + }), + ).toHaveAttribute('href', '/upgrade-suggestion-form'); + expect( + screen.getByRole('link', { + name: 'buy_process.new_plan_selection.sticky_custom_cta', + }), + ).toHaveAttribute('href', '/upgrade-suggestion-form'); + }); + + it('should prioritize more than 10m message and hide downgrade warning in emails plan', async () => { + await renderNewPlanSelection( + ['/new-plan-selection'], + { + appSessionUser: { + plan: { + idPlan: 30223, + planType: PLAN_TYPE.byEmail, + isFreeAccount: false, + planSubscription: 1, + }, + }, + }, + { useI18nKeysAsValues: true }, + ); + + fireEvent.change(getEmailsSelect(), { + target: { value: 'less-than-100000' }, + }); + await settleAsyncState(); + await waitFor(() => + expect(screen.getByTestId('dp-emails-downgrade-message')).toBeInTheDocument(), + ); + + fireEvent.change(getEmailsSelect(), { + target: { value: 'more-than-10000000' }, + }); + await settleAsyncState(); + + await waitFor(() => + expect(screen.getByTestId('dp-emails-more-than-10m-message')).toBeInTheDocument(), + ); + expect(screen.queryByTestId('dp-emails-downgrade-message')).not.toBeInTheDocument(); + expect( + screen.getByRole('link', { + name: 'buy_process.new_plan_selection.contact_advisor_cta', + }), + ).toHaveAttribute('href', '/upgrade-suggestion-form'); + expect( + screen.getByRole('link', { + name: 'buy_process.new_plan_selection.sticky_custom_cta', + }), + ).toHaveAttribute('href', '/upgrade-suggestion-form'); + }); + + it('should build checkout URL for selected byEmail plan when no commercial scenario is active', async () => { + await renderNewPlanSelection(['/new-plan-selection'], { + appSessionUser: { + plan: { + idPlan: 30222, + planType: PLAN_TYPE.byEmail, + isFreeAccount: false, + planSubscription: 1, + }, + }, + }); + + fireEvent.change(getEmailsSelect(), { + target: { value: '1' }, + }); + await settleAsyncState(); + + await waitFor(() => + expect(screen.getByRole('link', { name: 'Elegir Plan' }).getAttribute('href')).toBe( + '/checkout/premium/monthly-deliveries?selected-plan=30223&buyType=1', + ), + ); + }); + it('should show downgrade warning when paid user selects a smaller contacts plan and hide it when returns to current size', async () => { await renderNewPlanSelection( ['/new-plan-selection'], diff --git a/src/components/MyPlan/SubscriptionDetails/EmailMarketingPlan/index.js b/src/components/MyPlan/SubscriptionDetails/EmailMarketingPlan/index.js index 08773f708..6082ea0c2 100644 --- a/src/components/MyPlan/SubscriptionDetails/EmailMarketingPlan/index.js +++ b/src/components/MyPlan/SubscriptionDetails/EmailMarketingPlan/index.js @@ -20,11 +20,9 @@ export const EmailMarketingPlan = ({ user, plan, features }) => { }; const cancelAccount = () => setStartCancellationFlow(false); - const isContactPlan = plan.planType === PLAN_TYPE.byContact; const newPlanSelectionFlag = features?.newPlanSelectionEnabled ?? user?.features?.newPlanSelectionEnabled; - const shouldGoToPlanSelection = !plan.isFreeAccount && (!isContactPlan || !newPlanSelectionFlag); - const changePlanUrl = shouldGoToPlanSelection ? plan.buttonUrl : '/new-plan-selection?buyType=1'; + const changePlanUrl = !newPlanSelectionFlag ? plan.buttonUrl : '/new-plan-selection?buyType=1'; return (
diff --git a/src/i18n/en.js b/src/i18n/en.js index ff7cc44a5..fab070486 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -187,7 +187,17 @@ other {}}}}}}}}}} credits_price_per_credit: `Price per Credit: US$${formattedNumberPlaceholder}`, credits_promocode_savings_text: 'You save {percentage}% on this purchase', custom_price_value: 'Tailored*', + emails_downgrade_warning_message: 'Need to reduce deliveries?{br}If you need to reduce your Plan deliveries, contact our team so we can help you with the change.', + emails_extra_email_price: 'Extra email cost US$ {price}', + emails_label: 'How many Deliveries do you need?', + emails_more_than_10m_info_message: 'Need more Deliveries?{br}Contact our team so we can offer you a custom Plan that fits your business needs.', + emails_option: '{emails}', + emails_option_less_than_100k: 'Less than 100,000', + emails_option_more_than_10m: 'More than 10,000,000', + emails_plan_description: 'Send more and pay less. Unlimited Contacts with a lower unit cost per delivery.{br}Ideal for companies with large databases.', + emails_plan_title: 'Email Plan', empty_message: 'There are no Contacts plans available at the moment.', + empty_emails_message: 'There are no Delivery plans available right now.', included_features: { item_1_description: 'Automate customer journeys, recover opportunities, and improve relationships with smart sendings.', item_1_title: 'Automate your marketing and connect more', @@ -223,6 +233,7 @@ other {}}}}}}}}}} sticky_custom_subtitle: 'Unlimited contacts + Emails with tailored pricing', sticky_custom_title: 'Customized Email Plan', sticky_default_cta: 'Buy Now', + sticky_emails_subtitle: 'Up to {emails} deliveries per month', sticky_frequency_discount_text: 'Billing {periodCapitalized} {percentage}%OFF | 1 {period} payment of {currency}{total}', sticky_promocode_discount_text: '{percentage}% OFF for {months, plural, one {# month} other {# months}}', sticky_promocode_discount_text_without_months: '{percentage}% OFF', diff --git a/src/i18n/es.js b/src/i18n/es.js index 268403948..9770b035a 100644 --- a/src/i18n/es.js +++ b/src/i18n/es.js @@ -188,7 +188,17 @@ other {}}}}}}}}}} credits_price_per_credit: `Precio por Credito: US$${formattedNumberPlaceholder}`, credits_promocode_savings_text: 'Ahorras {percentage}% en esta compra', custom_price_value: 'A medida*', + emails_downgrade_warning_message: 'Necesitas disminuir los Envios?{br}Si necesitas reducir la cantidad de Envios de tu Plan, contacta a nuestro equipo para poder realizar el cambio.', + emails_extra_email_price: 'Costo por email excedente US$ {price}', + emails_label: 'Cuantos Envios necesitas?', + emails_more_than_10m_info_message: 'Necesitas mas Envios?{br}Contacta a nuestro equipo para poder ofrecerte un Plan personalizado que se adecue con las necesidades de tu negocio.', + emails_option: '{emails}', + emails_option_less_than_100k: 'Menos de 100.000', + emails_option_more_than_10m: 'Mas de 10.000.000', + emails_plan_description: 'Envia mas y paga menos. Contactos ilimitados con un costo por envio unitario mas bajo.{br}Ideal para empresas con grandes bases de datos.', + emails_plan_title: 'Plan Envios', empty_message: 'No hay planes por Contactos disponibles en este momento.', + empty_emails_message: 'No hay planes por Envios disponibles en este momento.', included_features: { item_1_description: 'Automatiza recorridos, recupera oportunidades y mejora la relación con tus Contactos con envíos inteligentes.', item_1_title: 'Automatiza tu marketing y conecta más', @@ -224,6 +234,7 @@ other {}}}}}}}}}} sticky_custom_subtitle: 'Contactos ilimitados + Envios con precios a medida', sticky_custom_title: 'Plan Envios Personalizado', sticky_default_cta: 'Comprar Ahora', + sticky_emails_subtitle: 'Hasta {emails} Envios por mes', sticky_frequency_discount_text: 'Facturación {periodCapitalized} {percentage}%OFF | 1 Pago {period} de {currency}{total}', sticky_promocode_discount_text: 'Descuento {percentage}% OFF por {months, plural, one {# mes} other {# meses}}', sticky_promocode_discount_text_without_months: 'Descuento {percentage}% OFF', From d463798cfaa03bea118f883a056d51ac9707df69 Mon Sep 17 00:00:00 2001 From: jhoffman-ms Date: Wed, 20 May 2026 14:32:36 -0300 Subject: [PATCH 2/8] feat(new-plan-selection): preselect next byEmail plan on load --- .../BuyProcess/NewPlanSelection/index.js | 10 +++++++ .../BuyProcess/NewPlanSelection/index.test.js | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/components/BuyProcess/NewPlanSelection/index.js b/src/components/BuyProcess/NewPlanSelection/index.js index 49a8d047e..11b435215 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.js +++ b/src/components/BuyProcess/NewPlanSelection/index.js @@ -46,6 +46,16 @@ const getPlanIndexByQueryOrSession = ({ plans, search, sessionPlan, planType }) (plan) => sessionPlan?.plan?.planType === planType && plan.id === sessionPlan.plan.idPlan, ); + if (planType === PLAN_TYPE.byEmail && currentPlanIndex >= 0) { + const currentEmails = + plans[currentPlanIndex]?.emailsByMonth ?? plans[currentPlanIndex]?.emailQty ?? 0; + const nextPlanIndex = plans.findIndex( + (plan) => (plan.emailsByMonth ?? plan.emailQty ?? 0) > currentEmails, + ); + + return nextPlanIndex >= 0 ? nextPlanIndex : currentPlanIndex; + } + return currentPlanIndex >= 0 ? currentPlanIndex : 0; }; diff --git a/src/components/BuyProcess/NewPlanSelection/index.test.js b/src/components/BuyProcess/NewPlanSelection/index.test.js index 346dbeebb..d93c4d404 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.test.js +++ b/src/components/BuyProcess/NewPlanSelection/index.test.js @@ -641,6 +641,34 @@ describe('NewPlanSelection component', () => { expect(screen.queryByTestId('dp-credits-plan')).not.toBeInTheDocument(); }); + it('should preselect the next byEmail plan based on current emails quantity', async () => { + await renderNewPlanSelection( + ['/new-plan-selection'], + { + appSessionUser: { + plan: { + idPlan: 30222, + planType: PLAN_TYPE.byEmail, + isFreeAccount: false, + planSubscription: 1, + }, + }, + }, + { useI18nKeysAsValues: true }, + ); + + expect(getEmailsSelect()).toHaveValue('1'); + expect(screen.getByText('buy_process.new_plan_selection.sticky_emails_subtitle')).toBeInTheDocument(); + expect( + screen.getByRole('link', { + name: 'buy_process.new_plan_selection.choose_plan', + }), + ).toHaveAttribute( + 'href', + '/checkout/premium/monthly-deliveries?selected-plan=30223&buyType=1', + ); + }); + it('should render less-than-100k as the first option in emails dropdown', async () => { await renderNewPlanSelection( ['/new-plan-selection'], From 59bbb14fb337a296737f76791d53389a9f8a30ea Mon Sep 17 00:00:00 2001 From: jhoffman-ms Date: Wed, 20 May 2026 15:14:59 -0300 Subject: [PATCH 3/8] feat(new-plan-selection): improve by-email selection and messages --- .../BuyProcess/NewPlanSelection/EmailsPlan/index.js | 9 +++++---- src/components/BuyProcess/NewPlanSelection/index.js | 11 ++++++----- .../BuyProcess/NewPlanSelection/index.test.js | 9 ++++----- src/i18n/en.js | 2 +- src/i18n/es.js | 2 +- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/components/BuyProcess/NewPlanSelection/EmailsPlan/index.js b/src/components/BuyProcess/NewPlanSelection/EmailsPlan/index.js index cb4a15cd2..fe82a5736 100644 --- a/src/components/BuyProcess/NewPlanSelection/EmailsPlan/index.js +++ b/src/components/BuyProcess/NewPlanSelection/EmailsPlan/index.js @@ -128,9 +128,9 @@ export const EmailsPlan = InjectAppServices( const selectedPlanFee = selectedPlan?.fee ?? 0; const hasPromocodeDiscount = Boolean( effectivePromocodeApplied?.canApply && - promocodeDiscountPercentage > 0 && - !isLessThan100kSelected && - !isMoreThan10mSelected, + promocodeDiscountPercentage > 0 && + !isLessThan100kSelected && + !isMoreThan10mSelected, ); const displayedMonthlyPrice = hasPromocodeDiscount && typeof amountDetailsData?.value?.nextMonthTotal === 'number' @@ -147,7 +147,8 @@ export const EmailsPlan = InjectAppServices( const stickyEmailsLabel = selectedPlan ? thousandSeparatorNumber(intl.defaultLocale, amountByPlanType(selectedPlan)) : ''; - const currentSessionEmailPlan = plans.find((plan) => plan.id === sessionPlan?.plan?.idPlan) ?? null; + const currentSessionEmailPlan = + plans.find((plan) => plan.id === sessionPlan?.plan?.idPlan) ?? null; const currentSessionEmailCapacity = amountByPlanType(currentSessionEmailPlan ?? {}); const selectedEmailCapacity = amountByPlanType(selectedPlan ?? {}); const shouldShowDowngradeWarning = diff --git a/src/components/BuyProcess/NewPlanSelection/index.js b/src/components/BuyProcess/NewPlanSelection/index.js index 11b435215..958948a56 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.js +++ b/src/components/BuyProcess/NewPlanSelection/index.js @@ -104,11 +104,12 @@ export const NewPlanSelection = InjectAppServices( const fetchPlans = async () => { try { setLoading(true); - const [fetchedPlansByContact, fetchedPlansByCredit, fetchedPlansByEmail] = await Promise.all([ - planService.getPlansByType(PLAN_TYPE.byContact, { includeDowngrades: true }), - planService.getPlansByType(PLAN_TYPE.byCredit), - planService.getPlansByType(PLAN_TYPE.byEmail, { includeDowngrades: true }), - ]); + const [fetchedPlansByContact, fetchedPlansByCredit, fetchedPlansByEmail] = + await Promise.all([ + planService.getPlansByType(PLAN_TYPE.byContact, { includeDowngrades: true }), + planService.getPlansByType(PLAN_TYPE.byCredit), + planService.getPlansByType(PLAN_TYPE.byEmail, { includeDowngrades: true }), + ]); setPlansByContact(fetchedPlansByContact); setPlansByCredit(fetchedPlansByCredit); setPlansByEmail(fetchedPlansByEmail); diff --git a/src/components/BuyProcess/NewPlanSelection/index.test.js b/src/components/BuyProcess/NewPlanSelection/index.test.js index d93c4d404..08ecb343c 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.test.js +++ b/src/components/BuyProcess/NewPlanSelection/index.test.js @@ -658,15 +658,14 @@ describe('NewPlanSelection component', () => { ); expect(getEmailsSelect()).toHaveValue('1'); - expect(screen.getByText('buy_process.new_plan_selection.sticky_emails_subtitle')).toBeInTheDocument(); + expect( + screen.getByText('buy_process.new_plan_selection.sticky_emails_subtitle'), + ).toBeInTheDocument(); expect( screen.getByRole('link', { name: 'buy_process.new_plan_selection.choose_plan', }), - ).toHaveAttribute( - 'href', - '/checkout/premium/monthly-deliveries?selected-plan=30223&buyType=1', - ); + ).toHaveAttribute('href', '/checkout/premium/monthly-deliveries?selected-plan=30223&buyType=1'); }); it('should render less-than-100k as the first option in emails dropdown', async () => { diff --git a/src/i18n/en.js b/src/i18n/en.js index fab070486..45a691bfa 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -196,8 +196,8 @@ other {}}}}}}}}}} emails_option_more_than_10m: 'More than 10,000,000', emails_plan_description: 'Send more and pay less. Unlimited Contacts with a lower unit cost per delivery.{br}Ideal for companies with large databases.', emails_plan_title: 'Email Plan', - empty_message: 'There are no Contacts plans available at the moment.', empty_emails_message: 'There are no Delivery plans available right now.', + empty_message: 'There are no Contacts plans available at the moment.', included_features: { item_1_description: 'Automate customer journeys, recover opportunities, and improve relationships with smart sendings.', item_1_title: 'Automate your marketing and connect more', diff --git a/src/i18n/es.js b/src/i18n/es.js index 9770b035a..83d8d8411 100644 --- a/src/i18n/es.js +++ b/src/i18n/es.js @@ -197,8 +197,8 @@ other {}}}}}}}}}} emails_option_more_than_10m: 'Mas de 10.000.000', emails_plan_description: 'Envia mas y paga menos. Contactos ilimitados con un costo por envio unitario mas bajo.{br}Ideal para empresas con grandes bases de datos.', emails_plan_title: 'Plan Envios', - empty_message: 'No hay planes por Contactos disponibles en este momento.', empty_emails_message: 'No hay planes por Envios disponibles en este momento.', + empty_message: 'No hay planes por Contactos disponibles en este momento.', included_features: { item_1_description: 'Automatiza recorridos, recupera oportunidades y mejora la relación con tus Contactos con envíos inteligentes.', item_1_title: 'Automatiza tu marketing y conecta más', From 2c3815316dc6efe53bb32b75cdbb3973300b7b55 Mon Sep 17 00:00:00 2001 From: jhoffman-ms Date: Wed, 20 May 2026 15:39:22 -0300 Subject: [PATCH 4/8] test(new-plan-selection): wait for sticky summary render --- src/components/BuyProcess/NewPlanSelection/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BuyProcess/NewPlanSelection/index.test.js b/src/components/BuyProcess/NewPlanSelection/index.test.js index 08ecb343c..89a670b7a 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.test.js +++ b/src/components/BuyProcess/NewPlanSelection/index.test.js @@ -340,7 +340,7 @@ describe('NewPlanSelection component', () => { expect(screen.getAllByText(/C[oó]digo de descuento/i).length).toBeGreaterThan(0); expect(screen.getByRole('link', { name: /Elegir Plan/i })).toBeInTheDocument(); expect(screen.queryByText(/tipo de plan/i)).not.toBeInTheDocument(); - expect(screen.getByTestId('dp-sticky-plan-summary')).toBeInTheDocument(); + await waitFor(() => expect(screen.getByTestId('dp-sticky-plan-summary')).toBeInTheDocument()); expect(screen.getByText(/Comprar Ahora/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Ver m[aá]s funcionalidades/i })).toBeInTheDocument(); From 70dd95db2eb7b826fa9a67c41916920b8d917061 Mon Sep 17 00:00:00 2001 From: jhoffman-ms Date: Thu, 21 May 2026 07:58:06 -0300 Subject: [PATCH 5/8] feat(new-plan-selection): preselect next contact plan by subscribers count --- .../BuyProcess/NewPlanSelection/index.js | 23 ++++++- .../BuyProcess/NewPlanSelection/index.test.js | 65 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/components/BuyProcess/NewPlanSelection/index.js b/src/components/BuyProcess/NewPlanSelection/index.js index 958948a56..074decd08 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.js +++ b/src/components/BuyProcess/NewPlanSelection/index.js @@ -37,15 +37,36 @@ const getPlanIndexByQueryOrSession = ({ plans, search, sessionPlan, planType }) const query = new URLSearchParams(search); const selectedPlanId = parseInt(query.get('selected-plan'), 10); const selectedPlanIndex = plans.findIndex((plan) => plan.id === selectedPlanId); + const isCurrentPlanType = sessionPlan?.plan?.planType === planType; if (selectedPlanIndex >= 0) { return selectedPlanIndex; } const currentPlanIndex = plans.findIndex( - (plan) => sessionPlan?.plan?.planType === planType && plan.id === sessionPlan.plan.idPlan, + (plan) => isCurrentPlanType && plan.id === sessionPlan.plan.idPlan, ); + if (planType === PLAN_TYPE.byContact && isCurrentPlanType) { + const subscribersCount = Number(sessionPlan?.plan?.subscribersCount); + + if (!Number.isNaN(subscribersCount)) { + const firstPlanWithMoreCapacityIndex = plans.findIndex( + (plan) => (plan.subscriberLimit ?? 0) > subscribersCount, + ); + + if (firstPlanWithMoreCapacityIndex >= 0) { + if (firstPlanWithMoreCapacityIndex === currentPlanIndex) { + return firstPlanWithMoreCapacityIndex + 1 < plans.length + ? firstPlanWithMoreCapacityIndex + 1 + : firstPlanWithMoreCapacityIndex; + } + + return firstPlanWithMoreCapacityIndex; + } + } + } + if (planType === PLAN_TYPE.byEmail && currentPlanIndex >= 0) { const currentEmails = plans[currentPlanIndex]?.emailsByMonth ?? plans[currentPlanIndex]?.emailQty ?? 0; diff --git a/src/components/BuyProcess/NewPlanSelection/index.test.js b/src/components/BuyProcess/NewPlanSelection/index.test.js index 89a670b7a..c2e80ed2c 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.test.js +++ b/src/components/BuyProcess/NewPlanSelection/index.test.js @@ -212,6 +212,7 @@ const createForcedServices = ({ features = {}, dopplerAccountPlansApiClient = {}, appSessionUser = {}, + planService = {}, } = {}) => ({ appSessionRef: { current: { @@ -273,6 +274,7 @@ const createForcedServices = ({ return []; }), + ...planService, }, }); @@ -668,6 +670,69 @@ describe('NewPlanSelection component', () => { ).toHaveAttribute('href', '/checkout/premium/monthly-deliveries?selected-plan=30223&buyType=1'); }); + it('should preselect the next contact plan when first eligible plan matches current plan by subscribers count', async () => { + const contactPlansWithExtraLevel = [ + { + type: PLAN_TYPE.byContact, + id: 10222, + name: '500-SUBSCRIBERS', + subscriberLimit: 500, + fee: 10, + billingCycleDetails: contactPlans[0].billingCycleDetails, + }, + { + type: PLAN_TYPE.byContact, + id: 10223, + name: '1500-SUBSCRIBERS', + subscriberLimit: 1500, + fee: 20, + billingCycleDetails: contactPlans[1].billingCycleDetails, + }, + { + type: PLAN_TYPE.byContact, + id: 10224, + name: '3000-SUBSCRIBERS', + subscriberLimit: 3000, + fee: 30, + billingCycleDetails: contactPlans[1].billingCycleDetails, + }, + ]; + + await renderNewPlanSelection(['/new-plan-selection'], { + appSessionUser: { + plan: { + idPlan: 10223, + planType: PLAN_TYPE.byContact, + isFreeAccount: false, + planSubscription: 1, + subscribersCount: 1300, + }, + }, + planService: { + getPlansByType: jest.fn(async (planType) => { + if (planType === PLAN_TYPE.byContact) { + return contactPlansWithExtraLevel; + } + + if (planType === PLAN_TYPE.byCredit) { + return creditPlans; + } + + if (planType === PLAN_TYPE.byEmail) { + return emailPlans; + } + + return []; + }), + }, + }); + + expect(getContactsSelect()).toHaveValue('2'); + expect(screen.getByRole('link', { name: 'Elegir Plan' }).getAttribute('href')).toContain( + 'selected-plan=10224', + ); + }); + it('should render less-than-100k as the first option in emails dropdown', async () => { await renderNewPlanSelection( ['/new-plan-selection'], From 80ff359b218fdd973b862a1602adcbbf9d12d45a Mon Sep 17 00:00:00 2001 From: jhoffman-ms Date: Thu, 21 May 2026 08:33:17 -0300 Subject: [PATCH 6/8] feat(new-plan-selection): avoid initial downgrade for byContact users --- .../BuyProcess/NewPlanSelection/index.js | 12 ++-- .../BuyProcess/NewPlanSelection/index.test.js | 57 ++++++++++++++++++- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/components/BuyProcess/NewPlanSelection/index.js b/src/components/BuyProcess/NewPlanSelection/index.js index 074decd08..4c81a3c4f 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.js +++ b/src/components/BuyProcess/NewPlanSelection/index.js @@ -49,17 +49,21 @@ const getPlanIndexByQueryOrSession = ({ plans, search, sessionPlan, planType }) if (planType === PLAN_TYPE.byContact && isCurrentPlanType) { const subscribersCount = Number(sessionPlan?.plan?.subscribersCount); + const currentPlanCapacity = + currentPlanIndex >= 0 ? (plans[currentPlanIndex]?.subscriberLimit ?? 0) : 0; if (!Number.isNaN(subscribersCount)) { + if (currentPlanIndex >= 0 && subscribersCount <= currentPlanCapacity) { + return currentPlanIndex + 1 < plans.length ? currentPlanIndex + 1 : currentPlanIndex; + } + const firstPlanWithMoreCapacityIndex = plans.findIndex( (plan) => (plan.subscriberLimit ?? 0) > subscribersCount, ); if (firstPlanWithMoreCapacityIndex >= 0) { - if (firstPlanWithMoreCapacityIndex === currentPlanIndex) { - return firstPlanWithMoreCapacityIndex + 1 < plans.length - ? firstPlanWithMoreCapacityIndex + 1 - : firstPlanWithMoreCapacityIndex; + if (currentPlanIndex >= 0 && firstPlanWithMoreCapacityIndex <= currentPlanIndex) { + return currentPlanIndex + 1 < plans.length ? currentPlanIndex + 1 : currentPlanIndex; } return firstPlanWithMoreCapacityIndex; diff --git a/src/components/BuyProcess/NewPlanSelection/index.test.js b/src/components/BuyProcess/NewPlanSelection/index.test.js index c2e80ed2c..9d85277e1 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.test.js +++ b/src/components/BuyProcess/NewPlanSelection/index.test.js @@ -670,7 +670,7 @@ describe('NewPlanSelection component', () => { ).toHaveAttribute('href', '/checkout/premium/monthly-deliveries?selected-plan=30223&buyType=1'); }); - it('should preselect the next contact plan when first eligible plan matches current plan by subscribers count', async () => { + it('should preselect the next contact plan when subscribers count is lower than current plan capacity and a higher plan exists', async () => { const contactPlansWithExtraLevel = [ { type: PLAN_TYPE.byContact, @@ -733,6 +733,61 @@ describe('NewPlanSelection component', () => { ); }); + it('should not preselect a smaller contact plan when subscribers count is lower than current plan capacity and there is no higher plan', async () => { + await renderNewPlanSelection(['/new-plan-selection'], { + appSessionUser: { + plan: { + idPlan: 10223, + planType: PLAN_TYPE.byContact, + isFreeAccount: false, + planSubscription: 1, + subscribersCount: 1300, + }, + }, + }); + + expect(getContactsSelect()).toHaveValue('1'); + expect( + within(screen.getByTestId('dp-sticky-plan-summary')).queryByRole('link', { + name: 'Comprar Ahora', + }), + ).not.toBeInTheDocument(); + expect( + within(screen.getByTestId('dp-sticky-plan-summary')).getByRole('button', { + name: 'Comprar Ahora', + }), + ).toBeDisabled(); + expect( + within(getContactsPlanSection()).queryByRole('link', { + name: 'Elegir Plan', + }), + ).not.toBeInTheDocument(); + expect( + within(getContactsPlanSection()).getByRole('button', { + name: 'Elegir Plan', + }), + ).toBeDisabled(); + }); + + it('should preselect a higher contact plan when subscribers count is greater than current plan capacity', async () => { + await renderNewPlanSelection(['/new-plan-selection'], { + appSessionUser: { + plan: { + idPlan: 10222, + planType: PLAN_TYPE.byContact, + isFreeAccount: false, + planSubscription: 1, + subscribersCount: 700, + }, + }, + }); + + expect(getContactsSelect()).toHaveValue('1'); + expect(screen.getByRole('link', { name: 'Elegir Plan' }).getAttribute('href')).toContain( + 'selected-plan=10223', + ); + }); + it('should render less-than-100k as the first option in emails dropdown', async () => { await renderNewPlanSelection( ['/new-plan-selection'], From 8b3ec8f74138eb8edb1a5c8bf8276c8759cd612e Mon Sep 17 00:00:00 2001 From: jhoffman-ms Date: Thu, 21 May 2026 09:08:27 -0300 Subject: [PATCH 7/8] fix(new-plan-selection): enable contacts subscription for free users --- .../NewPlanSelection/ContactsPlan/index.js | 2 +- .../BuyProcess/NewPlanSelection/index.test.js | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/BuyProcess/NewPlanSelection/ContactsPlan/index.js b/src/components/BuyProcess/NewPlanSelection/ContactsPlan/index.js index 381c7d811..89d9cff9f 100644 --- a/src/components/BuyProcess/NewPlanSelection/ContactsPlan/index.js +++ b/src/components/BuyProcess/NewPlanSelection/ContactsPlan/index.js @@ -227,7 +227,7 @@ export const ContactsPlan = InjectAppServices( const shouldShowLosePromotionWarning = !isTailoredPlan && isUpgradePlan && isAppliedPromocodeSameAsSaved; const shouldUseAdvisorCta = isTailoredPlan || shouldShowDowngradeWarning; - const shouldDisablePaymentFrequency = !keepControlsEnabled; + const shouldDisablePaymentFrequency = !isFreeAccount && !keepControlsEnabled; const stickyDiscountSummary = useMemo(() => { if (isTailoredPlan) { diff --git a/src/components/BuyProcess/NewPlanSelection/index.test.js b/src/components/BuyProcess/NewPlanSelection/index.test.js index 9d85277e1..09da0e43d 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.test.js +++ b/src/components/BuyProcess/NewPlanSelection/index.test.js @@ -1083,13 +1083,13 @@ describe('NewPlanSelection component', () => { ); }); - it('should keep user subscription frequency selected and disable payment frequency controls', async () => { + it('should keep user subscription frequency selected and keep payment frequency enabled for free users', async () => { await renderNewPlanSelection(); const annualFrequencyButton = within(getContactsPlanSection()).getByRole('button', { name: /Anual/i, }); - expect(annualFrequencyButton).toBeDisabled(); + expect(annualFrequencyButton).not.toBeDisabled(); await waitFor(() => expect(screen.getByRole('link', { name: 'Elegir Plan' }).getAttribute('href')).toBe( @@ -1100,6 +1100,24 @@ describe('NewPlanSelection component', () => { expect(screen.queryByText(/Facturación Anual/i)).not.toBeInTheDocument(); }); + it('should keep contacts payment frequency disabled for users with current contact plan', async () => { + await renderNewPlanSelection(['/new-plan-selection'], { + appSessionUser: { + plan: { + idPlan: 10222, + planType: PLAN_TYPE.byContact, + isFreeAccount: false, + planSubscription: 1, + }, + }, + }); + + const annualFrequencyButton = within(getContactsPlanSection()).getByRole('button', { + name: /Anual/i, + }); + expect(annualFrequencyButton).toBeDisabled(); + }); + it('should render credits plan before contacts plan for users with current credit plan', async () => { await renderNewPlanSelection( ['/new-plan-selection'], From d5285265d17d77938b60b886d3eeee84a318b597 Mon Sep 17 00:00:00 2001 From: jhoffman-ms Date: Thu, 21 May 2026 09:36:49 -0300 Subject: [PATCH 8/8] fix(new-plan-selection): avoid promocode prepopulation for non-monthly paid users --- .../NewPlanSelection/ContactsPlan/index.js | 9 ++++++++ .../NewPlanSelection/index.styles.js | 2 +- .../BuyProcess/NewPlanSelection/index.test.js | 23 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/components/BuyProcess/NewPlanSelection/ContactsPlan/index.js b/src/components/BuyProcess/NewPlanSelection/ContactsPlan/index.js index 89d9cff9f..8af358889 100644 --- a/src/components/BuyProcess/NewPlanSelection/ContactsPlan/index.js +++ b/src/components/BuyProcess/NewPlanSelection/ContactsPlan/index.js @@ -24,6 +24,15 @@ const getFormattedPriceOptions = (value) => ({ const MORE_THAN_100K_OPTION_VALUE = 'more-than-100000'; const getSessionPromocodeApplied = (sessionPlan) => { + const isFreeAccount = Boolean(sessionPlan?.plan?.isFreeAccount); + const planSubscription = Number(sessionPlan?.plan?.planSubscription); + const hasNonMonthlySubscription = !Number.isNaN(planSubscription) && planSubscription !== 1; + + // For paid users with non-monthly subscription we should not prepopulate promocode. + if (!isFreeAccount && hasNonMonthlySubscription) { + return null; + } + const promotion = sessionPlan?.plan?.promotion; const sanitizeCode = (value) => value === undefined || value === null ? '' : String(value).trim(); diff --git a/src/components/BuyProcess/NewPlanSelection/index.styles.js b/src/components/BuyProcess/NewPlanSelection/index.styles.js index b22fa5875..04b0098f6 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.styles.js +++ b/src/components/BuyProcess/NewPlanSelection/index.styles.js @@ -118,7 +118,7 @@ export const NewPlanSelectionStyled = styled.div` border: 1px solid #eaeaea; border-radius: 3px; margin-bottom: 24px; - margin-top: 6px; + margin-top: 24px; padding: 30px 114px; } diff --git a/src/components/BuyProcess/NewPlanSelection/index.test.js b/src/components/BuyProcess/NewPlanSelection/index.test.js index 09da0e43d..21ac9561d 100644 --- a/src/components/BuyProcess/NewPlanSelection/index.test.js +++ b/src/components/BuyProcess/NewPlanSelection/index.test.js @@ -605,6 +605,29 @@ describe('NewPlanSelection component', () => { }); }); + it('should not prepopulate contacts promocode for paid users with non-monthly subscription', async () => { + await renderNewPlanSelection(['/new-plan-selection'], { + appSessionUser: { + plan: { + idPlan: 10222, + planType: PLAN_TYPE.byContact, + isFreeAccount: false, + planSubscription: 12, + promotion: { + idUserTypePlan: 10222, + code: 'PROMOCODE_ANNUAL', + discount: 20, + duration: 12, + }, + }, + }, + }); + + const promocodeInput = within(getContactsPlanSection()).getByRole('textbox'); + expect(promocodeInput).toHaveValue(''); + expect(screen.queryByRole('link', { name: /Elegir Plan/i })).not.toBeInTheDocument(); + }); + it('should update selected contacts plan in checkout URL when contacts dropdown changes', async () => { await renderNewPlanSelection();