From a9bf66e689c33451f9014108ffb7d44655b49516 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Tue, 2 Dec 2025 10:49:55 -0500 Subject: [PATCH] [PM-27600] Replace Hard-Coded Storage amount (#17393) * feat(billing): add provided as a required property to premium response * fix(billing): replace hard coded storage variables with retrieved plan * tests(billing): add tests to pricing-summary service * feat(billing): add optional property. * fix(billing): update storage logic * fix(billing): remove optional check * fix(billing): remove optionality * fix(billing): remove optionality * fix(billing): refactored storage calculation logic * feat(billing): add provided amounts to subscription-pricing-service * fix(billing): update cloud premium component * fix(billing): update desktop premium component * fix(billing): update org plans component * fix(billing) update stories and tests * fix(billing): update messages * fix(billing): replace storage sizes * fix(billing): update messages * fix(billing): update components * fix(billing): update components for pricing and storage retrieval * fix(billing): revert self-hosted change --- apps/browser/src/_locales/en/messages.json | 9 + .../popup/settings/premium-v2.component.html | 2 +- .../popup/settings/premium-v2.component.ts | 13 +- .../app/accounts/premium.component.html | 2 +- .../billing/app/accounts/premium.component.ts | 3 + apps/desktop/src/locales/en/messages.json | 9 + .../cloud-hosted-premium.component.html | 7 +- .../premium/cloud-hosted-premium.component.ts | 7 +- .../change-plan-dialog.component.ts | 12 +- .../organization-plans.component.html | 4 +- .../services/pricing-summary.service.spec.ts | 232 ++++++++++++++++++ .../services/pricing-summary.service.ts | 6 +- apps/web/src/locales/en/messages.json | 11 +- .../premium-upgrade-dialog.component.spec.ts | 2 + ...remium-upgrade-dialog.component.stories.ts | 1 + .../billing/components/premium.component.ts | 6 + .../models/response/premium-plan.response.ts | 7 + .../subscription-pricing.service.spec.ts | 18 ++ .../services/subscription-pricing.service.ts | 7 + .../types/subscription-pricing-tier.ts | 9 +- 20 files changed, 343 insertions(+), 24 deletions(-) create mode 100644 apps/web/src/app/billing/services/pricing-summary.service.spec.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 2114949948..6a7df1678b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1475,6 +1475,15 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpStorageV2": { + "message": "$SIZE$ encrypted storage for file attachments.", + "placeholders": { + "size": { + "content": "$1", + "example": "1 GB" + } + } + }, "premiumSignUpEmergency": { "message": "Emergency access." }, diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.html b/apps/browser/src/billing/popup/settings/premium-v2.component.html index 47d72751af..fea3e55805 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.html +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.html @@ -12,7 +12,7 @@
diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts index fceeeedf17..86a508d270 100644 --- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts @@ -22,8 +22,8 @@ import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { DefaultSubscriptionPricingService } from "@bitwarden/common/billing/services/subscription-pricing.service"; import { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -75,6 +75,7 @@ export class CloudHostedPremiumComponent { return { seat: premiumPlan.passwordManager.annualPrice, storage: premiumPlan.passwordManager.annualPricePerAdditionalStorageGB, + providedStorageGb: premiumPlan.passwordManager.providedStorageGB, }; }), shareReplay({ bufferSize: 1, refCount: true }), @@ -84,6 +85,8 @@ export class CloudHostedPremiumComponent { storagePrice$ = this.premiumPrices$.pipe(map((prices) => prices.storage)); + providedStorageGb$ = this.premiumPrices$.pipe(map((prices) => prices.providedStorageGb)); + protected isLoadingPrices$ = this.premiumPrices$.pipe( map(() => false), startWith(true), @@ -134,7 +137,7 @@ export class CloudHostedPremiumComponent { private accountService: AccountService, private subscriberBillingClient: SubscriberBillingClient, private taxClient: TaxClient, - private subscriptionPricingService: DefaultSubscriptionPricingService, + private subscriptionPricingService: SubscriptionPricingServiceAbstraction, ) { this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( switchMap((account) => diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 0fd7746fc9..978bb35c5c 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -620,7 +620,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get storageGb() { - return this.sub?.maxStorageGb ? this.sub?.maxStorageGb - 1 : 0; + return Math.max( + 0, + (this.sub?.maxStorageGb ?? 0) - this.selectedPlan.PasswordManager.baseStorageGb, + ); } passwordManagerSeatTotal(plan: PlanResponse): number { @@ -644,12 +647,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return 0; } - return ( - plan.PasswordManager.additionalStoragePricePerGb * - // TODO: Eslint upgrade. Please resolve this since the null check does nothing - // eslint-disable-next-line no-constant-binary-expression - Math.abs(this.sub?.maxStorageGb ? this.sub?.maxStorageGb - 1 : 0 || 0) - ); + return plan.PasswordManager.additionalStoragePricePerGb * this.storageGb; } additionalStoragePriceMonthly(selectedPlan: PlanResponse) { diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index 6234fc6e6e..d06604ba29 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -104,7 +104,7 @@
  • {{ "gbEncryptedFileStorage" - | i18n: selectableProduct.PasswordManager.baseStorageGb + "GB" + | i18n: selectableProduct.PasswordManager.baseStorageGb + " GB" }}
  • @@ -239,7 +239,7 @@ {{ "additionalStorageIntervalDesc" | i18n - : "1 GB" + : `${selectedPlan.PasswordManager.baseStorageGb} GB` : (additionalStoragePriceMonthly(selectedPlan) | currency: "$") : ("month" | i18n) }} diff --git a/apps/web/src/app/billing/services/pricing-summary.service.spec.ts b/apps/web/src/app/billing/services/pricing-summary.service.spec.ts new file mode 100644 index 0000000000..4e15d318a0 --- /dev/null +++ b/apps/web/src/app/billing/services/pricing-summary.service.spec.ts @@ -0,0 +1,232 @@ +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums"; +import { + BillingCustomerDiscount, + OrganizationSubscriptionResponse, +} from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { + PasswordManagerPlanFeaturesResponse, + PlanResponse, + SecretsManagerPlanFeaturesResponse, +} from "@bitwarden/common/billing/models/response/plan.response"; + +import { PricingSummaryData } from "../shared/pricing-summary/pricing-summary.component"; + +import { PricingSummaryService } from "./pricing-summary.service"; + +describe("PricingSummaryService", () => { + let service: PricingSummaryService; + + beforeEach(() => { + service = new PricingSummaryService(); + }); + + describe("getPricingSummaryData", () => { + let mockPlan: PlanResponse; + let mockSub: OrganizationSubscriptionResponse; + let mockOrganization: Organization; + + beforeEach(() => { + // Create mock plan with password manager features + mockPlan = { + productTier: ProductTierType.Teams, + PasswordManager: { + basePrice: 0, + seatPrice: 48, + baseSeats: 0, + hasAdditionalSeatsOption: true, + hasPremiumAccessOption: false, + premiumAccessOptionPrice: 0, + hasAdditionalStorageOption: true, + additionalStoragePricePerGb: 6, + baseStorageGb: 1, + } as PasswordManagerPlanFeaturesResponse, + SecretsManager: { + basePrice: 0, + seatPrice: 72, + baseSeats: 3, + hasAdditionalSeatsOption: true, + hasAdditionalServiceAccountOption: true, + additionalPricePerServiceAccount: 6, + baseServiceAccount: 50, + } as SecretsManagerPlanFeaturesResponse, + } as PlanResponse; + + // Create mock subscription + mockSub = { + seats: 5, + smSeats: 5, + smServiceAccounts: 5, + maxStorageGb: 2, + customerDiscount: null, + } as OrganizationSubscriptionResponse; + + // Create mock organization + mockOrganization = { + useSecretsManager: false, + } as Organization; + }); + + it("should calculate pricing data correctly for password manager only", async () => { + const result = await service.getPricingSummaryData( + mockPlan, + mockSub, + mockOrganization, + PlanInterval.Monthly, + false, + 50, // estimatedTax + ); + + expect(result).toEqual({ + selectedPlanInterval: "month", + passwordManagerSeats: 5, + passwordManagerSeatTotal: 240, // 48 * 5 + secretsManagerSeatTotal: 360, // 72 * 5 + additionalStorageTotal: 6, // 6 * (2 - 1) + additionalStoragePriceMonthly: 6, + additionalServiceAccountTotal: 0, // No additional service accounts (50 base vs 5 used) + totalAppliedDiscount: 0, + secretsManagerSubtotal: 360, // 0 + 360 + 0 + passwordManagerSubtotal: 246, // 0 + 240 + 6 + total: 296, // 246 + 50 (tax) - organization doesn't use secrets manager + organization: mockOrganization, + sub: mockSub, + selectedPlan: mockPlan, + selectedInterval: PlanInterval.Monthly, + discountPercentageFromSub: 0, + discountPercentage: 20, + acceptingSponsorship: false, + additionalServiceAccount: 0, // 50 - 5 = 45, which is > 0, so return 0 + storageGb: 1, + isSecretsManagerTrial: false, + estimatedTax: 50, + }); + }); + + it("should calculate pricing data correctly with secrets manager enabled", async () => { + mockOrganization.useSecretsManager = true; + + const result = await service.getPricingSummaryData( + mockPlan, + mockSub, + mockOrganization, + PlanInterval.Monthly, + false, + 50, + ); + + expect(result.total).toBe(656); // passwordManagerSubtotal (246) + secretsManagerSubtotal (360) + tax (50) + }); + + it("should handle secrets manager trial", async () => { + const result = await service.getPricingSummaryData( + mockPlan, + mockSub, + mockOrganization, + PlanInterval.Monthly, + true, // isSecretsManagerTrial + 50, + ); + + expect(result.passwordManagerSeatTotal).toBe(0); // Should be 0 during trial + expect(result.discountPercentageFromSub).toBe(0); // Should be 0 during trial + }); + + it("should handle premium access option", async () => { + mockPlan.PasswordManager.hasPremiumAccessOption = true; + mockPlan.PasswordManager.premiumAccessOptionPrice = 25; + + const result = await service.getPricingSummaryData( + mockPlan, + mockSub, + mockOrganization, + PlanInterval.Monthly, + false, + 50, + ); + + expect(result.passwordManagerSubtotal).toBe(271); // 0 + 240 + 6 + 25 + }); + + it("should handle customer discount", async () => { + mockSub.customerDiscount = { + id: "discount1", + active: true, + percentOff: 10, + appliesTo: ["subscription"], + } as BillingCustomerDiscount; + + const result = await service.getPricingSummaryData( + mockPlan, + mockSub, + mockOrganization, + PlanInterval.Monthly, + false, + 50, + ); + + expect(result.discountPercentageFromSub).toBe(10); + }); + + it("should handle zero storage calculation", async () => { + mockSub.maxStorageGb = 1; // Same as base storage + + const result = await service.getPricingSummaryData( + mockPlan, + mockSub, + mockOrganization, + PlanInterval.Monthly, + false, + 50, + ); + + expect(result.additionalStorageTotal).toBe(0); + expect(result.storageGb).toBe(0); + }); + }); + + describe("getAdditionalServiceAccount", () => { + let mockPlan: PlanResponse; + let mockSub: OrganizationSubscriptionResponse; + + beforeEach(() => { + mockPlan = { + SecretsManager: { + baseServiceAccount: 50, + } as SecretsManagerPlanFeaturesResponse, + } as PlanResponse; + + mockSub = { + smServiceAccounts: 55, + } as OrganizationSubscriptionResponse; + }); + + it("should return additional service accounts when used exceeds base", () => { + const result = service.getAdditionalServiceAccount(mockPlan, mockSub); + expect(result).toBe(5); // Math.abs(50 - 55) = 5 + }); + + it("should return 0 when used is less than or equal to base", () => { + mockSub.smServiceAccounts = 40; + const result = service.getAdditionalServiceAccount(mockPlan, mockSub); + expect(result).toBe(0); + }); + + it("should return 0 when used equals base", () => { + mockSub.smServiceAccounts = 50; + const result = service.getAdditionalServiceAccount(mockPlan, mockSub); + expect(result).toBe(0); + }); + + it("should return 0 when plan is null", () => { + const result = service.getAdditionalServiceAccount(null, mockSub); + expect(result).toBe(0); + }); + + it("should return 0 when plan has no SecretsManager", () => { + mockPlan.SecretsManager = null; + const result = service.getAdditionalServiceAccount(mockPlan, mockSub); + expect(result).toBe(0); + }); + }); +}); diff --git a/apps/web/src/app/billing/services/pricing-summary.service.ts b/apps/web/src/app/billing/services/pricing-summary.service.ts index b3c071a8b8..da2fe0e8db 100644 --- a/apps/web/src/app/billing/services/pricing-summary.service.ts +++ b/apps/web/src/app/billing/services/pricing-summary.service.ts @@ -31,9 +31,10 @@ export class PricingSummaryService { const additionalServiceAccount = this.getAdditionalServiceAccount(plan, sub); + const storageGb = Math.max(0, (sub?.maxStorageGb ?? 0) - plan.PasswordManager.baseStorageGb); + const additionalStorageTotal = plan.PasswordManager?.hasAdditionalStorageOption - ? plan.PasswordManager.additionalStoragePricePerGb * - (sub?.maxStorageGb ? sub.maxStorageGb - 1 : 0) + ? plan.PasswordManager.additionalStoragePricePerGb * storageGb : 0; const additionalStoragePriceMonthly = plan.PasswordManager?.additionalStoragePricePerGb || 0; @@ -66,7 +67,6 @@ export class PricingSummaryService { : (sub?.customerDiscount?.percentOff ?? 0); const discountPercentage = 20; const acceptingSponsorship = false; - const storageGb = sub?.maxStorageGb ? sub?.maxStorageGb - 1 : 0; const total = organization?.useSecretsManager ? passwordManagerSubtotal + secretsManagerSubtotal + estimatedTax diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 90468c61d5..582efade7f 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3060,7 +3060,16 @@ "message": "Upgrade your account to a Premium membership and unlock some great additional features." }, "premiumSignUpStorage": { - "message": "1 GB encrypted storage for file attachments." + "message": "1 GB encrypted storage for file attachments." + }, + "premiumSignUpStorageV2": { + "message": "$SIZE$ encrypted storage for file attachments.", + "placeholders": { + "size": { + "content": "$1", + "example": "1 GB" + } + } }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." diff --git a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.spec.ts b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.spec.ts index 107eb068e7..30a4d38b1d 100644 --- a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.spec.ts +++ b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.spec.ts @@ -40,6 +40,7 @@ describe("PremiumUpgradeDialogComponent", () => { type: "standalone", annualPrice: 10, annualPricePerAdditionalStorageGB: 4, + providedStorageGB: 1, features: [ { key: "feature1", value: "Feature 1" }, { key: "feature2", value: "Feature 2" }, @@ -58,6 +59,7 @@ describe("PremiumUpgradeDialogComponent", () => { users: 6, annualPrice: 40, annualPricePerAdditionalStorageGB: 4, + providedStorageGB: 1, features: [{ key: "featureA", value: "Feature A" }], }, }; diff --git a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.stories.ts b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.stories.ts index 7ba09192d3..7fd66878ca 100644 --- a/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.stories.ts +++ b/libs/angular/src/billing/components/premium-upgrade-dialog/premium-upgrade-dialog.component.stories.ts @@ -31,6 +31,7 @@ const mockPremiumTier: PersonalSubscriptionPricingTier = { type: "standalone", annualPrice: 10, annualPricePerAdditionalStorageGB: 4, + providedStorageGB: 1, features: [ { key: "builtInAuthenticator", value: "Built-in authenticator" }, { key: "secureFileStorage", value: "Secure file storage" }, diff --git a/libs/angular/src/billing/components/premium.component.ts b/libs/angular/src/billing/components/premium.component.ts index 6d0b90385b..3f53d62e56 100644 --- a/libs/angular/src/billing/components/premium.component.ts +++ b/libs/angular/src/billing/components/premium.component.ts @@ -5,6 +5,7 @@ import { firstValueFrom, Observable, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -16,6 +17,7 @@ import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/com export class PremiumComponent implements OnInit { isPremium$: Observable; price = 10; + storageProvidedGb = 0; refreshPromise: Promise; cloudWebVaultUrl: string; @@ -29,6 +31,7 @@ export class PremiumComponent implements OnInit { billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, accountService: AccountService, + private billingApiService: BillingApiServiceAbstraction, ) { this.isPremium$ = accountService.activeAccount$.pipe( switchMap((account) => @@ -39,6 +42,9 @@ export class PremiumComponent implements OnInit { async ngOnInit() { this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); + const premiumResponse = await this.billingApiService.getPremiumPlan(); + this.storageProvidedGb = premiumResponse.storage.provided; + this.price = premiumResponse.seat.price; } async refresh() { diff --git a/libs/common/src/billing/models/response/premium-plan.response.ts b/libs/common/src/billing/models/response/premium-plan.response.ts index f5df560a60..73e4f834c6 100644 --- a/libs/common/src/billing/models/response/premium-plan.response.ts +++ b/libs/common/src/billing/models/response/premium-plan.response.ts @@ -4,10 +4,12 @@ export class PremiumPlanResponse extends BaseResponse { seat: { stripePriceId: string; price: number; + provided: number; }; storage: { stripePriceId: string; price: number; + provided: number; }; constructor(response: any) { @@ -30,6 +32,7 @@ export class PremiumPlanResponse extends BaseResponse { class PurchasableResponse extends BaseResponse { stripePriceId: string; price: number; + provided: number; constructor(response: any) { super(response); @@ -43,5 +46,9 @@ class PurchasableResponse extends BaseResponse { if (typeof this.price !== "number" || isNaN(this.price)) { throw new Error("PurchasableResponse: Missing or invalid 'Price' property"); } + this.provided = this.getResponseProperty("Provided"); + if (typeof this.provided !== "number" || isNaN(this.provided)) { + throw new Error("PurchasableResponse: Missing or invalid 'Provided' property"); + } } } diff --git a/libs/common/src/billing/services/subscription-pricing.service.spec.ts b/libs/common/src/billing/services/subscription-pricing.service.spec.ts index 8f5e9c0a3a..76e15c646a 100644 --- a/libs/common/src/billing/services/subscription-pricing.service.spec.ts +++ b/libs/common/src/billing/services/subscription-pricing.service.spec.ts @@ -55,6 +55,7 @@ describe("DefaultSubscriptionPricingService", () => { basePrice: 36, seatPrice: 0, additionalStoragePricePerGb: 4, + providedStorageGB: 1, allowSeatAutoscale: false, maxSeats: 6, maxCollections: null, @@ -94,6 +95,7 @@ describe("DefaultSubscriptionPricingService", () => { basePrice: 0, seatPrice: 36, additionalStoragePricePerGb: 4, + providedStorageGB: 1, allowSeatAutoscale: true, maxSeats: null, maxCollections: null, @@ -359,6 +361,7 @@ describe("DefaultSubscriptionPricingService", () => { type: "standalone", annualPrice: 10, annualPricePerAdditionalStorageGB: 4, + providedStorageGB: 1, features: [ { key: "builtInAuthenticator", value: "Built-in authenticator" }, { key: "secureFileStorage", value: "Secure file storage" }, @@ -383,6 +386,7 @@ describe("DefaultSubscriptionPricingService", () => { annualPrice: mockFamiliesPlan.PasswordManager.basePrice, annualPricePerAdditionalStorageGB: mockFamiliesPlan.PasswordManager.additionalStoragePricePerGb, + providedStorageGB: mockFamiliesPlan.PasswordManager.baseStorageGb, features: [ { key: "premiumAccounts", value: "6 premium accounts" }, { key: "familiesUnlimitedSharing", value: "Unlimited sharing for families" }, @@ -456,6 +460,7 @@ describe("DefaultSubscriptionPricingService", () => { expect(premiumTier.passwordManager.annualPrice).toEqual(10); expect(premiumTier.passwordManager.annualPricePerAdditionalStorageGB).toEqual(4); + expect(premiumTier.passwordManager.providedStorageGB).toEqual(1); expect(familiesTier.passwordManager.annualPrice).toEqual( mockFamiliesPlan.PasswordManager.basePrice, @@ -463,6 +468,9 @@ describe("DefaultSubscriptionPricingService", () => { expect(familiesTier.passwordManager.annualPricePerAdditionalStorageGB).toEqual( mockFamiliesPlan.PasswordManager.additionalStoragePricePerGb, ); + expect(familiesTier.passwordManager.providedStorageGB).toEqual( + mockFamiliesPlan.PasswordManager.baseStorageGb, + ); done(); }); @@ -487,6 +495,7 @@ describe("DefaultSubscriptionPricingService", () => { annualPricePerUser: mockTeamsPlan.PasswordManager.seatPrice, annualPricePerAdditionalStorageGB: mockTeamsPlan.PasswordManager.additionalStoragePricePerGb, + providedStorageGB: mockTeamsPlan.PasswordManager.baseStorageGb, features: [ { key: "secureItemSharing", value: "Secure item sharing" }, { key: "eventLogMonitoring", value: "Event log monitoring" }, @@ -522,6 +531,7 @@ describe("DefaultSubscriptionPricingService", () => { annualPricePerUser: mockEnterprisePlan.PasswordManager.seatPrice, annualPricePerAdditionalStorageGB: mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb, + providedStorageGB: mockEnterprisePlan.PasswordManager.baseStorageGb, features: [ { key: "enterpriseSecurityPolicies", value: "Enterprise security policies" }, { key: "passwordLessSso", value: "Passwordless SSO" }, @@ -648,6 +658,9 @@ describe("DefaultSubscriptionPricingService", () => { expect(teamsSecretsManager.annualPricePerAdditionalServiceAccount).toEqual( mockTeamsPlan.SecretsManager.additionalPricePerServiceAccount, ); + expect(teamsPasswordManager.providedStorageGB).toEqual( + mockTeamsPlan.PasswordManager.baseStorageGb, + ); const enterprisePasswordManager = enterpriseTier.passwordManager as any; const enterpriseSecretsManager = enterpriseTier.secretsManager as any; @@ -657,6 +670,9 @@ describe("DefaultSubscriptionPricingService", () => { expect(enterprisePasswordManager.annualPricePerAdditionalStorageGB).toEqual( mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb, ); + expect(enterprisePasswordManager.providedStorageGB).toEqual( + mockEnterprisePlan.PasswordManager.baseStorageGb, + ); expect(enterpriseSecretsManager.annualPricePerUser).toEqual( mockEnterprisePlan.SecretsManager.seatPrice, ); @@ -729,6 +745,7 @@ describe("DefaultSubscriptionPricingService", () => { annualPricePerUser: mockTeamsPlan.PasswordManager.seatPrice, annualPricePerAdditionalStorageGB: mockTeamsPlan.PasswordManager.additionalStoragePricePerGb, + providedStorageGB: mockTeamsPlan.PasswordManager.baseStorageGb, features: [ { key: "secureItemSharing", value: "Secure item sharing" }, { key: "eventLogMonitoring", value: "Event log monitoring" }, @@ -764,6 +781,7 @@ describe("DefaultSubscriptionPricingService", () => { annualPricePerUser: mockEnterprisePlan.PasswordManager.seatPrice, annualPricePerAdditionalStorageGB: mockEnterprisePlan.PasswordManager.additionalStoragePricePerGb, + providedStorageGB: mockEnterprisePlan.PasswordManager.baseStorageGb, features: [ { key: "enterpriseSecurityPolicies", value: "Enterprise security policies" }, { key: "passwordLessSso", value: "Passwordless SSO" }, diff --git a/libs/common/src/billing/services/subscription-pricing.service.ts b/libs/common/src/billing/services/subscription-pricing.service.ts index f1502eb26e..a3f048fee7 100644 --- a/libs/common/src/billing/services/subscription-pricing.service.ts +++ b/libs/common/src/billing/services/subscription-pricing.service.ts @@ -40,6 +40,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer */ private static readonly FALLBACK_PREMIUM_SEAT_PRICE = 10; private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4; + private static readonly FALLBACK_PREMIUM_PROVIDED_STORAGE_GB = 1; constructor( private billingApiService: BillingApiServiceAbstraction, @@ -114,11 +115,13 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer map((premiumPlan) => ({ seat: premiumPlan.seat.price, storage: premiumPlan.storage.price, + provided: premiumPlan.storage.provided, })), ) : of({ seat: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE, storage: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE, + provided: DefaultSubscriptionPricingService.FALLBACK_PREMIUM_PROVIDED_STORAGE_GB, }), ), map((premiumPrices) => ({ @@ -130,6 +133,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer type: "standalone", annualPrice: premiumPrices.seat, annualPricePerAdditionalStorageGB: premiumPrices.storage, + providedStorageGB: premiumPrices.provided, features: [ this.featureTranslations.builtInAuthenticator(), this.featureTranslations.secureFileStorage(), @@ -161,6 +165,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer annualPrice: familiesPlan.PasswordManager.basePrice, annualPricePerAdditionalStorageGB: familiesPlan.PasswordManager.additionalStoragePricePerGb, + providedStorageGB: familiesPlan.PasswordManager.baseStorageGb, features: [ this.featureTranslations.premiumAccounts(), this.featureTranslations.familiesUnlimitedSharing(), @@ -214,6 +219,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer annualPricePerUser: annualTeamsPlan.PasswordManager.seatPrice, annualPricePerAdditionalStorageGB: annualTeamsPlan.PasswordManager.additionalStoragePricePerGb, + providedStorageGB: annualTeamsPlan.PasswordManager.baseStorageGb, features: [ this.featureTranslations.secureItemSharing(), this.featureTranslations.eventLogMonitoring(), @@ -253,6 +259,7 @@ export class DefaultSubscriptionPricingService implements SubscriptionPricingSer annualPricePerUser: annualEnterprisePlan.PasswordManager.seatPrice, annualPricePerAdditionalStorageGB: annualEnterprisePlan.PasswordManager.additionalStoragePricePerGb, + providedStorageGB: annualEnterprisePlan.PasswordManager.baseStorageGb, features: [ this.featureTranslations.enterpriseSecurityPolicies(), this.featureTranslations.passwordLessSso(), diff --git a/libs/common/src/billing/types/subscription-pricing-tier.ts b/libs/common/src/billing/types/subscription-pricing-tier.ts index 8febc4b86d..3f5c076ba4 100644 --- a/libs/common/src/billing/types/subscription-pricing-tier.ts +++ b/libs/common/src/billing/types/subscription-pricing-tier.ts @@ -30,13 +30,19 @@ type HasAdditionalStorage = { annualPricePerAdditionalStorageGB: number; }; +type HasProvidedStorage = { + providedStorageGB: number; +}; + type StandalonePasswordManager = HasFeatures & - HasAdditionalStorage & { + HasAdditionalStorage & + HasProvidedStorage & { type: "standalone"; annualPrice: number; }; type PackagedPasswordManager = HasFeatures & + HasProvidedStorage & HasAdditionalStorage & { type: "packaged"; users: number; @@ -52,6 +58,7 @@ type CustomPasswordManager = HasFeatures & { }; type ScalablePasswordManager = HasFeatures & + HasProvidedStorage & HasAdditionalStorage & { type: "scalable"; annualPricePerUser: number;