mirror of
https://github.com/bitwarden/browser
synced 2026-02-04 02:33:33 +00:00
fix(billing): Update component and service to use new tax service
This commit is contained in:
@@ -4,15 +4,13 @@ import { mock, mockReset } from "jest-mock-extended";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { AccountBillingClient } from "../../../../clients";
|
||||
import { TokenizedPaymentMethod } from "../../../../payment/types";
|
||||
import { AccountBillingClient, TaxAmounts, TaxClient } from "../../../../clients";
|
||||
import { BillingAddress, TokenizedPaymentMethod } from "../../../../payment/types";
|
||||
import { PersonalSubscriptionPricingTierIds } from "../../../../types/subscription-pricing-tier";
|
||||
|
||||
import { UpgradePaymentService, PlanDetails } from "./upgrade-payment.service";
|
||||
@@ -20,7 +18,7 @@ import { UpgradePaymentService, PlanDetails } from "./upgrade-payment.service";
|
||||
describe("UpgradePaymentService", () => {
|
||||
const mockOrganizationBillingService = mock<OrganizationBillingServiceAbstraction>();
|
||||
const mockAccountBillingClient = mock<AccountBillingClient>();
|
||||
const mockTaxService = mock<TaxServiceAbstraction>();
|
||||
const mockTaxClient = mock<TaxClient>();
|
||||
const mockLogService = mock<LogService>();
|
||||
const mockApiService = mock<ApiService>();
|
||||
const mockSyncService = mock<SyncService>();
|
||||
@@ -42,9 +40,14 @@ describe("UpgradePaymentService", () => {
|
||||
type: "card",
|
||||
};
|
||||
|
||||
const mockBillingAddress = {
|
||||
const mockBillingAddress: BillingAddress = {
|
||||
line1: "123 Test St",
|
||||
line2: null,
|
||||
city: "Test City",
|
||||
state: "TS",
|
||||
country: "US",
|
||||
postalCode: "12345",
|
||||
taxId: null,
|
||||
};
|
||||
|
||||
const mockPremiumPlanDetails: PlanDetails = {
|
||||
@@ -89,7 +92,7 @@ describe("UpgradePaymentService", () => {
|
||||
beforeEach(() => {
|
||||
mockReset(mockOrganizationBillingService);
|
||||
mockReset(mockAccountBillingClient);
|
||||
mockReset(mockTaxService);
|
||||
mockReset(mockTaxClient);
|
||||
mockReset(mockLogService);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
@@ -101,7 +104,7 @@ describe("UpgradePaymentService", () => {
|
||||
useValue: mockOrganizationBillingService,
|
||||
},
|
||||
{ provide: AccountBillingClient, useValue: mockAccountBillingClient },
|
||||
{ provide: TaxServiceAbstraction, useValue: mockTaxService },
|
||||
{ provide: TaxClient, useValue: mockTaxClient },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: ApiService, useValue: mockApiService },
|
||||
{ provide: SyncService, useValue: mockSyncService },
|
||||
@@ -114,55 +117,52 @@ describe("UpgradePaymentService", () => {
|
||||
describe("calculateEstimatedTax", () => {
|
||||
it("should calculate tax for premium plan", async () => {
|
||||
// Arrange
|
||||
const mockResponse = mock<PreviewInvoiceResponse>();
|
||||
mockResponse.taxAmount = 2.5;
|
||||
const mockResponse = mock<TaxAmounts>();
|
||||
mockResponse.tax = 2.5;
|
||||
|
||||
mockTaxService.previewIndividualInvoice.mockResolvedValue(mockResponse);
|
||||
mockTaxClient.previewTaxForPremiumSubscriptionPurchase.mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
const result = await sut.calculateEstimatedTax(mockPremiumPlanDetails, mockBillingAddress);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(2.5);
|
||||
expect(mockTaxService.previewIndividualInvoice).toHaveBeenCalledWith({
|
||||
passwordManager: { additionalStorage: 0 },
|
||||
taxInformation: {
|
||||
postalCode: "12345",
|
||||
country: "US",
|
||||
},
|
||||
});
|
||||
expect(mockTaxClient.previewTaxForPremiumSubscriptionPurchase).toHaveBeenCalledWith(
|
||||
0,
|
||||
mockBillingAddress,
|
||||
);
|
||||
});
|
||||
|
||||
it("should calculate tax for families plan", async () => {
|
||||
// Arrange
|
||||
const mockResponse = mock<PreviewInvoiceResponse>();
|
||||
mockResponse.taxAmount = 5.0;
|
||||
const mockResponse = mock<TaxAmounts>();
|
||||
mockResponse.tax = 5.0;
|
||||
|
||||
mockTaxService.previewOrganizationInvoice.mockResolvedValue(mockResponse);
|
||||
mockTaxClient.previewTaxForOrganizationSubscriptionPurchase.mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
const result = await sut.calculateEstimatedTax(mockFamiliesPlanDetails, mockBillingAddress);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(5.0);
|
||||
expect(mockTaxService.previewOrganizationInvoice).toHaveBeenCalledWith({
|
||||
passwordManager: {
|
||||
additionalStorage: 0,
|
||||
plan: PlanType.FamiliesAnnually,
|
||||
seats: 6,
|
||||
expect(mockTaxClient.previewTaxForOrganizationSubscriptionPurchase).toHaveBeenCalledWith(
|
||||
{
|
||||
cadence: "annually",
|
||||
tier: "families",
|
||||
passwordManager: {
|
||||
additionalStorage: 0,
|
||||
seats: 6,
|
||||
sponsored: false,
|
||||
},
|
||||
},
|
||||
taxInformation: {
|
||||
postalCode: "12345",
|
||||
country: "US",
|
||||
taxId: null,
|
||||
},
|
||||
});
|
||||
mockBillingAddress,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw and log error if personal tax calculation fails", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Tax service error");
|
||||
mockTaxService.previewIndividualInvoice.mockRejectedValue(error);
|
||||
mockTaxClient.previewTaxForPremiumSubscriptionPurchase.mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
@@ -174,7 +174,7 @@ describe("UpgradePaymentService", () => {
|
||||
it("should throw and log error if organization tax calculation fails", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Tax service error");
|
||||
mockTaxService.previewOrganizationInvoice.mockRejectedValue(error);
|
||||
mockTaxClient.previewTaxForOrganizationSubscriptionPurchase.mockRejectedValue(error);
|
||||
// Act & Assert
|
||||
await expect(
|
||||
sut.calculateEstimatedTax(mockFamiliesPlanDetails, mockBillingAddress),
|
||||
@@ -309,22 +309,5 @@ describe("UpgradePaymentService", () => {
|
||||
}),
|
||||
).rejects.toThrow("Payment method type or token is missing");
|
||||
});
|
||||
|
||||
describe("tokenizablePaymentMethodToLegacyEnum", () => {
|
||||
it("should convert 'card' to PaymentMethodType.Card", () => {
|
||||
const result = sut.tokenizablePaymentMethodToLegacyEnum("card");
|
||||
expect(result).toBe(PaymentMethodType.Card);
|
||||
});
|
||||
|
||||
it("should convert 'bankAccount' to PaymentMethodType.BankAccount", () => {
|
||||
const result = sut.tokenizablePaymentMethodToLegacyEnum("bankAccount");
|
||||
expect(result).toBe(PaymentMethodType.BankAccount);
|
||||
});
|
||||
|
||||
it("should convert 'payPal' to PaymentMethodType.PayPal", () => {
|
||||
const result = sut.tokenizablePaymentMethodToLegacyEnum("payPal");
|
||||
expect(result).toBe(PaymentMethodType.PayPal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,17 +7,19 @@ import {
|
||||
OrganizationBillingServiceAbstraction,
|
||||
SubscriptionInformation,
|
||||
} from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { AccountBillingClient } from "../../../../clients";
|
||||
import {
|
||||
AccountBillingClient,
|
||||
OrganizationSubscriptionPurchase,
|
||||
TaxAmounts,
|
||||
TaxClient,
|
||||
} from "../../../../clients";
|
||||
import {
|
||||
BillingAddress,
|
||||
TokenizablePaymentMethod,
|
||||
tokenizablePaymentMethodToLegacyEnum,
|
||||
TokenizedPaymentMethod,
|
||||
} from "../../../../payment/types";
|
||||
import {
|
||||
@@ -26,12 +28,6 @@ import {
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "../../../../types/subscription-pricing-tier";
|
||||
|
||||
type TaxInformation = {
|
||||
postalCode: string;
|
||||
country: string;
|
||||
taxId: string | null;
|
||||
};
|
||||
|
||||
export type PlanDetails = {
|
||||
tier: PersonalSubscriptionPricingTierId;
|
||||
details: PersonalSubscriptionPricingTier;
|
||||
@@ -53,7 +49,7 @@ export class UpgradePaymentService {
|
||||
constructor(
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private accountBillingClient: AccountBillingClient,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private taxClient: TaxClient,
|
||||
private logService: LogService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
@@ -64,20 +60,13 @@ export class UpgradePaymentService {
|
||||
*/
|
||||
async calculateEstimatedTax(
|
||||
planDetails: PlanDetails,
|
||||
billingAddress: Pick<BillingAddress, "country" | "postalCode">,
|
||||
billingAddress: BillingAddress,
|
||||
): Promise<number> {
|
||||
try {
|
||||
const taxInformation: TaxInformation = {
|
||||
postalCode: billingAddress.postalCode,
|
||||
country: billingAddress.country,
|
||||
// This is null for now since we only process Families and Premium plans
|
||||
taxId: null,
|
||||
};
|
||||
|
||||
const isOrganizationPlan = planDetails.tier === PersonalSubscriptionPricingTierIds.Families;
|
||||
const isPremiumPlan = planDetails.tier === PersonalSubscriptionPricingTierIds.Premium;
|
||||
|
||||
let taxServiceCall: Promise<{ taxAmount: number }> | null = null;
|
||||
let taxClientCall: Promise<TaxAmounts> | null = null;
|
||||
|
||||
if (isOrganizationPlan) {
|
||||
const seats = this.getPasswordManagerSeats(planDetails);
|
||||
@@ -85,36 +74,28 @@ export class UpgradePaymentService {
|
||||
throw new Error("Seats must be greater than 0 for organization plan");
|
||||
}
|
||||
// Currently, only Families plan is supported for organization plans
|
||||
const request: PreviewOrganizationInvoiceRequest = {
|
||||
passwordManager: {
|
||||
additionalStorage: 0,
|
||||
plan: PlanType.FamiliesAnnually,
|
||||
seats: seats,
|
||||
},
|
||||
taxInformation,
|
||||
const request: OrganizationSubscriptionPurchase = {
|
||||
tier: "families",
|
||||
cadence: "annually",
|
||||
passwordManager: { seats, additionalStorage: 0, sponsored: false },
|
||||
};
|
||||
|
||||
taxServiceCall = this.taxService.previewOrganizationInvoice(request);
|
||||
taxClientCall = this.taxClient.previewTaxForOrganizationSubscriptionPurchase(
|
||||
request,
|
||||
billingAddress,
|
||||
);
|
||||
}
|
||||
|
||||
if (isPremiumPlan) {
|
||||
const request: PreviewIndividualInvoiceRequest = {
|
||||
passwordManager: { additionalStorage: 0 },
|
||||
taxInformation: {
|
||||
postalCode: billingAddress.postalCode,
|
||||
country: billingAddress.country,
|
||||
},
|
||||
};
|
||||
|
||||
taxServiceCall = this.taxService.previewIndividualInvoice(request);
|
||||
taxClientCall = this.taxClient.previewTaxForPremiumSubscriptionPurchase(0, billingAddress);
|
||||
}
|
||||
|
||||
if (taxServiceCall === null) {
|
||||
throw new Error("Tax service call is not defined");
|
||||
if (taxClientCall === null) {
|
||||
throw new Error("Tax client call is not defined");
|
||||
}
|
||||
|
||||
const invoice = await taxServiceCall;
|
||||
return invoice.taxAmount;
|
||||
const preview = await taxClientCall;
|
||||
return preview.tax;
|
||||
} catch (error: unknown) {
|
||||
this.logService.error("Tax calculation failed:", error);
|
||||
throw error;
|
||||
@@ -166,7 +147,7 @@ export class UpgradePaymentService {
|
||||
payment: {
|
||||
paymentMethod: [
|
||||
paymentMethod.token,
|
||||
this.tokenizablePaymentMethodToLegacyEnum(paymentMethod.type),
|
||||
tokenizablePaymentMethodToLegacyEnum(paymentMethod.type),
|
||||
],
|
||||
billing: {
|
||||
country: billingAddress.country,
|
||||
@@ -183,21 +164,6 @@ export class UpgradePaymentService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert tokenizable payment method to legacy enum
|
||||
* note: this will be removed once another PR is merged
|
||||
*/
|
||||
tokenizablePaymentMethodToLegacyEnum(paymentMethod: TokenizablePaymentMethod): PaymentMethodType {
|
||||
switch (paymentMethod) {
|
||||
case "bankAccount":
|
||||
return PaymentMethodType.BankAccount;
|
||||
case "card":
|
||||
return PaymentMethodType.Card;
|
||||
case "payPal":
|
||||
return PaymentMethodType.PayPal;
|
||||
}
|
||||
}
|
||||
|
||||
private getPasswordManagerSeats(planDetails: PlanDetails): number {
|
||||
return "users" in planDetails.details.passwordManager
|
||||
? planDetails.details.passwordManager.users
|
||||
|
||||
@@ -249,8 +249,13 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
|
||||
|
||||
this.upgradePaymentService
|
||||
.calculateEstimatedTax(this.selectedPlan, {
|
||||
line1: null,
|
||||
line2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
country: billingAddress.country,
|
||||
postalCode: billingAddress.postalCode,
|
||||
taxId: null,
|
||||
})
|
||||
.then((tax) => {
|
||||
this.estimatedTax = tax;
|
||||
|
||||
Reference in New Issue
Block a user