From e41680df41488b4c81300a381e7cfdea51399f2a Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:35:34 +0100 Subject: [PATCH] [PM 26691] [Fix: Remove dollar amount from total section when redeeming free families for enterprise (#16887) * Resolve the dollar amount issue * Resolve the non addition of storage amount * Resolve the estimate tax amount * Fix the improper tax calculation * resolv ethe duplicate code * Added changes to apply the discount only for acceptingSponsorship = true --- .../change-plan-dialog.component.ts | 16 +-- .../organization-plans.component.ts | 134 ++++++++++++------ .../services/pricing-summary.service.ts | 7 +- 3 files changed, 97 insertions(+), 60 deletions(-) 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 e2a30dd585c..b0bdf31076b 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 @@ -670,6 +670,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { if (this.selectedPlan.PasswordManager.hasPremiumAccessOption) { subTotal += this.selectedPlan.PasswordManager.premiumAccessOptionPrice; } + if (this.selectedPlan.PasswordManager.hasAdditionalStorageOption) { + subTotal += this.additionalStorageTotal(this.selectedPlan); + } return subTotal - this.discount; } @@ -707,18 +710,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } if (this.organization.useSecretsManager) { - return ( - this.passwordManagerSubtotal + - this.additionalStorageTotal(this.selectedPlan) + - this.secretsManagerSubtotal() + - this.estimatedTax - ); + return this.passwordManagerSubtotal + this.secretsManagerSubtotal() + this.estimatedTax; } - return ( - this.passwordManagerSubtotal + - this.additionalStorageTotal(this.selectedPlan) + - this.estimatedTax - ); + return this.passwordManagerSubtotal + this.estimatedTax; } get teamsStarterPlanIsAvailable() { diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 7c081b38279..0fa0b59b3cd 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -49,6 +49,7 @@ import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { OrganizationSubscriptionPlan, + OrganizationSubscriptionPurchase, SubscriberBillingClient, TaxClient, } from "@bitwarden/web-vault/app/billing/clients"; @@ -478,7 +479,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } get passwordManagerSubtotal() { - let subTotal = this.selectedPlan.PasswordManager.basePrice; + const basePriceAfterDiscount = this.acceptingSponsorship + ? Math.max(this.selectedPlan.PasswordManager.basePrice - this.discount, 0) + : this.selectedPlan.PasswordManager.basePrice; + let subTotal = basePriceAfterDiscount; if ( this.selectedPlan.PasswordManager.hasAdditionalSeatsOption && this.formGroup.controls.additionalSeats.value @@ -488,19 +492,19 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.formGroup.value.additionalSeats, ); } - if ( - this.selectedPlan.PasswordManager.hasAdditionalStorageOption && - this.formGroup.controls.additionalStorage.value - ) { - subTotal += this.additionalStorageTotal(this.selectedPlan); - } if ( this.selectedPlan.PasswordManager.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value ) { subTotal += this.selectedPlan.PasswordManager.premiumAccessOptionPrice; } - return subTotal - this.discount; + if ( + this.selectedPlan.PasswordManager.hasAdditionalStorageOption && + this.formGroup.controls.additionalStorage.value + ) { + subTotal += this.additionalStorageTotal(this.selectedPlan); + } + return subTotal; } get secretsManagerSubtotal() { @@ -707,54 +711,90 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } } + private getPlanFromLegacyEnum(): OrganizationSubscriptionPlan { + switch (this.formGroup.value.plan) { + case PlanType.FamiliesAnnually: + return { tier: "families", cadence: "annually" }; + case PlanType.TeamsMonthly: + return { tier: "teams", cadence: "monthly" }; + case PlanType.TeamsAnnually: + return { tier: "teams", cadence: "annually" }; + case PlanType.EnterpriseMonthly: + return { tier: "enterprise", cadence: "monthly" }; + case PlanType.EnterpriseAnnually: + return { tier: "enterprise", cadence: "annually" }; + } + } + + private buildTaxPreviewRequest( + additionalStorage: number, + sponsored: boolean, + ): OrganizationSubscriptionPurchase { + const passwordManagerSeats = this.selectedPlan.PasswordManager.hasAdditionalSeatsOption + ? this.formGroup.value.additionalSeats + : 1; + + return { + ...this.getPlanFromLegacyEnum(), + passwordManager: { + seats: passwordManagerSeats, + additionalStorage, + sponsored, + }, + secretsManager: this.formGroup.value.secretsManager.enabled + ? { + seats: this.secretsManagerForm.value.userSeats, + additionalServiceAccounts: this.secretsManagerForm.value.additionalServiceAccounts, + standalone: false, + } + : undefined, + }; + } + private async refreshSalesTax(): Promise { if (this.billingFormGroup.controls.billingAddress.invalid) { return; } - const getPlanFromLegacyEnum = (): OrganizationSubscriptionPlan => { - switch (this.formGroup.value.plan) { - case PlanType.FamiliesAnnually: - return { tier: "families", cadence: "annually" }; - case PlanType.TeamsMonthly: - return { tier: "teams", cadence: "monthly" }; - case PlanType.TeamsAnnually: - return { tier: "teams", cadence: "annually" }; - case PlanType.EnterpriseMonthly: - return { tier: "enterprise", cadence: "monthly" }; - case PlanType.EnterpriseAnnually: - return { tier: "enterprise", cadence: "annually" }; - } - }; - const billingAddress = getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress); - const passwordManagerSeats = - this.formGroup.value.productTier === ProductTierType.Families - ? 1 - : this.formGroup.value.additionalSeats; + // should still be taxed. We mark the plan as NOT sponsored when there is additional storage + // so the server calculates tax, but we'll adjust the calculation to only tax the storage. + const hasPaidStorage = (this.formGroup.value.additionalStorage || 0) > 0; + const sponsoredForTaxPreview = this.acceptingSponsorship && !hasPaidStorage; - const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPurchase( - { - ...getPlanFromLegacyEnum(), - passwordManager: { - seats: passwordManagerSeats, - additionalStorage: this.formGroup.value.additionalStorage, - sponsored: false, - }, - secretsManager: this.formGroup.value.secretsManager.enabled - ? { - seats: this.secretsManagerForm.value.userSeats, - additionalServiceAccounts: this.secretsManagerForm.value.additionalServiceAccounts, - standalone: false, - } - : undefined, - }, - billingAddress, - ); + if (this.acceptingSponsorship && hasPaidStorage) { + // For sponsored plans with paid storage, calculate tax only on storage + // by comparing tax on base+storage vs tax on base only + //TODO: Move this logic to PreviewOrganizationTaxCommand - https://bitwarden.atlassian.net/browse/PM-27585 + const [baseTaxAmounts, fullTaxAmounts] = await Promise.all([ + this.taxClient.previewTaxForOrganizationSubscriptionPurchase( + this.buildTaxPreviewRequest(0, false), + billingAddress, + ), + this.taxClient.previewTaxForOrganizationSubscriptionPurchase( + this.buildTaxPreviewRequest(this.formGroup.value.additionalStorage, false), + billingAddress, + ), + ]); - this.estimatedTax = taxAmounts.tax; - this.total = taxAmounts.total; + // Tax on storage = Tax on (base + storage) - Tax on (base only) + this.estimatedTax = fullTaxAmounts.tax - baseTaxAmounts.tax; + } else { + const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPurchase( + this.buildTaxPreviewRequest(this.formGroup.value.additionalStorage, sponsoredForTaxPreview), + billingAddress, + ); + + this.estimatedTax = taxAmounts.tax; + } + + const subtotal = + this.passwordManagerSubtotal + + (this.planOffersSecretsManager && this.secretsManagerForm.value.enabled + ? this.secretsManagerSubtotal + : 0); + this.total = subtotal + this.estimatedTax; } private async updateOrganization() { 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 b06dc80a070..b3c071a8b88 100644 --- a/apps/web/src/app/billing/services/pricing-summary.service.ts +++ b/apps/web/src/app/billing/services/pricing-summary.service.ts @@ -50,6 +50,9 @@ export class PricingSummaryService { if (plan.PasswordManager?.hasPremiumAccessOption) { passwordManagerSubtotal += plan.PasswordManager.premiumAccessOptionPrice; } + if (plan.PasswordManager?.hasAdditionalStorageOption) { + passwordManagerSubtotal += additionalStorageTotal; + } const secretsManagerSubtotal = plan.SecretsManager ? (plan.SecretsManager.basePrice || 0) + @@ -66,8 +69,8 @@ export class PricingSummaryService { const storageGb = sub?.maxStorageGb ? sub?.maxStorageGb - 1 : 0; const total = organization?.useSecretsManager - ? passwordManagerSubtotal + additionalStorageTotal + secretsManagerSubtotal + estimatedTax - : passwordManagerSubtotal + additionalStorageTotal + estimatedTax; + ? passwordManagerSubtotal + secretsManagerSubtotal + estimatedTax + : passwordManagerSubtotal + estimatedTax; return { selectedPlanInterval: selectedInterval === PlanInterval.Annually ? "year" : "month",