diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index be9a85ffe4..d557073895 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -55,10 +55,7 @@ ; organizationIsUnmanaged$: Observable; - protected isBreadcrumbEventLogsEnabled$: Observable; protected showSponsoredFamiliesDropdown$: Observable; - protected canShowPoliciesTab$: Observable; protected paymentDetailsPageData$: Observable<{ route: string; @@ -94,9 +92,6 @@ export class OrganizationLayoutComponent implements OnInit { ) {} async ngOnInit() { - this.isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.PM12276_BreadcrumbEventLogs, - ); document.body.classList.remove("layout_frontend"); this.organization$ = this.route.params.pipe( @@ -141,18 +136,6 @@ export class OrganizationLayoutComponent implements OnInit { this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations)); - this.canShowPoliciesTab$ = this.organization$.pipe( - switchMap((organization) => - this.organizationBillingService - .isBreadcrumbingPoliciesEnabled$(organization) - .pipe( - map( - (isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies, - ), - ), - ), - ); - this.paymentDetailsPageData$ = this.configService .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout) .pipe( diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 02be3476ad..344e8afef5 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -1,4 +1,4 @@ -@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async); +@let usePlaceHolderEvents = !organization?.useEvents; (); readonly ProductTierType = ProductTierType; - protected isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.PM12276_BreadcrumbEventLogs, - ); - constructor( private apiService: ApiService, private route: ActivatedRoute, diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index 8eb204b65a..843d1d18d5 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -1,20 +1,7 @@ - - @let organization = organization$ | async; - @if (isBreadcrumbingEnabled$ | async) { - - } - + + @let organization = organization$ | async; @if (loading) { ; + organization$: Observable; private orgPolicies: PolicyResponse[]; protected policiesEnabledMap: Map = new Map(); - protected isBreadcrumbingEnabled$: Observable; constructor( private route: ActivatedRoute, - private accountService: AccountService, private organizationService: OrganizationService, + private accountService: AccountService, private policyApiService: PolicyApiServiceAbstraction, private policyListService: PolicyListService, - private organizationBillingService: OrganizationBillingServiceAbstraction, private dialogService: DialogService, protected configService: ConfigService, ) {} @@ -62,9 +53,11 @@ export class PoliciesComponent implements OnInit { const userId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + this.organization$ = this.organizationService .organizations$(userId) .pipe(getOrganizationById(this.organizationId)); + this.policies = this.policyListService.getPolicies(); await this.load(); @@ -100,11 +93,7 @@ export class PoliciesComponent implements OnInit { this.orgPolicies.forEach((op) => { this.policiesEnabledMap.set(op.type, op.enabled); }); - this.isBreadcrumbingEnabled$ = this.organization$.pipe( - switchMap((organization) => - this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), - ), - ); + this.loading = false; } @@ -117,34 +106,8 @@ export class PoliciesComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); - switch (result) { - case PolicyEditDialogResult.Saved: - await this.load(); - break; - case PolicyEditDialogResult.UpgradePlan: - await this.changePlan(await firstValueFrom(this.organization$)); - break; + if (result === PolicyEditDialogResult.Saved) { + await this.load(); } } - - protected readonly CollectionDialogTabType = CollectionDialogTabType; - protected readonly All = All; - - protected async changePlan(organization: Organization) { - const reference = openChangePlanDialog(this.dialogService, { - data: { - organizationId: organization.id, - subscription: null, - productTierType: organization.productTierType, - }, - }); - - const result = await lastValueFrom(reference.closed); - - if (result === ChangePlanDialogResultType.Closed) { - return; - } - - await this.load(); - } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html index 90cfb52e5a..6573801ad2 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html @@ -1,17 +1,5 @@
- - -
- - - diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index 2984db67d3..f78ab20702 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -9,20 +9,12 @@ import { ViewContainerRef, } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { map, Observable, switchMap } from "rxjs"; +import { Observable, map } from "rxjs"; -import { - getOrganizationById, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, @@ -45,7 +37,6 @@ export type PolicyEditDialogData = { // eslint-disable-next-line @bitwarden/platform/no-enums export enum PolicyEditDialogResult { Saved = "saved", - UpgradePlan = "upgrade-plan", } @Component({ selector: "app-policy-edit", @@ -66,22 +57,15 @@ export class PolicyEditComponent implements AfterViewInit { formGroup = this.formBuilder.group({ enabled: [this.enabled], }); - protected organization$: Observable; - protected isBreadcrumbingEnabled$: Observable; - constructor( @Inject(DIALOG_DATA) protected data: PolicyEditDialogData, - private accountService: AccountService, private policyApiService: PolicyApiServiceAbstraction, - private organizationService: OrganizationService, private i18nService: I18nService, private cdr: ChangeDetectorRef, private formBuilder: FormBuilder, private dialogRef: DialogRef, private toastService: ToastService, - private organizationBillingService: OrganizationBillingServiceAbstraction, ) {} - get policy(): BasePolicy { return this.data.policy; } @@ -115,16 +99,6 @@ export class PolicyEditComponent implements AfterViewInit { throw e; } } - this.organization$ = this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.organizationService.organizations$(userId)), - getOrganizationById(this.data.organizationId), - ); - this.isBreadcrumbingEnabled$ = this.organization$.pipe( - switchMap((organization) => - this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization), - ), - ); } submit = async () => { @@ -154,8 +128,4 @@ export class PolicyEditComponent implements AfterViewInit { static open = (dialogService: DialogService, config: DialogConfig) => { return dialogService.open(PolicyEditComponent, config); }; - - protected upgradePlan(): void { - this.dialogRef.close(PolicyEditDialogResult.UpgradePlan); - } } diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts index 4c825b26bb..9e33986b87 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts @@ -1,13 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { inject, NgModule } from "@angular/core"; -import { CanMatchFn, RouterModule, Routes } from "@angular/router"; -import { map } from "rxjs"; +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; import { canAccessReportingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; // eslint-disable-next-line no-restricted-imports import { ExposedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/exposed-passwords-report.component"; @@ -26,11 +23,6 @@ import { EventsComponent } from "../manage/events.component"; import { ReportsHomeComponent } from "./reports-home.component"; -const breadcrumbEventLogsPermission$: CanMatchFn = () => - inject(ConfigService) - .getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs) - .pipe(map((breadcrumbEventLogs) => breadcrumbEventLogs === true)); - const routes: Routes = [ { path: "", @@ -92,24 +84,10 @@ const routes: Routes = [ }, ], }, - // Event routing is temporarily duplicated { path: "events", component: EventsComponent, - canMatch: [breadcrumbEventLogsPermission$], // if this matches, the flag is ON - canActivate: [ - organizationPermissionsGuard( - (org) => (org.canAccessEventLogs && org.useEvents) || org.isOwner, - ), - ], - data: { - titleId: "eventLogs", - }, - }, - { - path: "events", - component: EventsComponent, - canActivate: [organizationPermissionsGuard((org) => org.canAccessEventLogs)], + canActivate: [organizationPermissionsGuard((org) => org.canAccessEventLogs || org.isOwner)], data: { titleId: "eventLogs", }, diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts index cfec0be531..a644086628 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts @@ -1,10 +1,8 @@ -import { NgModule, inject } from "@angular/core"; +import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { map } from "rxjs"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard"; import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard"; @@ -43,14 +41,7 @@ const routes: Routes = [ { path: "policies", component: PoliciesComponent, - canActivate: [ - organizationPermissionsGuard((o: Organization) => { - const organizationBillingService = inject(OrganizationBillingServiceAbstraction); - return organizationBillingService - .isBreadcrumbingPoliciesEnabled$(o) - .pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled)); - }), - ], + canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)], data: { titleId: "policies", }, diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index 33dd4dcaa2..84dd00949c 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -34,7 +34,6 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { getById } from "@bitwarden/common/platform/misc"; @@ -188,22 +187,16 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { await this.loadOrg(this.params.organizationId); } - const isBreadcrumbEventLogsEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs), + this.organizationSelected.setAsyncValidators( + freeOrgCollectionLimitValidator( + this.organizations$, + this.collectionService + .encryptedCollections$(userId) + .pipe(map((collections) => collections ?? [])), + this.i18nService, + ), ); - - if (isBreadcrumbEventLogsEnabled) { - this.organizationSelected.setAsyncValidators( - freeOrgCollectionLimitValidator( - this.organizations$, - this.collectionService - .encryptedCollections$(userId) - .pipe(map((collections) => collections ?? [])), - this.i18nService, - ), - ); - this.formGroup.updateValueAndValidity(); - } + this.formGroup.updateValueAndValidity(); this.organizationSelected.valueChanges .pipe( diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 3dbf7d6c71..216564e625 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -13,7 +13,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -218,28 +217,22 @@ export class VaultHeaderComponent { } async addCollection(): Promise { - const isBreadcrumbEventLogsEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs), + const organization = this.organizations?.find( + (org) => org.productTierType === ProductTierType.Free, ); - if (isBreadcrumbEventLogsEnabled) { - const organization = this.organizations?.find( - (org) => org.productTierType === ProductTierType.Free, - ); - - if (this.organizations?.length == 1 && !!organization) { - const collections = await firstValueFrom( - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => - this.collectionAdminService.collectionAdminViews$(organization.id, userId), - ), + if (this.organizations?.length == 1 && !!organization) { + const collections = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.collectionAdminService.collectionAdminViews$(organization.id, userId), ), - ); - if (collections.length === organization.maxCollections) { - await this.showFreeOrgUpgradeDialog(organization); - return; - } + ), + ); + if (collections.length === organization.maxCollections) { + await this.showFreeOrgUpgradeDialog(organization); + return; } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 4920acc1ba..5066cabd05 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1332,7 +1332,6 @@ const safeProviders: SafeProvider[] = [ I18nServiceAbstraction, OrganizationApiServiceAbstraction, SyncService, - ConfigService, ], }), safeProvider({ diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 113b55465a..9089c165a3 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,7 +1,3 @@ -import { Observable } from "rxjs"; - -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; - import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; @@ -63,10 +59,4 @@ export abstract class OrganizationBillingServiceAbstraction { organizationId: string, subscription: SubscriptionInformation, ): Promise; - - /** - * Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria. - * @param organization - */ - abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable; } diff --git a/libs/common/src/billing/services/organization-billing.service.spec.ts b/libs/common/src/billing/services/organization-billing.service.spec.ts index 43457f810d..1e666e75bb 100644 --- a/libs/common/src/billing/services/organization-billing.service.spec.ts +++ b/libs/common/src/billing/services/organization-billing.service.spec.ts @@ -1,22 +1,26 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { + BillingApiServiceAbstraction, + SubscriptionInformation, +} from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SyncService } from "@bitwarden/common/platform/sync"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KeyService } from "@bitwarden/key-management"; -describe("BillingAccountProfileStateService", () => { +import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; +import { EncString } from "../../key-management/crypto/models/enc-string"; +import { OrgKey } from "../../types/key"; +import { PaymentMethodResponse } from "../models/response/payment-method.response"; + +describe("OrganizationBillingService", () => { let apiService: jest.Mocked; let billingApiService: jest.Mocked; let keyService: jest.Mocked; @@ -24,7 +28,6 @@ describe("BillingAccountProfileStateService", () => { let i18nService: jest.Mocked; let organizationApiService: jest.Mocked; let syncService: jest.Mocked; - let configService: jest.Mocked; let sut: OrganizationBillingService; @@ -36,7 +39,6 @@ describe("BillingAccountProfileStateService", () => { i18nService = mock(); organizationApiService = mock(); syncService = mock(); - configService = mock(); sut = new OrganizationBillingService( apiService, @@ -46,7 +48,6 @@ describe("BillingAccountProfileStateService", () => { i18nService, organizationApiService, syncService, - configService, ); }); @@ -54,98 +55,246 @@ describe("BillingAccountProfileStateService", () => { return jest.resetAllMocks(); }); - describe("isBreadcrumbingPoliciesEnabled", () => { - it("returns false when feature flag is disabled", async () => { - configService.getFeatureFlag$.mockReturnValue(of(false)); - const org = { - isProviderUser: false, - canEditSubscription: true, - productTierType: ProductTierType.Teams, - } as Organization; + describe("getPaymentSource()", () => { + it("given a valid organization id, then it returns a payment source", async () => { + //Arrange + const orgId = "organization-test"; + const paymentMethodResponse = { + paymentSource: { type: PaymentMethodType.Card }, + } as PaymentMethodResponse; + billingApiService.getOrganizationPaymentMethod.mockResolvedValue(paymentMethodResponse); - const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); - expect(actual).toBe(false); - expect(configService.getFeatureFlag$).toHaveBeenCalledWith( - FeatureFlag.PM12276_BreadcrumbEventLogs, + //Act + const returnedPaymentSource = await sut.getPaymentSource(orgId); + + //Assert + expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); + expect(returnedPaymentSource).toEqual(paymentMethodResponse.paymentSource); + }); + + it("given an invalid organizationId, it should return undefined", async () => { + //Arrange + const orgId = "invalid-id"; + billingApiService.getOrganizationPaymentMethod.mockResolvedValue(null); + + //Act + const returnedPaymentSource = await sut.getPaymentSource(orgId); + + //Assert + expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); + expect(returnedPaymentSource).toBeUndefined(); + }); + + it("given an API error occurs, then it throws the error", async () => { + // Arrange + const orgId = "error-org"; + billingApiService.getOrganizationPaymentMethod.mockRejectedValue(new Error("API Error")); + + // Act & Assert + await expect(sut.getPaymentSource(orgId)).rejects.toThrow("API Error"); + expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1); + }); + }); + + describe("purchaseSubscription()", () => { + it("given valid subscription information, then it returns successful response", async () => { + //Arrange + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.EnterpriseAnnually2023 }, + payment: { + paymentMethod: ["card-token", PaymentMethodType.Card], + billing: { postalCode: "12345" }, + }, + } as SubscriptionInformation; + + const organizationResponse = { + name: subscriptionInformation.organization.name, + billingEmail: subscriptionInformation.organization.billingEmail, + planType: subscriptionInformation.plan.type, + } as OrganizationResponse; + + organizationApiService.create.mockResolvedValue(organizationResponse); + keyService.makeOrgKey.mockResolvedValue([new EncString("encrypyted-key"), {} as OrgKey]); + keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypyted-key")]); + encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted")); + + //Act + const response = await sut.purchaseSubscription(subscriptionInformation); + + //Assert + expect(organizationApiService.create).toHaveBeenCalledTimes(1); + expect(response).toEqual(organizationResponse); + }); + + it("given organization creation fails, then it throws an error", async () => { + // Arrange + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.EnterpriseAnnually2023 }, + payment: { + paymentMethod: ["card-token", PaymentMethodType.Card], + billing: { postalCode: "12345" }, + }, + } as SubscriptionInformation; + + organizationApiService.create.mockRejectedValue(new Error("Failed to create organization")); + keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]); + keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); + encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted")); + + // Act & Assert + await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow( + "Failed to create organization", ); }); - it("returns false when organization belongs to a provider", async () => { - configService.getFeatureFlag$.mockReturnValue(of(true)); - const org = { - isProviderUser: true, - canEditSubscription: true, - productTierType: ProductTierType.Teams, - } as Organization; + it("given key generation fails, then it throws an error", async () => { + // Arrange + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.EnterpriseAnnually2023 }, + payment: { + paymentMethod: ["card-token", PaymentMethodType.Card], + billing: { postalCode: "12345" }, + }, + } as SubscriptionInformation; - const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); - expect(actual).toBe(false); - }); + keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed")); - it("returns false when cannot edit subscription", async () => { - configService.getFeatureFlag$.mockReturnValue(of(true)); - const org = { - isProviderUser: false, - canEditSubscription: false, - productTierType: ProductTierType.Teams, - } as Organization; - - const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); - expect(actual).toBe(false); - }); - - it.each([ - ["Teams", ProductTierType.Teams], - ["TeamsStarter", ProductTierType.TeamsStarter], - ])("returns true when all conditions are met with %s tier", async (_, productTierType) => { - configService.getFeatureFlag$.mockReturnValue(of(true)); - const org = { - isProviderUser: false, - canEditSubscription: true, - productTierType: productTierType, - } as Organization; - - const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); - expect(actual).toBe(true); - expect(configService.getFeatureFlag$).toHaveBeenCalledWith( - FeatureFlag.PM12276_BreadcrumbEventLogs, + // Act & Assert + await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow( + "Key generation failed", ); }); - it("returns false when product tier is not supported", async () => { - configService.getFeatureFlag$.mockReturnValue(of(true)); - const org = { - isProviderUser: false, - canEditSubscription: true, - productTierType: ProductTierType.Enterprise, - } as Organization; + it("given an invalid plan type, then it throws an error", async () => { + // Arrange + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: -1 as unknown as PlanType }, + payment: { + paymentMethod: ["card-token", PaymentMethodType.Card], + billing: { postalCode: "12345" }, + }, + } as SubscriptionInformation; - const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); - expect(actual).toBe(false); + // Act & Assert + await expect(sut.purchaseSubscription(subscriptionInformation)).rejects.toThrow(); + }); + }); + + describe("purchaseSubscriptionNoPaymentMethod()", () => { + it("given valid subscription information, then it returns successful response", async () => { + //Arrange + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.EnterpriseAnnually2023 }, + } as SubscriptionInformation; + + const organizationResponse = { + name: subscriptionInformation.organization.name, + plan: { type: subscriptionInformation.plan.type }, + planType: subscriptionInformation.plan.type, + } as OrganizationResponse; + + organizationApiService.createWithoutPayment.mockResolvedValue(organizationResponse); + keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]); + keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); + encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted")); + + //Act + const response = await sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation); + + //Assert + expect(organizationApiService.createWithoutPayment).toHaveBeenCalledTimes(1); + expect(response).toEqual(organizationResponse); }); - it("handles all conditions false correctly", async () => { - configService.getFeatureFlag$.mockReturnValue(of(false)); - const org = { - isProviderUser: true, - canEditSubscription: false, - productTierType: ProductTierType.Free, - } as Organization; + it("given organization creation fails without payment method, then it throws an error", async () => { + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.EnterpriseAnnually2023 }, + } as SubscriptionInformation; - const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); - expect(actual).toBe(false); + organizationApiService.createWithoutPayment.mockRejectedValue(new Error("Creation failed")); + keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]); + keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); + encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypted")); + + await expect( + sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation), + ).rejects.toThrow("Creation failed"); }); - it("verifies feature flag is only called once", async () => { - configService.getFeatureFlag$.mockReturnValue(of(false)); - const org = { - isProviderUser: false, - canEditSubscription: true, - productTierType: ProductTierType.Teams, - } as Organization; + it("given key generation fails, then it throws an error", async () => { + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.EnterpriseAnnually2023 }, + } as SubscriptionInformation; - await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org)); - expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1); + keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed")); + keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); + + await expect( + sut.purchaseSubscriptionNoPaymentMethod(subscriptionInformation), + ).rejects.toThrow("Key generation failed"); + }); + }); + + describe("startFree()", () => { + it("given valid free plan information, then it creates a free organization", async () => { + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.Free }, + } as SubscriptionInformation; + + const organizationResponse = { + name: subscriptionInformation.organization.name, + billingEmail: subscriptionInformation.organization.billingEmail, + planType: subscriptionInformation.plan.type, + } as OrganizationResponse; + + organizationApiService.create.mockResolvedValue(organizationResponse); + keyService.makeOrgKey.mockResolvedValue([new EncString("encrypyted-key"), {} as OrgKey]); + keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypyted-key")]); + encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted")); + + //Act + const response = await sut.startFree(subscriptionInformation); + + //Assert + expect(organizationApiService.create).toHaveBeenCalledTimes(1); + expect(response).toEqual(organizationResponse); + }); + + it("given key generation fails, then it throws an error", async () => { + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.Free }, + } as SubscriptionInformation; + + keyService.makeOrgKey.mockRejectedValue(new Error("Key generation failed")); + keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); + + await expect(sut.startFree(subscriptionInformation)).rejects.toThrow("Key generation failed"); + }); + + it("given organization creation fails, then it throws an error", async () => { + // Arrange + const subscriptionInformation = { + organization: { name: "test-business", billingEmail: "test@test.com" }, + plan: { type: PlanType.Free }, + } as SubscriptionInformation; + + organizationApiService.create.mockRejectedValue(new Error("Failed to create organization")); + keyService.makeOrgKey.mockResolvedValue([new EncString("encrypted-key"), {} as OrgKey]); + keyService.makeKeyPair.mockResolvedValue(["key", new EncString("encrypted-key")]); + encryptService.encryptString.mockResolvedValue(new EncString("collection-encrypyted")); + // Act & Assert + await expect(sut.startFree(subscriptionInformation)).rejects.toThrow( + "Failed to create organization", + ); }); }); }); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index aaf2281540..e4fe2f9a6b 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,10 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Observable, of, switchMap } from "rxjs"; - -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KeyService } from "@bitwarden/key-management"; @@ -27,7 +22,7 @@ import { PlanInformation, SubscriptionInformation, } from "../abstractions"; -import { PlanType, ProductTierType } from "../enums"; +import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; import { PaymentSourceResponse } from "../models/response/payment-source.response"; @@ -47,12 +42,11 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private i18nService: I18nService, private organizationApiService: OrganizationApiService, private syncService: SyncService, - private configService: ConfigService, ) {} async getPaymentSource(organizationId: string): Promise { const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId); - return paymentMethod.paymentSource; + return paymentMethod?.paymentSource; } async purchaseSubscription(subscription: SubscriptionInformation): Promise { @@ -229,29 +223,4 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPaymentInformation(request, subscription.payment); await this.billingApiService.restartSubscription(organizationId, request); } - - isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable { - if (organization === null || organization === undefined) { - return of(false); - } - - return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe( - switchMap((featureFlagEnabled) => { - if (!featureFlagEnabled) { - return of(false); - } - - if (organization.isProviderUser || !organization.canEditSubscription) { - return of(false); - } - - const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter]; - const isSupportedProduct = supportedProducts.some( - (product) => product === organization.productTierType, - ); - - return of(isSupportedProduct); - }), - ); - } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 262b31e624..00bd8c4c20 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -23,7 +23,6 @@ export enum FeatureFlag { /* Billing */ TrialPaymentOptional = "PM-8163-trial-payment", - PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", UseOrganizationWarningsService = "use-organization-warnings-service", PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout", @@ -95,7 +94,6 @@ export const DefaultFeatureFlagValue = { /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, - [FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE, [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, [FeatureFlag.UseOrganizationWarningsService]: FALSE, [FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,