From 24d374a9cda066364e516748c0a815e09a4fd6ab Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 4 Feb 2026 14:53:55 -0500 Subject: [PATCH] refactor(billing): Return organization ID from PremiumOrgUpgradeService --- .../premium-org-upgrade-payment.component.ts | 19 +++--- .../premium-org-upgrade.service.spec.ts | 60 ++++++++++++++++++- .../services/premium-org-upgrade.service.ts | 16 ++++- 3 files changed, 81 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts index 479693f56f8..bd5964ad81b 100644 --- a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts @@ -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 { // 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, }; } diff --git a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/services/premium-org-upgrade.service.spec.ts b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/services/premium-org-upgrade.service.spec.ts index ff6e337af7b..6719943cf72 100644 --- a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/services/premium-org-upgrade.service.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/services/premium-org-upgrade.service.spec.ts @@ -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; let syncService: jest.Mocked; let keyService: jest.Mocked; + let organizationService: jest.Mocked; 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", () => { diff --git a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/services/premium-org-upgrade.service.ts b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/services/premium-org-upgrade.service.ts index d14a09d4fd1..59c97e0373e 100644 --- a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/services/premium-org-upgrade.service.ts +++ b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/services/premium-org-upgrade.service.ts @@ -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 { + ): Promise { 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(