1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 06:54:07 +00:00

refactor(billing): Return organization ID from PremiumOrgUpgradeService

This commit is contained in:
Stephon Brown
2026-02-04 14:53:55 -05:00
parent cfe7108a31
commit 24d374a9cd
3 changed files with 81 additions and 14 deletions

View File

@@ -4,6 +4,7 @@ import {
Component,
computed,
DestroyRef,
inject,
input,
OnInit,
output,
@@ -179,14 +180,12 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit
};
});
constructor(
private i18nService: I18nService,
private subscriptionPricingService: SubscriptionPricingServiceAbstraction,
private toastService: ToastService,
private logService: LogService,
private destroyRef: DestroyRef,
private premiumOrgUpgradeService: PremiumOrgUpgradeService,
) {}
private readonly i18nService = inject(I18nService);
private readonly subscriptionPricingService = inject(SubscriptionPricingServiceAbstraction);
private readonly toastService = inject(ToastService);
private readonly logService = inject(LogService);
private readonly destroyRef = inject(DestroyRef);
private readonly premiumOrgUpgradeService = inject(PremiumOrgUpgradeService);
async ngOnInit(): Promise<void> {
// If the selected plan is Personal Premium, no upgrade is needed
@@ -288,7 +287,7 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit
throw new Error("Payment method is required");
}
await this.premiumOrgUpgradeService.upgradeToOrganization(
const organizationId = await this.premiumOrgUpgradeService.upgradeToOrganization(
this.account(),
organizationName,
this.selectedPlan()!,
@@ -297,7 +296,7 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit
return {
status: this.getUpgradeStatus(this.selectedPlanId()),
organizationId: null,
organizationId,
};
}

View File

@@ -1,6 +1,8 @@
import { TestBed } from "@angular/core/testing";
import { of } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BusinessSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@@ -21,6 +23,7 @@ describe("PremiumOrgUpgradeService", () => {
let previewInvoiceClient: jest.Mocked<PreviewInvoiceClient>;
let syncService: jest.Mocked<SyncService>;
let keyService: jest.Mocked<KeyService>;
let organizationService: jest.Mocked<OrganizationService>;
const mockAccount = { id: "user-id", email: "test@bitwarden.com" } as Account;
const mockPlanDetails: PremiumOrgUpgradePlanDetails = {
@@ -61,6 +64,17 @@ describe("PremiumOrgUpgradeService", () => {
.fn()
.mockResolvedValue([{ encryptedString: "encrypted-string" }, "decrypted-key"]),
} as any;
organizationService = {
organizations$: jest.fn().mockReturnValue(
of([
{
id: "new-org-id",
name: "Test Organization",
isOwner: true,
} as Organization,
]),
),
} as any;
TestBed.configureTestingModule({
providers: [
@@ -70,6 +84,7 @@ describe("PremiumOrgUpgradeService", () => {
{ provide: SyncService, useValue: syncService },
{ provide: AccountService, useValue: { activeAccount$: of(mockAccount) } },
{ provide: KeyService, useValue: keyService },
{ provide: OrganizationService, useValue: organizationService },
],
});
@@ -77,8 +92,8 @@ describe("PremiumOrgUpgradeService", () => {
});
describe("upgradeToOrganization", () => {
it("should successfully upgrade premium account to organization", async () => {
await service.upgradeToOrganization(
it("should successfully upgrade premium account to organization and return organization ID", async () => {
const result = await service.upgradeToOrganization(
mockAccount,
"Test Organization",
mockPlanDetails,
@@ -94,6 +109,8 @@ describe("PremiumOrgUpgradeService", () => {
);
expect(keyService.makeOrgKey).toHaveBeenCalledWith("user-id");
expect(syncService.fullSync).toHaveBeenCalledWith(true);
expect(organizationService.organizations$).toHaveBeenCalledWith("user-id");
expect(result).toBe("new-org-id");
});
it("should throw an error if organization name is missing", async () => {
@@ -151,7 +168,10 @@ describe("PremiumOrgUpgradeService", () => {
});
it("should throw an error if encrypted string is undefined", async () => {
keyService.makeOrgKey.mockResolvedValue([{ encryptedString: undefined }, "decrypted-key"]);
keyService.makeOrgKey.mockResolvedValue([
{ encryptedString: null } as any,
"decrypted-key" as any,
]);
await expect(
service.upgradeToOrganization(
mockAccount,
@@ -187,6 +207,40 @@ describe("PremiumOrgUpgradeService", () => {
),
).rejects.toThrow("Sync failed");
});
it("should throw an error if organization is not found after sync", async () => {
organizationService.organizations$.mockReturnValue(
of([
{
id: "different-org-id",
name: "Different Organization",
isOwner: true,
} as Organization,
]),
);
await expect(
service.upgradeToOrganization(
mockAccount,
"Test Organization",
mockPlanDetails,
mockBillingAddress,
),
).rejects.toThrow("Failed to find newly created organization");
});
it("should throw an error if no organizations are returned", async () => {
organizationService.organizations$.mockReturnValue(of([]));
await expect(
service.upgradeToOrganization(
mockAccount,
"Test Organization",
mockPlanDetails,
mockBillingAddress,
),
).rejects.toThrow("Failed to find newly created organization");
});
});
describe("previewProratedInvoice", () => {

View File

@@ -1,5 +1,7 @@
import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import {
@@ -46,6 +48,7 @@ export class PremiumOrgUpgradeService {
private previewInvoiceClient: PreviewInvoiceClient,
private syncService: SyncService,
private keyService: KeyService,
private organizationService: OrganizationService,
) {}
async previewProratedInvoice(
@@ -71,7 +74,7 @@ export class PremiumOrgUpgradeService {
organizationName: string,
planDetails: PremiumOrgUpgradePlanDetails,
billingAddress: BillingAddress,
): Promise<void> {
): Promise<string> {
if (!organizationName) {
throw new Error("Organization name is required for organization upgrade");
}
@@ -96,6 +99,17 @@ export class PremiumOrgUpgradeService {
);
await this.syncService.fullSync(true);
// Get the newly created organization
const organizations = await firstValueFrom(this.organizationService.organizations$(account.id));
const newOrg = organizations?.find((org) => org.name === organizationName && org.isOwner);
if (!newOrg) {
throw new Error("Failed to find newly created organization");
}
return newOrg.id;
}
private ProductTierTypeFromSubscriptionTierId(