From 6053781e5703c4ea7ce2ebe57eb4f4e55911fd09 Mon Sep 17 00:00:00 2001 From: Olzhas Askar Date: Tue, 18 Aug 2026 15:40:50 +0200 Subject: [PATCH] Merge pull request #36496 from overleaf/oa-annual-events [web] Subscription event segmentation GitOrigin-RevId: fdc25ac7bc7320ed82617b7c27c12a17eb31d723 --- .../Subscription/CustomerIoPlanHelpers.mjs | 31 ++------ .../Features/Subscription/PlansLocator.mjs | 13 ++++ .../Subscription/RecurlyEventHandler.mjs | 16 +++- .../Subscription/SubscriptionController.mjs | 4 + .../components/dashboard/pause-modal.tsx | 10 +-- .../dashboard/states/active/active.tsx | 9 ++- .../active/cancel-subscription-button.tsx | 11 ++- .../util/subscription-event-segmentation.ts | 22 ++++++ .../components/dashboard/pause-modal.test.tsx | 27 +++++++ .../dashboard/states/active/active.test.tsx | 56 +++++++++++++- .../subscription-event-segmentation.test.ts | 51 +++++++++++++ .../Subscription/RecurlyEventHandler.test.mjs | 18 +++++ .../SubscriptionController.test.mjs | 75 +++++++++++++++++++ services/web/types/stripe/webhook-event.ts | 9 +-- 14 files changed, 305 insertions(+), 47 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/util/subscription-event-segmentation.ts create mode 100644 services/web/test/frontend/features/subscription/util/subscription-event-segmentation.test.ts diff --git a/services/web/app/src/Features/Subscription/CustomerIoPlanHelpers.mjs b/services/web/app/src/Features/Subscription/CustomerIoPlanHelpers.mjs index 2c6e65a5ed..e0be126584 100644 --- a/services/web/app/src/Features/Subscription/CustomerIoPlanHelpers.mjs +++ b/services/web/app/src/Features/Subscription/CustomerIoPlanHelpers.mjs @@ -2,6 +2,7 @@ import Settings from '@overleaf/settings' import { AI_ADD_ON_CODE, isStandaloneAiAddOnPlanCode } from './AiHelper.mjs' import FeaturesHelper from './FeaturesHelper.mjs' +import PlansLocator from './PlansLocator.mjs' /** * @typedef {InstanceType} MongoSubscription @@ -120,9 +121,7 @@ function normalizePlanTypeFromPlanCode(planCode) { if (!planCode) { return '' } - const plan = /** @type {Plan[]} */ (Settings.plans).find( - candidate => candidate.planCode === planCode - ) + const plan = PlansLocator.findLocalPlanInSettings(planCode) return normalizePlanType({ plan: { planCode, @@ -161,18 +160,6 @@ function getFriendlyPlanName(planType) { return planType } -/** - * @param {Nullable} [bestSubscription] - * @returns {'annual' | 'monthly' | null} - */ -function getPlanCadence(bestSubscription) { - if (!bestSubscription?.plan) { - return null - } - - return bestSubscription.plan.annual ? 'annual' : 'monthly' -} - /** * @param {Nullable} [planCode] * @returns {'annual' | 'monthly' | null} @@ -182,11 +169,9 @@ function getPlanCadenceFromPlanCode(planCode) { return null } - const plan = /** @type {Plan[]} */ (Settings.plans).find( - candidate => candidate.planCode === planCode - ) + const plan = PlansLocator.findLocalPlanInSettings(planCode) if (plan) { - return plan.annual ? 'annual' : 'monthly' + return PlansLocator.getPlanCadence({ plan }) } if (planCode.includes('annual')) { @@ -348,7 +333,7 @@ function getAiPlanCadence( if (aiPlan === 'ai-assist-add-on') { return ( - getPlanCadence(bestSubscription) || + PlansLocator.getPlanCadence(bestSubscription) || getPlanCadenceFromPlanCode(individualSubscription?.planCode) ) } @@ -443,9 +428,7 @@ function getGroupSize( } return allGroupSubscriptions.reduce((largestGroupSize, subscription) => { - const plan = /** @type {Plan[]} */ (Settings.plans).find( - candidate => candidate.planCode === subscription.planCode - ) + const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) const groupSize = subscription.membersLimit ?? plan?.membersLimit ?? 0 return Math.max(largestGroupSize, groupSize) @@ -595,7 +578,7 @@ function getPlanProperties({ }) { const planType = normalizePlanType(bestSubscription) const displayPlanType = getFriendlyPlanName(planType) - const planTermLabel = getPlanCadence(bestSubscription) + const planTermLabel = PlansLocator.getPlanCadence(bestSubscription) const aiPlan = getAiPlanType( bestSubscription, individualSubscription, diff --git a/services/web/app/src/Features/Subscription/PlansLocator.mjs b/services/web/app/src/Features/Subscription/PlansLocator.mjs index 401420ea5c..f0eec8c5a5 100644 --- a/services/web/app/src/Features/Subscription/PlansLocator.mjs +++ b/services/web/app/src/Features/Subscription/PlansLocator.mjs @@ -165,6 +165,18 @@ function getPlanTypeAndPeriodFromRecurlyPlanCode(recurlyPlanCode) { return recurlyPlanCodeToPlanTypeAndPeriod[recurlyPlanCode] } +/** + * Prefer this over `getPlanTypeAndPeriodFromRecurlyPlanCode` when you have the + * plan: that map misses legacy plan codes and license-count group plan codes. + * + * @param {{ plan?: { annual?: boolean } } | null} [subscription] anything carrying a local plan + * @returns {'annual' | 'monthly' | null} + */ +function getPlanCadence(subscription) { + if (!subscription?.plan) return null + return subscription.plan.annual ? 'annual' : 'monthly' +} + /** * @param {string|null} [planCode] * @returns {Plan|null} @@ -219,6 +231,7 @@ export default { findLocalPlanInSettings, buildStripeLookupKey, getPlanTypeAndPeriodFromRecurlyPlanCode, + getPlanCadence, isGroupPlanCode, convertLegacyGroupPlanCodeToConsolidatedGroupPlanCodeIfNeeded, getVersionFromStripeLookupKey, diff --git a/services/web/app/src/Features/Subscription/RecurlyEventHandler.mjs b/services/web/app/src/Features/Subscription/RecurlyEventHandler.mjs index c523785a36..e2799ac026 100644 --- a/services/web/app/src/Features/Subscription/RecurlyEventHandler.mjs +++ b/services/web/app/src/Features/Subscription/RecurlyEventHandler.mjs @@ -29,7 +29,11 @@ async function sendRecurlyAnalyticsEvent(event, eventData) { await _sendSubscriptionStartedEvent(userId, eventData) break case 'updated_subscription_notification': - await _sendSubscriptionUpdatedEvent(userId, eventData) + await _sendSubscriptionUpdatedEvent( + userId, + eventData, + subscription?.planCode + ) break case 'canceled_subscription_notification': await _sendSubscriptionCancelledEvent(userId, eventData) @@ -146,7 +150,11 @@ async function _sendSubscriptionStartedEvent(userId, eventData) { } } -async function _sendSubscriptionUpdatedEvent(userId, eventData) { +async function _sendSubscriptionUpdatedEvent( + userId, + eventData, + planCodeBeforeSync +) { const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } = _getSubscriptionData(eventData) AnalyticsManager.recordEventForUserInBackground( @@ -154,6 +162,10 @@ async function _sendSubscriptionUpdatedEvent(userId, eventData) { 'subscription-updated', { plan_code: planCode, + // Mongo sync might already have happened, + // we only want to record when it haven't yet. + previous_plan_code: + planCodeBeforeSync !== planCode ? planCodeBeforeSync : undefined, quantity, is_trial: isTrial, has_ai_add_on: hasAiAddOn, diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.mjs b/services/web/app/src/Features/Subscription/SubscriptionController.mjs index c873af9c25..e8d7f9cb72 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionController.mjs @@ -244,6 +244,10 @@ async function userSubscriptionPage(req, res) { req.session, 'subscription-page-view', { + plan_code: personalSubscription?.planCode, + billing_cycle: PlansLocator.getPlanCadence(personalSubscription), + is_trial: isInTrial, + currency: personalSubscription?.payment?.currency, domain, } ) diff --git a/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx index 53ca2113cb..3c3fd954bd 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx @@ -17,7 +17,7 @@ import * as eventTracking from '../../../../infrastructure/event-tracking' import PauseDuck from '../../images/pause-duck.svg' import GenericErrorAlert from './generic-error-alert' import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription' -import isInFreeTrial from '../../util/is-in-free-trial' +import getSubscriptionEventSegmentation from '../../util/subscription-event-segmentation' const pauseMonthDurationOptions = [1, 2, 3] @@ -38,10 +38,10 @@ export default function PauseSubscriptionModal() { function handleCancelSubscriptionClick() { const subscription = personalSubscription as PaidSubscription - eventTracking.sendMB('subscription-page-cancel-button-click', { - plan_code: subscription?.planCode, - is_trial: isInFreeTrial(subscription?.payment.trialEndsAt), - }) + eventTracking.sendMB( + 'subscription-page-cancel-button-click', + getSubscriptionEventSegmentation(subscription) + ) setShowCancellation(true) } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx index dd163e2ac1..6f302a0d58 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx @@ -12,6 +12,7 @@ import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal' import { CancelAiAddOnModal } from '@/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal' import OLButton from '@/shared/components/ol/ol-button' import isInFreeTrial from '../../../../util/is-in-free-trial' +import getSubscriptionEventSegmentation from '../../../../util/subscription-event-segmentation' import AddOns from '@/features/subscription/components/dashboard/states/active/add-ons' import { AI_ADD_ON_CODE, @@ -75,7 +76,13 @@ export function ActiveSubscription({ planName = subscription.plan.name } - const handlePlanChange = () => setModalIdShown('change-plan') + const handlePlanChange = () => { + sendMB( + 'subscription-page-upgrade-button-click', + getSubscriptionEventSegmentation(subscription) + ) + setModalIdShown('change-plan') + } const handleCancelClick = (addOnCode: string) => { if ( diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx index 09fc01053e..1054ca97fe 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx @@ -6,7 +6,7 @@ import { PaidSubscription } from '../../../../../../../../types/subscription/das import { useFeatureFlag } from '@/shared/context/split-test-context' import { useLocation } from '@/shared/hooks/use-location' import { stripHasSubscription } from '../../../../data/subscription-url' -import isInFreeTrial from '../../../../util/is-in-free-trial' +import getSubscriptionEventSegmentation from '../../../../util/subscription-event-segmentation' export function CancelSubscriptionButton() { const { t } = useTranslation() @@ -19,7 +19,6 @@ export function CancelSubscriptionButton() { } = useSubscriptionDashboardContext() const subscription = personalSubscription as PaidSubscription - const isInTrial = isInFreeTrial(subscription?.payment.trialEndsAt) const hasPendingOrActivePause = subscription.payment.state === 'paused' || (subscription.payment.state === 'active' && @@ -32,10 +31,10 @@ export function CancelSubscriptionButton() { planIsEligibleForPause function handleCancelSubscriptionClick() { - eventTracking.sendMB('subscription-page-cancel-button-click', { - plan_code: subscription?.planCode, - is_trial: isInTrial, - }) + eventTracking.sendMB( + 'subscription-page-cancel-button-click', + getSubscriptionEventSegmentation(subscription) + ) const url = location.toString() if (url) { window.history.replaceState(null, '', stripHasSubscription(url)) diff --git a/services/web/frontend/js/features/subscription/util/subscription-event-segmentation.ts b/services/web/frontend/js/features/subscription/util/subscription-event-segmentation.ts new file mode 100644 index 0000000000..11f78c2cb0 --- /dev/null +++ b/services/web/frontend/js/features/subscription/util/subscription-event-segmentation.ts @@ -0,0 +1,22 @@ +import { PaidSubscription } from '@ol-types/subscription/dashboard/subscription' +import isInFreeTrial from './is-in-free-trial' + +/** + * The shared segmentation for `subscription-page-*` analytics events, so that + * every event fired from the subscription dashboard can be compared against the + * `subscription-page-view` event recorded server side. + */ +export default function getSubscriptionEventSegmentation( + subscription?: PaidSubscription +) { + return { + plan_code: subscription?.planCode, + billing_cycle: subscription?.plan + ? subscription.plan.annual + ? 'annual' + : 'monthly' + : undefined, + is_trial: isInFreeTrial(subscription?.payment?.trialEndsAt), + currency: subscription?.payment?.currency, + } +} diff --git a/services/web/test/frontend/features/subscription/components/dashboard/pause-modal.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/pause-modal.test.tsx index 421f90275d..6862172063 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/pause-modal.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/pause-modal.test.tsx @@ -11,6 +11,7 @@ import { import { renderActiveSubscription } from '../../helpers/render-active-subscription' import { location } from '@/shared/components/location' import { MetaTag } from '@/utils/meta' +import * as eventTracking from '@/infrastructure/event-tracking' const pauseSubscriptionSplitTestMeta: MetaTag[] = [ { name: 'ol-splitTestVariants', value: { 'pause-subscription': 'enabled' } }, @@ -85,6 +86,32 @@ describe('', function () { await screen.findByText('Pause instead, to pick up where you left off') }) + it('sends the cancel event with the same segmentation as the dashboard', async function () { + const sendMBSpy = sinon.spy(eventTracking, 'sendMB') + try { + renderSubscriptionWithPauseSupport() + clickCancelButton() + await screen.findByText('Pause instead, to pick up where you left off') + sendMBSpy.resetHistory() + + fireEvent.click( + screen.getByRole('button', { name: 'Cancel subscription' }) + ) + + expect(sendMBSpy).to.be.calledOnceWith( + 'subscription-page-cancel-button-click', + sinon.match({ + plan_code: 'collaborator', + billing_cycle: 'monthly', + is_trial: false, + currency: 'USD', + }) + ) + } finally { + sendMBSpy.restore() + } + }) + it('renders options for pause duration', async function () { renderSubscriptionWithPauseSupport() clickCancelButton() diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx index 4ea9209ed7..27cdf3344c 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx @@ -135,6 +135,38 @@ describe('', function () { ) }) + it('sends an event when the change plan button is clicked', function () { + renderActiveSubscription(monthlyActiveCollaborator) + + fireEvent.click(screen.getByRole('button', { name: 'Change plan' })) + + expect(sendMBSpy).to.be.calledOnceWith( + 'subscription-page-upgrade-button-click', + sinon.match({ + plan_code: 'collaborator', + billing_cycle: 'monthly', + is_trial: false, + currency: 'USD', + }) + ) + }) + + it('sends the annual billing cycle when on an annual plan', function () { + renderActiveSubscription(annualActiveSubscriptionEuro) + + fireEvent.click(screen.getByRole('button', { name: 'Change plan' })) + + expect(sendMBSpy).to.be.calledOnceWith( + 'subscription-page-upgrade-button-click', + sinon.match({ + plan_code: annualActiveSubscriptionEuro.planCode, + billing_cycle: 'annual', + is_trial: false, + currency: 'EUR', + }) + ) + }) + it('does not show "Change plan" option when past due', function () { // account is likely in expired state, but be sure to not show option if state is still active const activePastDueSubscription = cloneDeep(annualActiveSubscription) @@ -329,13 +361,35 @@ describe('', function () { showConfirmCancelUI() expect(sendMBSpy).to.be.calledOnceWith( - 'subscription-page-cancel-button-click' + 'subscription-page-cancel-button-click', + sinon.match({ + plan_code: annualActiveSubscription.planCode, + billing_cycle: 'annual', + is_trial: false, + currency: 'USD', + }) ) screen.getByText('We’d love you to stay') screen.getByRole('button', { name: 'Cancel my subscription' }) }) + it('sends the cancel event with trial segmentation when in a trial', function () { + renderActiveSubscription(trialCollaboratorSubscription) + + showConfirmCancelUI() + + expect(sendMBSpy).to.be.calledOnceWith( + 'subscription-page-cancel-button-click', + sinon.match({ + plan_code: trialCollaboratorSubscription.planCode, + billing_cycle: 'monthly', + is_trial: true, + currency: trialCollaboratorSubscription.payment.currency, + }) + ) + }) + it('cancels subscription and redirects page', async function () { const endPointResponse = { status: 200, diff --git a/services/web/test/frontend/features/subscription/util/subscription-event-segmentation.test.ts b/services/web/test/frontend/features/subscription/util/subscription-event-segmentation.test.ts new file mode 100644 index 0000000000..de5ce22c27 --- /dev/null +++ b/services/web/test/frontend/features/subscription/util/subscription-event-segmentation.test.ts @@ -0,0 +1,51 @@ +import { expect } from 'chai' +import getSubscriptionEventSegmentation from '../../../../../frontend/js/features/subscription/util/subscription-event-segmentation' +import { + annualActiveSubscriptionEuro, + monthlyActiveCollaborator, + trialCollaboratorSubscription, +} from '../fixtures/subscriptions' + +describe('getSubscriptionEventSegmentation', function () { + it('segments a monthly subscription', function () { + expect( + getSubscriptionEventSegmentation(monthlyActiveCollaborator) + ).to.deep.equal({ + plan_code: 'collaborator', + billing_cycle: 'monthly', + is_trial: false, + currency: 'USD', + }) + }) + + it('segments an annual subscription', function () { + expect( + getSubscriptionEventSegmentation(annualActiveSubscriptionEuro) + ).to.deep.equal({ + plan_code: 'collaborator-annual', + billing_cycle: 'annual', + is_trial: false, + currency: 'EUR', + }) + }) + + it('segments a subscription in a free trial', function () { + expect( + getSubscriptionEventSegmentation(trialCollaboratorSubscription) + ).to.deep.equal({ + plan_code: 'collaborator_free_trial_7_days', + billing_cycle: 'monthly', + is_trial: true, + currency: 'USD', + }) + }) + + it('omits the plan segmentation without a subscription', function () { + expect(getSubscriptionEventSegmentation(undefined)).to.deep.equal({ + plan_code: undefined, + billing_cycle: undefined, + is_trial: false, + currency: undefined, + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/RecurlyEventHandler.test.mjs b/services/web/test/unit/src/Subscription/RecurlyEventHandler.test.mjs index 92737ae41c..615a6fb04e 100644 --- a/services/web/test/unit/src/Subscription/RecurlyEventHandler.test.mjs +++ b/services/web/test/unit/src/Subscription/RecurlyEventHandler.test.mjs @@ -209,6 +209,10 @@ describe('RecurlyEventHandler', function () { it('with updated_subscription_notification', async function (ctx) { ctx.planCode = 'new-plan-code' ctx.eventData.subscription.plan.plan_code = ctx.planCode + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ + _id: 'sub123', + planCode: 'collaborator', + }) await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'updated_subscription_notification', ctx.eventData @@ -219,6 +223,7 @@ describe('RecurlyEventHandler', function () { 'subscription-updated', { plan_code: ctx.planCode, + previous_plan_code: 'collaborator', quantity: 1, is_trial: true, has_ai_add_on: false, @@ -246,6 +251,19 @@ describe('RecurlyEventHandler', function () { ) }) + it('with updated_subscription_notification and no subscription in mongo', async function (ctx) { + await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + 'updated_subscription_notification', + ctx.eventData + ) + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForUserInBackground, + ctx.userId, + 'subscription-updated', + sinon.match({ previous_plan_code: undefined }) + ) + }) + it('with canceled_subscription_notification', async function (ctx) { ctx.eventData.subscription.state = 'cancelled' await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent( diff --git a/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs index 10beebe4d4..d5085a1396 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs @@ -7,6 +7,7 @@ import MockResponse from '../helpers/MockResponse.mjs' import SubscriptionErrors from '../../../../app/src/Features/Subscription/Errors.mjs' import { PaymentServiceResourceNotFoundError } from '../../../../modules/subscriptions/app/src/PaymentServiceErrors.mjs' import SubscriptionHelper from '../../../../app/src/Features/Subscription/SubscriptionHelper.mjs' +import PlansLocator from '../../../../app/src/Features/Subscription/PlansLocator.mjs' import { AI_ADD_ON_CODE } from '../../../../app/src/Features/Subscription/AiHelper.mjs' const modulePath = @@ -360,6 +361,7 @@ describe('SubscriptionController', function () { vi.doMock('../../../../app/src/Features/Subscription/PlansLocator', () => ({ default: (ctx.PlansLocator = { + getPlanCadence: PlansLocator.getPlanCadence, findLocalPlanInSettings: sinon.stub().returns({ annual: false, }), @@ -611,6 +613,79 @@ describe('SubscriptionController', function () { }) }) + describe('subscription-page-view event', function () { + const renderWithSubscription = async (ctx, personalSubscription) => { + ctx.AnalyticsManager.recordEventForSession.resetHistory() + ctx.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves( + { personalSubscription, memberGroupSubscriptions: [] } + ) + await new Promise((resolve, reject) => { + ctx.res.render = () => resolve() + ctx.SubscriptionController.userSubscriptionPage( + ctx.req, + ctx.res, + ctx.rejectOnError(reject) + ) + }) + return ctx.AnalyticsManager.recordEventForSession.lastCall.args[2] + } + + it('should segment a monthly subscription', async function (ctx) { + const segmentation = await renderWithSubscription(ctx, { + planCode: 'collaborator', + plan: { planCode: 'collaborator' }, + payment: { currency: 'USD', trialEndsAt: null }, + }) + expect(segmentation).to.deep.include({ + plan_code: 'collaborator', + billing_cycle: 'monthly', + is_trial: false, + currency: 'USD', + }) + }) + + it('should segment an annual subscription', async function (ctx) { + const segmentation = await renderWithSubscription(ctx, { + planCode: 'collaborator-annual', + plan: { planCode: 'collaborator-annual', annual: true }, + payment: { currency: 'EUR', trialEndsAt: null }, + }) + expect(segmentation).to.deep.include({ + plan_code: 'collaborator-annual', + billing_cycle: 'annual', + is_trial: false, + currency: 'EUR', + }) + }) + + it('should segment a subscription in trial', async function (ctx) { + const segmentation = await renderWithSubscription(ctx, { + planCode: 'collaborator_free_trial_7_days', + plan: { planCode: 'collaborator_free_trial_7_days' }, + payment: { + currency: 'USD', + trialEndsAt: new Date(Date.now() + 1000 * 60 * 60 * 24), + }, + }) + expect(segmentation).to.deep.include({ + plan_code: 'collaborator_free_trial_7_days', + billing_cycle: 'monthly', + is_trial: true, + currency: 'USD', + }) + }) + + it('should omit the plan segmentation without a personal subscription', async function (ctx) { + const segmentation = await renderWithSubscription(ctx, undefined) + expect(segmentation).to.deep.include({ + plan_code: undefined, + billing_cycle: null, + is_trial: false, + currency: undefined, + }) + }) + }) + describe('when an unrecognized query param is present', function () { beforeEach(async function (ctx) { setReqValidationModeForTests('log') diff --git a/services/web/types/stripe/webhook-event.ts b/services/web/types/stripe/webhook-event.ts index 62c617820e..90322f6d5d 100644 --- a/services/web/types/stripe/webhook-event.ts +++ b/services/web/types/stripe/webhook-event.ts @@ -22,14 +22,7 @@ export interface CustomerSubscriptionUpdatedWebhookEvent cancel_at?: number | null // will only be present if the subscription was cancelled or reactivated items?: { // will be present if the subscription was downgraded, upgraded, or renewed - data: [ - { - price: { - id: string - } - quantity: number - }, - ] + data: Stripe.SubscriptionItem[] } status?: Stripe.Subscription.Status metadata?: Record