Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / helpers / subscription.ts
blobe9a748d45f9f049390de67d93699f651dede3ceb
1 import { addWeeks, fromUnixTime, isBefore } from 'date-fns';
3 import { isSplittedUser, onSessionMigrationChargebeeStatus } from '@proton/components/payments/core';
4 import type { ProductParam } from '@proton/shared/lib/apps/product';
5 import { getSupportedAddons, isIpAddon, isMemberAddon } from '@proton/shared/lib/helpers/addons';
7 import type {
8     FreeSubscription} from '../constants';
9 import {
10     ADDON_NAMES,
11     APPS,
12     COUPON_CODES,
13     CYCLE,
14     IPS_INCLUDED_IN_PLAN,
15     PLANS,
16     PLAN_SERVICES,
17     PLAN_TYPES,
18     isFreeSubscription,
19 } from '../constants';
20 import type {
21     Organization,
22     Plan,
23     PlanIDs,
24     PlansMap,
25     Pricing,
26     Subscription,
27     SubscriptionModel,
28     SubscriptionPlan,
29     UserModel,
30 } from '../interfaces';
31 import { Audience, ChargebeeEnabled, External } from '../interfaces';
32 import { hasBit } from './bitset';
34 const { PLAN, ADDON } = PLAN_TYPES;
36 const {
37     VISIONARY,
38     MAIL,
39     MAIL_PRO,
40     MAIL_BUSINESS,
41     DRIVE,
42     DRIVE_PRO,
43     PASS,
44     WALLET,
45     VPN,
46     VPN2024,
47     VPN_PASS_BUNDLE,
48     ENTERPRISE,
49     BUNDLE,
50     BUNDLE_PRO,
51     BUNDLE_PRO_2024,
52     FAMILY,
53     DUO,
54     VPN_PRO,
55     VPN_BUSINESS,
56     PASS_PRO,
57     PASS_BUSINESS,
58 } = PLANS;
60 const {
61     MEMBER_SCRIBE_MAILPLUS,
62     MEMBER_SCRIBE_MAIL_BUSINESS,
63     MEMBER_SCRIBE_DRIVEPLUS,
64     MEMBER_SCRIBE_BUNDLE,
65     MEMBER_SCRIBE_PASS,
66     MEMBER_SCRIBE_VPN,
67     MEMBER_SCRIBE_VPN2024,
68     MEMBER_SCRIBE_VPN_PASS_BUNDLE,
69     MEMBER_SCRIBE_MAIL_PRO,
70     MEMBER_SCRIBE_BUNDLE_PRO,
71     MEMBER_SCRIBE_BUNDLE_PRO_2024,
72     MEMBER_SCRIBE_PASS_PRO,
73     MEMBER_SCRIBE_VPN_BIZ,
74     MEMBER_SCRIBE_PASS_BIZ,
75     MEMBER_SCRIBE_VPN_PRO,
76     MEMBER_SCRIBE_FAMILY,
77     MEMBER_SCRIBE_DUO,
78 } = ADDON_NAMES;
80 type MaybeFreeSubscription = Subscription | FreeSubscription | undefined;
82 export const getPlan = (subscription: Subscription | FreeSubscription | undefined, service?: PLAN_SERVICES) => {
83     const result = (subscription?.Plans || []).find(
84         ({ Services, Type }) => Type === PLAN && (service === undefined ? true : hasBit(Services, service))
85     );
86     if (result) {
87         return result as SubscriptionPlan & { Name: PLANS };
88     }
89     return result;
92 export const getAddons = (subscription: Subscription | undefined) =>
93     (subscription?.Plans || []).filter(({ Type }) => Type === ADDON);
94 export const hasAddons = (subscription: Subscription | undefined) =>
95     (subscription?.Plans || []).some(({ Type }) => Type === ADDON);
97 export const getPlanName = (subscription: Subscription | undefined, service?: PLAN_SERVICES) => {
98     const plan = getPlan(subscription, service);
99     return plan?.Name;
102 export const getPlanTitle = (subscription: Subscription | undefined) => {
103     const plan = getPlan(subscription);
104     return plan?.Title;
107 export const hasSomePlan = (subscription: MaybeFreeSubscription, planName: PLANS) => {
108     if (isFreeSubscription(subscription)) {
109         return false;
110     }
112     return (subscription?.Plans || []).some(({ Name }) => Name === planName);
115 export const hasSomeAddonOrPlan = (
116     subscription: MaybeFreeSubscription,
117     addonName: ADDON_NAMES | PLANS | (ADDON_NAMES | PLANS)[]
118 ) => {
119     if (isFreeSubscription(subscription)) {
120         return false;
121     }
123     if (Array.isArray(addonName)) {
124         return (subscription?.Plans || []).some(({ Name }) => addonName.includes(Name as ADDON_NAMES));
125     }
127     return (subscription?.Plans || []).some(({ Name }) => Name === addonName);
130 export const hasLifetime = (subscription: Subscription | undefined) => {
131     return subscription?.CouponCode === COUPON_CODES.LIFETIME;
134 export const hasMigrationDiscount = (subscription?: Subscription) => {
135     return subscription?.CouponCode?.startsWith('MIGRATION');
138 export const isManagedExternally = (
139     subscription: Subscription | FreeSubscription | Pick<Subscription, 'External'> | undefined | null
140 ): boolean => {
141     if (!subscription || isFreeSubscription(subscription)) {
142         return false;
143     }
145     return subscription.External === External.Android || subscription.External === External.iOS;
148 export const hasVisionary = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, VISIONARY);
149 export const hasVPN = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, VPN);
150 export const hasVPN2024 = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, VPN2024);
151 export const hasVPNPassBundle = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, VPN_PASS_BUNDLE);
152 export const hasMail = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, MAIL);
153 export const hasMailPro = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, MAIL_PRO);
154 export const hasMailBusiness = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, MAIL_BUSINESS);
155 export const hasDrive = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, DRIVE);
156 export const hasDrivePro = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, DRIVE_PRO);
157 export const hasPass = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, PASS);
158 export const hasWallet = (subscription: MaybeFreeSubscription) => hasSomeAddonOrPlan(subscription, WALLET);
159 export const hasEnterprise = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, ENTERPRISE);
160 export const hasBundle = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, BUNDLE);
161 export const hasBundlePro = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, BUNDLE_PRO);
162 export const hasBundlePro2024 = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, BUNDLE_PRO_2024);
163 export const hasFamily = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, FAMILY);
164 export const hasDuo = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, DUO);
165 export const hasVpnPro = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, VPN_PRO);
166 export const hasVpnBusiness = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, VPN_BUSINESS);
167 export const hasPassPro = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, PASS_PRO);
168 export const hasPassBusiness = (subscription: MaybeFreeSubscription) => hasSomePlan(subscription, PASS_BUSINESS);
169 export const hasFree = (subscription: MaybeFreeSubscription) => (subscription?.Plans || []).length === 0;
171 export const hasAnyBundlePro = (subscription: MaybeFreeSubscription) =>
172     hasBundlePro(subscription) || hasBundlePro2024(subscription);
174 export const hasAIAssistant = (subscription: MaybeFreeSubscription) =>
175     hasSomeAddonOrPlan(subscription, [
176         MEMBER_SCRIBE_MAILPLUS,
177         MEMBER_SCRIBE_MAIL_BUSINESS,
178         MEMBER_SCRIBE_DRIVEPLUS,
179         MEMBER_SCRIBE_BUNDLE,
180         MEMBER_SCRIBE_PASS,
181         MEMBER_SCRIBE_VPN,
182         MEMBER_SCRIBE_VPN2024,
183         MEMBER_SCRIBE_VPN_PASS_BUNDLE,
184         MEMBER_SCRIBE_MAIL_PRO,
185         MEMBER_SCRIBE_BUNDLE_PRO,
186         MEMBER_SCRIBE_BUNDLE_PRO_2024,
187         MEMBER_SCRIBE_PASS_PRO,
188         MEMBER_SCRIBE_VPN_BIZ,
189         MEMBER_SCRIBE_PASS_BIZ,
190         MEMBER_SCRIBE_VPN_PRO,
191         MEMBER_SCRIBE_FAMILY,
192         MEMBER_SCRIBE_DUO,
193     ]);
195 export const PLANS_WITH_AI_INCLUDED = [VISIONARY];
197 export const hasPlanWithAIAssistantIncluded = (subscription: MaybeFreeSubscription) =>
198     hasSomeAddonOrPlan(subscription, PLANS_WITH_AI_INCLUDED);
200 export const hasAllProductsB2CPlan = (subscription: MaybeFreeSubscription) =>
201     hasDuo(subscription) || hasFamily(subscription) || hasBundle(subscription) || hasVisionary(subscription);
203 export const getUpgradedPlan = (subscription: Subscription | undefined, app: ProductParam) => {
204     if (hasFree(subscription)) {
205         switch (app) {
206             case APPS.PROTONPASS:
207                 return PLANS.PASS;
208             case APPS.PROTONDRIVE:
209                 return PLANS.DRIVE;
210             case APPS.PROTONVPN_SETTINGS:
211                 return PLANS.VPN;
212             case APPS.PROTONWALLET:
213                 return PLANS.WALLET;
214             default:
215             case APPS.PROTONMAIL:
216                 return PLANS.MAIL;
217         }
218     }
219     if (hasBundle(subscription) || hasBundlePro(subscription) || hasBundlePro2024(subscription)) {
220         return PLANS.BUNDLE_PRO_2024;
221     }
222     return PLANS.BUNDLE;
225 export const getIsB2BAudienceFromPlan = (planName: PLANS | ADDON_NAMES | undefined) => {
226     if (!planName) {
227         return false;
228     }
230     const b2bPlans: (PLANS | ADDON_NAMES)[] = [
231         MAIL_PRO,
232         MAIL_BUSINESS,
233         DRIVE_PRO,
234         BUNDLE_PRO,
235         BUNDLE_PRO_2024,
236         ENTERPRISE,
237         VPN_PRO,
238         VPN_BUSINESS,
239         PASS_PRO,
240         PASS_BUSINESS,
241     ];
243     return b2bPlans.includes(planName);
246 export const canCheckItemPaidChecklist = (subscription: Subscription | undefined) => {
247     return subscription?.Plans?.some(({ Name }) => [MAIL, DRIVE, FAMILY, DUO, BUNDLE].includes(Name as any));
250 export const canCheckItemGetStarted = (subscription: Subscription | undefined) => {
251     return subscription?.Plans?.some(({ Name }) => [VPN, VPN2024, WALLET, PASS, VPN_PASS_BUNDLE].includes(Name as any));
254 export const getIsVpnB2BPlan = (planName: PLANS | ADDON_NAMES) => {
255     return [VPN_PRO, VPN_BUSINESS].includes(planName as any);
258 export const getIsVpnPlan = (planName: PLANS | ADDON_NAMES | undefined) => {
259     return [VPN, VPN2024, VPN_PASS_BUNDLE, VPN_PRO, VPN_BUSINESS].includes(planName as any);
262 export const getIsConsumerVpnPlan = (planName: PLANS | ADDON_NAMES | undefined) => {
263     return [VPN, VPN2024, VPN_PASS_BUNDLE].includes(planName as any);
266 export const getIsPassB2BPlan = (planName?: PLANS | ADDON_NAMES) => {
267     return [PASS_PRO, PASS_BUSINESS].includes(planName as any);
270 export const getIsPassPlan = (planName: PLANS | ADDON_NAMES | undefined) => {
271     return [PASS, VPN_PASS_BUNDLE, PASS_PRO, PASS_BUSINESS].includes(planName as any);
274 export const getIsConsumerPassPlan = (planName: PLANS | ADDON_NAMES | undefined) => {
275     return [PASS, VPN_PASS_BUNDLE].includes(planName as any);
278 export const getIsSentinelPlan = (planName: PLANS | ADDON_NAMES | undefined) => {
279     return [
280         VISIONARY,
281         BUNDLE,
282         FAMILY,
283         DUO,
284         BUNDLE_PRO,
285         BUNDLE_PRO_2024,
286         PASS,
287         VPN_PASS_BUNDLE,
288         PASS_PRO,
289         PASS_BUSINESS,
290         MAIL_BUSINESS,
291     ].includes(planName as any);
294 export const getIsB2BAudienceFromSubscription = (subscription: Subscription | undefined) => {
295     return !!subscription?.Plans?.some(({ Name }) => getIsB2BAudienceFromPlan(Name));
298 export const getHasVpnB2BPlan = (subscription: MaybeFreeSubscription) => {
299     return hasVpnPro(subscription) || hasVpnBusiness(subscription);
302 export const getHasSomeVpnPlan = (subscription: MaybeFreeSubscription) => {
303     return (
304         hasVPN(subscription) ||
305         hasVPN2024(subscription) ||
306         hasVPNPassBundle(subscription) ||
307         hasVpnPro(subscription) ||
308         hasVpnBusiness(subscription)
309     );
312 export const getHasConsumerVpnPlan = (subscription: MaybeFreeSubscription) => {
313     return hasVPN(subscription) || hasVPN2024(subscription) || hasVPNPassBundle(subscription);
316 export const getHasPassB2BPlan = (subscription: MaybeFreeSubscription) => {
317     return hasPassPro(subscription) || hasPassBusiness(subscription);
320 export const getHasVpnOrPassB2BPlan = (subscription: MaybeFreeSubscription) => {
321     return getHasVpnB2BPlan(subscription) || getHasPassB2BPlan(subscription);
324 export const getHasMailB2BPlan = (subscription: MaybeFreeSubscription) => {
325     return hasMailPro(subscription) || hasMailBusiness(subscription);
328 export const getPrimaryPlan = (subscription: Subscription | undefined) => {
329     if (!subscription) {
330         return;
331     }
333     return getPlan(subscription);
336 export const getBaseAmount = (
337     name: PLANS | ADDON_NAMES,
338     plansMap: PlansMap,
339     subscription: Subscription | undefined,
340     cycle = CYCLE.MONTHLY
341 ) => {
342     const base = plansMap[name];
343     if (!base) {
344         return 0;
345     }
346     return (subscription?.Plans || [])
347         .filter(({ Name }) => Name === name)
348         .reduce((acc) => {
349             const pricePerCycle = base.Pricing[cycle] || 0;
350             return acc + pricePerCycle;
351         }, 0);
354 export const getPlanIDs = (subscription: MaybeFreeSubscription | null): PlanIDs => {
355     return (subscription?.Plans || []).reduce<PlanIDs>((acc, { Name, Quantity }) => {
356         acc[Name] = (acc[Name] || 0) + Quantity;
357         return acc;
358     }, {});
361 export const isTrial = (subscription: Subscription | FreeSubscription | undefined, plan?: PLANS): boolean => {
362     if (isFreeSubscription(subscription)) {
363         return false;
364     }
366     const isTrialV4 =
367         subscription?.CouponCode === COUPON_CODES.REFERRAL ||
368         subscription?.CouponCode === COUPON_CODES.MEMBER_DOWNGRADE_TRIAL;
369     const isTrialV5 = !!subscription?.IsTrial;
370     const trial = isTrialV4 || isTrialV5;
372     if (!plan) {
373         return trial;
374     }
376     return trial && getPlanName(subscription) === plan;
379 export const isTrialExpired = (subscription: Subscription | undefined) => {
380     const now = new Date();
381     return now > fromUnixTime(subscription?.PeriodEnd || 0);
384 export const willTrialExpire = (subscription: Subscription | undefined) => {
385     const now = new Date();
386     return isBefore(fromUnixTime(subscription?.PeriodEnd || 0), addWeeks(now, 1));
389 export const getHasMemberCapablePlan = (
390     organization: Organization | undefined,
391     subscription: Subscription | undefined
392 ) => {
393     const supportedAddons = getSupportedAddons(getPlanIDs(subscription));
394     return (organization?.MaxMembers || 0) > 1 || (Object.keys(supportedAddons) as ADDON_NAMES[]).some(isMemberAddon);
397 export const hasBlackFridayDiscount = (subscription: Subscription | undefined) => {
398     return [
399         COUPON_CODES.BLACK_FRIDAY_2022,
400         COUPON_CODES.MAIL_BLACK_FRIDAY_2022,
401         COUPON_CODES.VPN_BLACK_FRIDAY_2022,
402     ].includes(subscription?.CouponCode as COUPON_CODES);
405 export const getHas2023OfferCoupon = (coupon: string | undefined | null): boolean => {
406     return [COUPON_CODES.END_OF_YEAR_2023, COUPON_CODES.BLACK_FRIDAY_2023, COUPON_CODES.EOY_2023_1M_INTRO].includes(
407         coupon as any
408     );
411 export const hasVPNBlackFridayDiscount = (subscription: Subscription | undefined) => {
412     return subscription?.CouponCode === COUPON_CODES.VPN_BLACK_FRIDAY_2022;
415 export const hasMailBlackFridayDiscount = (subscription: Subscription | undefined) => {
416     return subscription?.CouponCode === COUPON_CODES.MAIL_BLACK_FRIDAY_2022;
419 export const allCycles = Object.freeze(
420     Object.values(CYCLE)
421         .filter((cycle): cycle is CYCLE => typeof cycle === 'number')
422         .sort((a, b) => a - b)
424 export const regularCycles = Object.freeze([CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS]);
425 export const customCycles = Object.freeze(allCycles.filter((cycle) => !regularCycles.includes(cycle)));
427 export const getValidCycle = (cycle: number): CYCLE | undefined => {
428     return allCycles.includes(cycle) ? cycle : undefined;
431 export const getValidAudience = (audience: string | undefined | null): Audience | undefined => {
432     return [Audience.B2B, Audience.B2C, Audience.FAMILY].find((realAudience) => audience === realAudience);
435 export const getIsCustomCycle = (subscription?: Subscription) => {
436     return customCycles.includes(subscription?.Cycle as any);
439 export function getNormalCycleFromCustomCycle(cycle: CYCLE): CYCLE;
440 export function getNormalCycleFromCustomCycle(cycle: undefined): undefined;
441 export function getNormalCycleFromCustomCycle(cycle: CYCLE | undefined): CYCLE | undefined;
442 export function getNormalCycleFromCustomCycle(cycle: CYCLE | undefined): CYCLE | undefined {
443     if (!cycle) {
444         return undefined;
445     }
447     if (regularCycles.includes(cycle)) {
448         return cycle;
449     }
451     // find the closest lower regular cycle
452     for (let i = regularCycles.length - 1; i >= 0; i--) {
453         const regularCycle = regularCycles[i];
455         if (regularCycle < cycle) {
456             return regularCycle;
457         }
458     }
460     // well, that should be unreachable, but let it be just in case
461     return CYCLE.MONTHLY;
464 export function getLongerCycle(cycle: CYCLE): CYCLE;
465 export function getLongerCycle(cycle: CYCLE | undefined): CYCLE | undefined {
466     if (!cycle) {
467         return undefined;
468     }
469     if (cycle === CYCLE.MONTHLY) {
470         return CYCLE.YEARLY;
471     }
472     if (cycle === CYCLE.YEARLY) {
473         return CYCLE.TWO_YEARS;
474     }
476     if (cycle === CYCLE.FIFTEEN || cycle === CYCLE.THIRTY) {
477         return CYCLE.TWO_YEARS;
478     }
480     return cycle;
483 export const hasYearly = (subscription?: Subscription) => {
484     return subscription?.Cycle === CYCLE.YEARLY;
487 export const hasMonthly = (subscription?: Subscription) => {
488     return subscription?.Cycle === CYCLE.MONTHLY;
491 export const hasTwoYears = (subscription?: Subscription) => {
492     return subscription?.Cycle === CYCLE.TWO_YEARS;
495 export const hasFifteen = (subscription?: Subscription) => {
496     return subscription?.Cycle === CYCLE.FIFTEEN;
499 export const hasThirty = (subscription?: Subscription) => {
500     return subscription?.Cycle === CYCLE.THIRTY;
503 export interface PricingForCycles {
504     [CYCLE.MONTHLY]: number;
505     [CYCLE.THREE]: number;
506     [CYCLE.YEARLY]: number;
507     [CYCLE.EIGHTEEN]: number;
508     [CYCLE.TWO_YEARS]: number;
509     [CYCLE.FIFTEEN]: number;
510     [CYCLE.THIRTY]: number;
513 export interface AggregatedPricing {
514     all: PricingForCycles;
515     defaultMonthlyPrice: number;
516     defaultMonthlyPriceWithoutAddons: number;
517     /**
518      * That's pricing that counts only aggregate of cost for members. That's useful for rendering of
519      * "per user per month" pricing.
520      * Examples:
521      * - If you have a B2C plan with 1 user, then this price will be the same as `all`.
522      * - If you have Mail Plus plan with several users, then this price will be the same as `all`, because each
523      *     additional member counts to the price of members.
524      * - If you have Bundle Pro with several users and with the default (minimum) number of custom domains, then
525      *     this price will be the same as `all`.
526      *
527      * Here things become different:
528      * - If you have Bundle Pro with several users and with more than the default (minimum) number of custom domains,
529      *     then this price will be `all - extra custom domains price`.
530      * - For VPN Business the behavior is more complex. It also has two addons: member and IPs/servers. By default it
531      *     has 2 members and 1 IP. The price for members should exclude price for the 1 default IP.
532      */
533     members: PricingForCycles;
534     membersNumber: number;
535     plans: PricingForCycles;
538 function isMultiUserPersonalPlan(plan: Plan) {
539     // even though Duo, Family and Visionary plans can have up to 6 users in the org,
540     // for the price displaying purposes we count it as 1 member.
541     return plan.Name === PLANS.DUO || plan.Name === PLANS.FAMILY || plan.Name === PLANS.VISIONARY;
544 export function getPlanMembers(plan: Plan, quantity: number, view = true): number {
545     const hasMembers = plan.Type === PLAN_TYPES.PLAN || (plan.Type === PLAN_TYPES.ADDON && isMemberAddon(plan.Name));
547     let membersNumberInPlan = 0;
548     if (isMultiUserPersonalPlan(plan) && view) {
549         membersNumberInPlan = 1;
550     } else if (hasMembers) {
551         membersNumberInPlan = plan.MaxMembers || 1;
552     }
554     return membersNumberInPlan * quantity;
557 export function getMembersFromPlanIDs(planIDs: PlanIDs, plansMap: PlansMap, view = true): number {
558     return (Object.entries(planIDs) as [PLANS | ADDON_NAMES, number][]).reduce((acc, [name, quantity]) => {
559         const plan = plansMap[name];
560         if (!plan) {
561             return acc;
562         }
564         return acc + getPlanMembers(plan, quantity, view);
565     }, 0);
568 export const INCLUDED_IP_PRICING = {
569     [CYCLE.MONTHLY]: 4999,
570     [CYCLE.YEARLY]: 3999 * CYCLE.YEARLY,
571     [CYCLE.TWO_YEARS]: 3599 * CYCLE.TWO_YEARS,
574 function getIpPrice(cycle: CYCLE): number {
575     if (cycle === CYCLE.MONTHLY) {
576         return INCLUDED_IP_PRICING[CYCLE.MONTHLY];
577     }
579     if (cycle === CYCLE.YEARLY) {
580         return INCLUDED_IP_PRICING[CYCLE.YEARLY];
581     }
583     if (cycle === CYCLE.TWO_YEARS) {
584         return INCLUDED_IP_PRICING[CYCLE.TWO_YEARS];
585     }
587     return 0;
590 export function getIpPricePerMonth(cycle: CYCLE): number {
591     return getIpPrice(cycle) / cycle;
595  * The purpose of this overridden price is to show a coupon discount in the cycle selector. If that would be supported
596  * this would not be needed.
597  */
598 export const getPricePerCycle = (plan: Plan | undefined, cycle: CYCLE) => {
599     return plan?.Pricing?.[cycle];
602 export function getPricePerMember(plan: Plan, cycle: CYCLE): number {
603     const totalPrice = getPricePerCycle(plan, cycle) || 0;
605     if (plan.Name === PLANS.VPN_BUSINESS) {
606         // For VPN business, we exclude IP price from calculation. And we also divide by 2,
607         // because it has 2 members by default too.
608         const IP_PRICE = getIpPrice(cycle);
609         return (totalPrice - IP_PRICE) / (plan.MaxMembers || 1);
610     }
612     if (isMultiUserPersonalPlan(plan)) {
613         return totalPrice;
614     }
616     // Some plans have 0 MaxMembers. That's because they don't have access to mail.
617     // In reality, they still get 1 member.
618     return totalPrice / (plan.MaxMembers || 1);
621 export function getPricingPerMember(plan: Plan): Pricing {
622     return allCycles.reduce((acc, cycle) => {
623         acc[cycle] = getPricePerMember(plan, cycle);
625         // If the plan doesn't have custom cycles, we need to remove it from the resulting Pricing object
626         const isNonDefinedCycle = acc[cycle] === undefined || acc[cycle] === null || acc[cycle] === 0;
627         if (customCycles.includes(cycle) && isNonDefinedCycle) {
628             delete acc[cycle];
629         }
631         return acc;
632     }, {} as Pricing);
635 interface OfferResult {
636     pricing: Pricing;
637     cycles: CYCLE[];
638     valid: boolean;
641 export const getPlanOffer = (plan: Plan) => {
642     const result = [CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS].reduce<OfferResult>(
643         (acc, cycle) => {
644             acc.pricing[cycle] = (plan.DefaultPricing?.[cycle] ?? 0) - (getPricePerCycle(plan, cycle) ?? 0);
645             return acc;
646         },
647         {
648             valid: false,
649             cycles: [],
650             pricing: {
651                 [CYCLE.MONTHLY]: 0,
652                 [CYCLE.YEARLY]: 0,
653                 [CYCLE.THREE]: 0,
654                 [CYCLE.TWO_YEARS]: 0,
655                 [CYCLE.FIFTEEN]: 0,
656                 [CYCLE.EIGHTEEN]: 0,
657                 [CYCLE.THIRTY]: 0,
658             },
659         }
660     );
661     const sortedResults = (Object.entries(result.pricing) as unknown as [CYCLE, number][]).sort((a, b) => b[1] - a[1]);
662     result.cycles = sortedResults.map(([cycle]) => cycle);
663     if (sortedResults[0][1] > 0) {
664         result.valid = true;
665     }
666     return result;
670  * Currently there is no convenient way to get the number of IPs for a VPN subscription.
671  * There is no dedicated field for that in the API.
672  * That's a hack that counts the number of IP addons.
673  */
674 export const getVPNDedicatedIPs = (subscription: Subscription | undefined) => {
675     const planName = getPlanName(subscription, PLAN_SERVICES.VPN);
677     // If you have other VPN plans, they don't have dedicated IPs
678     if (!planName) {
679         return 0;
680     }
682     // Some plans might have included IPs without any indication on the backend.
683     // For example, 1 IP is included in the Business plan
684     const includedIPs = IPS_INCLUDED_IN_PLAN[planName] || 0;
686     return (subscription as Subscription).Plans.reduce(
687         (acc, { Name: addonOrPlanName, Quantity }) => acc + (isIpAddon(addonOrPlanName) ? Quantity : 0),
688         includedIPs
689     );
692 export const getHasCoupon = (subscription: Subscription | undefined, coupon: string) => {
693     return [subscription?.CouponCode, subscription?.UpcomingSubscription?.CouponCode].includes(coupon);
697  * Checks if subscription can be cancelled by a user. Cancellation means that the user will be downgraded at the end
698  * of the current billing cycle. In contrast, "Downgrade subscription" button means that the user will be downgraded
699  * immediately. Note that B2B subscriptions also have "Cancel subscription" button, but it behaves differently, so
700  * we don't consider B2B subscriptions cancellable for the purpose of this function.
701  */
702 export const hasCancellablePlan = (subscription: Subscription | undefined, user: UserModel) => {
703     // These plans are can be cancelled inhouse too
704     const cancellablePlan = getHasConsumerVpnPlan(subscription) || hasPass(subscription);
706     // In Chargebee, all plans are cancellable
707     const chargebeeForced = onSessionMigrationChargebeeStatus(user, subscription) === ChargebeeEnabled.CHARGEBEE_FORCED;
709     // Splitted users should go to PUT v4 renew because they still have an active subscription in inhouse system
710     // And we force them to do the renew cancellation instead of subscription deletion because this case is much
711     // simpler to handle
712     const splittedUser = isSplittedUser(user.ChargebeeUser, user.ChargebeeUserExists, subscription?.BillingPlatform);
714     return cancellablePlan || chargebeeForced || splittedUser;
717 export function hasMaximumCycle(subscription?: SubscriptionModel | FreeSubscription): boolean {
718     return (
719         subscription?.Cycle === CYCLE.TWO_YEARS ||
720         subscription?.Cycle === CYCLE.THIRTY ||
721         subscription?.UpcomingSubscription?.Cycle === CYCLE.TWO_YEARS ||
722         subscription?.UpcomingSubscription?.Cycle === CYCLE.THIRTY
723     );