mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
Merge pull request #36496 from overleaf/oa-annual-events
[web] Subscription event segmentation GitOrigin-RevId: fdc25ac7bc7320ed82617b7c27c12a17eb31d723
This commit is contained in:
@@ -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<typeof import('../../models/Subscription.mjs').Subscription>} 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>} [bestSubscription]
|
||||
* @returns {'annual' | 'monthly' | null}
|
||||
*/
|
||||
function getPlanCadence(bestSubscription) {
|
||||
if (!bestSubscription?.plan) {
|
||||
return null
|
||||
}
|
||||
|
||||
return bestSubscription.plan.annual ? 'annual' : 'monthly'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Nullable<string>} [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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -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 (
|
||||
|
||||
+5
-6
@@ -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))
|
||||
|
||||
+22
@@ -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,
|
||||
}
|
||||
}
|
||||
+27
@@ -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('<PauseSubscriptionModal />', 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()
|
||||
|
||||
+55
-1
@@ -135,6 +135,38 @@ describe('<ActiveSubscription />', 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('<ActiveSubscription />', 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,
|
||||
|
||||
+51
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<string, string>
|
||||
|
||||
Reference in New Issue
Block a user