From 663d0ec92b029ccffa33434fc50124f262d48a46 Mon Sep 17 00:00:00 2001 From: Cy Okeke Date: Fri, 20 Dec 2024 16:46:32 +0100 Subject: [PATCH 1/4] changes to restart a unpaid or cancel subscription --- .../change-plan-dialog.component.html | 30 ++-- .../change-plan-dialog.component.ts | 138 ++++++++++++++++-- .../organization-billing.module.ts | 2 - .../billing/services/trial-flow.service.ts | 82 ++++++++--- .../components/vault-filter.component.ts | 37 +---- apps/web/src/locales/en/messages.json | 15 ++ .../billing-api.service.abstraction.ts | 5 + .../organization-billing.service.ts | 5 + .../organization-billing-metadata.response.ts | 2 + .../billing/services/billing-api.service.ts | 14 ++ .../services/organization-billing.service.ts | 19 +++ 11 files changed, 269 insertions(+), 80 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 93751f0ef72..3ff20310f23 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -1,7 +1,7 @@
- {{ "upgradeFreeOrganization" | i18n: currentPlanName }} + {{ dialogHeaderName }}

{{ "upgradePlans" | i18n }}

@@ -330,9 +330,15 @@
- +

{{ "paymentMethod" | i18n }}

-

+

{{ deprecateStripeSourcesAPI @@ -344,23 +350,11 @@

{{ "paymentMethod" | i18n }}

- - + +
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 572b7979515..5ea90a7b1c8 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 @@ -23,7 +23,14 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { + BillingApiServiceAbstraction, + BillingInformation, + OrganizationInformation, + PaymentInformation, + PlanInformation, + OrganizationBillingServiceAbstraction as OrganizationBillingService, +} from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType, PlanInterval, @@ -44,10 +51,10 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { BillingSharedModule } from "../shared/billing-shared.module"; import { PaymentV2Component } from "../shared/payment/payment-v2.component"; import { PaymentComponent } from "../shared/payment/payment.component"; import { TaxInfoComponent } from "../shared/tax-info.component"; - type ChangePlanDialogParams = { organizationId: string; subscription: OrganizationSubscriptionResponse; @@ -85,6 +92,8 @@ interface OnSuccessArgs { @Component({ templateUrl: "./change-plan-dialog.component.html", + standalone: true, + imports: [BillingSharedModule], }) export class ChangePlanDialogComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @@ -159,7 +168,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { organization: Organization; sub: OrganizationSubscriptionResponse; billing: BillingResponse; - currentPlanName: string; + dialogHeaderName: string; showPayment: boolean = false; totalOpened: boolean = false; currentPlan: PlanResponse; @@ -170,6 +179,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { paymentSource?: PaymentSourceResponse; deprecateStripeSourcesAPI: boolean; + isSubscriptionCanceled: boolean = false; private destroy$ = new Subject(); @@ -189,6 +199,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, + private organizationBillingService: OrganizationBillingService, ) {} async ngOnInit(): Promise { @@ -197,10 +208,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ); if (this.dialogParams.organizationId) { - this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType); this.sub = this.dialogParams.subscription ?? (await this.organizationApiService.getSubscription(this.dialogParams.organizationId)); + this.dialogHeaderName = this.resolveHeaderName(this.sub); this.organizationId = this.dialogParams.organizationId; this.currentPlan = this.sub?.plan; this.selectedPlan = this.sub?.plan; @@ -269,9 +280,42 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.loading = false; } + resolveHeaderName(subscription: OrganizationSubscriptionResponse): string { + if (subscription.subscription != null) { + this.isSubscriptionCanceled = subscription.subscription.cancelled; + if (subscription.subscription.cancelled) { + return this.i18nService.t("restartSubscription"); + } + } + + return this.i18nService.t( + "upgradeFreeOrganization", + this.resolvePlanName(this.dialogParams.productTierType), + ); + } + + shouldShowPaymentOptions(): boolean { + return this.canShowPaymentDetails() && !this.deprecateStripeSourcesAPI; + } + + shouldShowDeprecatedPaymentOptions(): boolean { + return this.canShowPaymentDetails() && this.deprecateStripeSourcesAPI; + } + + canShowPaymentDetails(): boolean { + return ( + this.upgradeRequiresPaymentMethod || + this.showPayment || + this.isPaymentSourceEmpty() || + this.isSubscriptionCanceled + ); + } + setInitialPlanSelection() { - this.focusedIndex = this.selectableProducts.length - 1; - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + if (!this.isSubscriptionCanceled) { + this.focusedIndex = this.selectableProducts.length - 1; + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + } } getPlanByType(productTier: ProductTierType) { @@ -376,6 +420,19 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ]; } case PlanCardState.Disabled: { + if (this.isSubscriptionCanceled) { + return [ + "tw-cursor-not-allowed", + "tw-bg-secondary-100", + "tw-font-normal", + "tw-bg-blur", + "tw-text-muted", + "tw-block", + "tw-rounded", + "tw-w-80", + ]; + } + return [ "tw-cursor-not-allowed", "tw-bg-secondary-100", @@ -397,7 +454,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return; } - if (plan === this.currentPlan) { + if (plan === this.currentPlan && !this.isSubscriptionCanceled) { return; } this.selectedPlan = plan; @@ -428,6 +485,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get selectableProducts() { + if (this.isSubscriptionCanceled) { + // Return only the current plan if the subscription is canceled + return [this.currentPlan]; + } + if (this.acceptingSponsorship) { const familyPlan = this.passwordManagerPlans.find( (plan) => plan.type === PlanType.FamiliesAnnually, @@ -675,11 +737,19 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { const doSubmit = async (): Promise => { let orgId: string = null; - orgId = await this.updateOrganization(); + if (this.isSubscriptionCanceled) { + await this.restartSubscription(); + orgId = this.organizationId; + } else { + orgId = await this.updateOrganization(); + } + this.toastService.showToast({ variant: "success", title: null, - message: this.i18nService.t("organizationUpgraded"), + message: this.isSubscriptionCanceled + ? this.i18nService.t("restartOrganizationSubscription") + : this.i18nService.t("organizationUpgraded"), }); await this.apiService.refreshIdentityToken(); @@ -709,6 +779,44 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.dialogRef.close(); }; + private async restartSubscription() { + const org = await this.organizationApiService.get(this.organizationId); + const organization: OrganizationInformation = { + name: org.name, + billingEmail: org.billingEmail, + }; + + const plan: PlanInformation = { + type: this.selectedPlan.type, + passwordManagerSeats: org.seats, + }; + + if (org.useSecretsManager) { + plan.subscribeToSecretsManager = true; + plan.secretsManagerSeats = org.smSeats; + } + + let paymentMethod: [string, PaymentMethodType]; + + if (this.deprecateStripeSourcesAPI) { + const { type, token } = await this.paymentV2Component.tokenize(); + paymentMethod = [token, type]; + } else { + paymentMethod = await this.paymentComponent.createPaymentToken(); + } + + const payment: PaymentInformation = { + paymentMethod, + billing: this.getBillingInformationFromTaxInfoComponent(), + }; + + await this.organizationBillingService.restartSubscription(this.organization.id, { + organization, + plan, + payment, + }); + } + private async updateOrganization() { const request = new OrganizationUpgradeRequest(); if (this.selectedPlan.productTier !== ProductTierType.Families) { @@ -774,6 +882,18 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return this.organizationId; } + private getBillingInformationFromTaxInfoComponent(): BillingInformation { + return { + country: this.taxComponent.country, + postalCode: this.taxComponent.postalCode, + taxId: this.taxComponent.taxId, + addressLine1: this.taxComponent.line1, + addressLine2: this.taxComponent.line2, + city: this.taxComponent.city, + state: this.taxComponent.state, + }; + } + private billingSubLabelText(): string { const selectedPlan = this.selectedPlan; const price = diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index b25cda662f2..48ac613711d 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -8,7 +8,6 @@ import { BillingSharedModule } from "../shared"; import { AdjustSubscription } from "./adjust-subscription.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; import { BillingSyncKeyComponent } from "./billing-sync-key.component"; -import { ChangePlanDialogComponent } from "./change-plan-dialog.component"; import { ChangePlanComponent } from "./change-plan.component"; import { DownloadLicenceDialogComponent } from "./download-license.component"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; @@ -44,7 +43,6 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; SecretsManagerSubscribeStandaloneComponent, SubscriptionHiddenComponent, SubscriptionStatusComponent, - ChangePlanDialogComponent, OrganizationPaymentMethodComponent, ], }) diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts index 558851ad64c..aa6804926cd 100644 --- a/apps/web/src/app/billing/services/trial-flow.service.ts +++ b/apps/web/src/app/billing/services/trial-flow.service.ts @@ -2,7 +2,9 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; +import { lastValueFrom } from "rxjs"; +import { OrganizationApiServiceAbstraction } 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/billing-api.service.abstraction"; import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; @@ -13,6 +15,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { DialogService } from "@bitwarden/components"; import { FreeTrial } from "../../core/types/free-trial"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "../organizations/change-plan-dialog.component"; @Injectable({ providedIn: "root" }) export class TrialFlowService { @@ -21,17 +27,18 @@ export class TrialFlowService { protected dialogService: DialogService, private router: Router, protected billingApiService: BillingApiServiceAbstraction, + private organizationApiService: OrganizationApiServiceAbstraction, ) {} checkForOrgsWithUpcomingPaymentIssues( organization: Organization, organizationSubscription: OrganizationSubscriptionResponse, paymentSource: BillingSourceResponse | PaymentSourceResponse, ): FreeTrial { - const trialEndDate = organizationSubscription?.subscription?.trialEndDate; + const trialEndDate = organizationSubscription.subscription?.trialEndDate; const displayBanner = !paymentSource && - organization?.isOwner && - organizationSubscription?.subscription?.status === "trialing"; + organization.isOwner && + organizationSubscription?.subscription.status === "trialing"; const trialRemainingDays = trialEndDate ? this.calculateTrialRemainingDays(trialEndDate) : 0; const freeTrialMessage = this.getFreeTrialMessage(trialRemainingDays); @@ -64,20 +71,28 @@ export class TrialFlowService { async handleUnpaidSubscriptionDialog( org: Organization, - organizationBillingMetadata: OrganizationBillingMetadataResponse, + billingMetadata: OrganizationBillingMetadataResponse, ): Promise { - if (organizationBillingMetadata.isSubscriptionUnpaid) { - const confirmed = await this.promptForPaymentNavigation(org); + if (billingMetadata.isSubscriptionUnpaid || billingMetadata.isSubscriptionCanceled) { + const confirmed = await this.promptForPaymentNavigation( + org, + billingMetadata.isSubscriptionCanceled, + billingMetadata.isSubscriptionUnpaid, + ); if (confirmed) { - await this.navigateToPaymentMethod(org?.id); + await this.navigateToPaymentMethod(org.id); } } } - private async promptForPaymentNavigation(org: Organization): Promise { - if (!org?.isOwner) { + private async promptForPaymentNavigation( + org: Organization, + isCanceled: boolean, + isUnpaid: boolean, + ): Promise { + if (!org.isOwner && !org.isProviderUser) { await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org?.name), + title: this.i18nService.t("suspendedOrganizationTitle", org.name), content: { key: "suspendedUserOrgMessage" }, type: "danger", acceptButtonText: this.i18nService.t("close"), @@ -85,13 +100,29 @@ export class TrialFlowService { }); return false; } - return await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org?.name), - content: { key: "suspendedOwnerOrgMessage" }, - type: "danger", - acceptButtonText: this.i18nService.t("continue"), - cancelButtonText: this.i18nService.t("close"), - }); + if (org.hasProvider) { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org.name), + content: { key: "suspendedManagedOrgMessage", placeholders: [org.providerName] }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + return false; + } + + if (org.isOwner && isUnpaid) { + return await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org.name), + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("continue"), + cancelButtonText: this.i18nService.t("close"), + }); + } + if (org.isOwner && isCanceled) { + await this.changePlan(org); + } } private async navigateToPaymentMethod(orgId: string) { @@ -99,4 +130,21 @@ export class TrialFlowService { state: { launchPaymentModalAutomatically: true }, }); } + + private async changePlan(org: Organization) { + const subscription = await this.organizationApiService.getSubscription(org.id); + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: org.id, + subscription: subscription, + productTierType: org.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === ChangePlanDialogResultType.Closed) { + return; + } + } } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index efb1754c811..827ab39384a 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -6,7 +6,6 @@ import { firstValueFrom, Subject } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -16,6 +15,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { DialogService } from "@bitwarden/components"; +import { TrialFlowService } from "../../../../billing/services/trial-flow.service"; import { VaultFilterService } from "../services/abstractions/vault-filter.service"; import { VaultFilterList, @@ -90,7 +90,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { return "searchVault"; } - + private trialFlowService = inject(TrialFlowService); constructor( protected vaultFilterService: VaultFilterService, protected policyService: PolicyService, @@ -126,12 +126,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { this.i18nService.t("disabledOrganizationFilterError"), ); const metadata = await this.billingApiService.getOrganizationBillingMetadata(orgNode.node.id); - if (metadata.isSubscriptionUnpaid) { - const confirmed = await this.promptForPaymentNavigation(orgNode.node); - if (confirmed) { - await this.navigateToPaymentMethod(orgNode.node.id); - } - } + await this.trialFlowService.handleUnpaidSubscriptionDialog(orgNode.node, metadata); return; } const filter = this.activeFilter; @@ -144,32 +139,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { await this.vaultFilterService.expandOrgFilter(); }; - private async promptForPaymentNavigation(org: Organization): Promise { - if (!org?.isOwner) { - await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org?.name), - content: { key: "suspendedUserOrgMessage" }, - type: "danger", - acceptButtonText: this.i18nService.t("close"), - cancelButtonText: null, - }); - return false; - } - return await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org?.name), - content: { key: "suspendedOwnerOrgMessage" }, - type: "danger", - acceptButtonText: this.i18nService.t("continue"), - cancelButtonText: this.i18nService.t("close"), - }); - } - - private async navigateToPaymentMethod(orgId: string) { - await this.router.navigate(["organizations", `${orgId}`, "billing", "payment-method"], { - state: { launchPaymentModalAutomatically: true }, - }); - } - applyTypeFilter = async (filterNode: TreeNode): Promise => { const filter = this.activeFilter; filter.resetFilter(); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 5e86e13f6ce..49448b9378f 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3158,6 +3158,9 @@ "organizationUpgraded": { "message": "Organization upgraded" }, + "restartOrganizationSubscription": { + "message": "Organization subscription restarted" + }, "leave": { "message": "Leave" }, @@ -9590,6 +9593,9 @@ } } }, + "restartSubscription": { + "message": "Restart your subscription" + }, "includeSsoAuthenticationMessage": { "message": "SSO Authentication" }, @@ -9876,6 +9882,15 @@ "suspendedUserOrgMessage": { "message": "Contact your organization owner for assistance." }, + "suspendedManagedOrgMessage": { + "message": "Contact $PROVIDER$ for assistance.", + "placeholders": { + "provider": { + "content": "$1", + "example": "Acme c" + } + } + }, "suspendedOwnerOrgMessage": { "message": "To regain access to your organization, add a payment method." }, diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 8b82795fb50..20042541ebe 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -8,6 +8,7 @@ import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/reque import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response"; import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response"; +import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; @@ -74,4 +75,8 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: VerifyBankAccountRequest, ) => Promise; + restartSubscription: ( + organizationId: string, + request: OrganizationCreateRequest, + ) => Promise; } diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index ddcd61573a6..7c4e0a39f8f 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -57,4 +57,9 @@ export abstract class OrganizationBillingServiceAbstraction { ) => Promise; startFree: (subscription: SubscriptionInformation) => Promise; + + restartSubscription: ( + organizationId: string, + subscription: SubscriptionInformation, + ) => Promise; } diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts index d9733aa80f2..758a49b647b 100644 --- a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -6,6 +6,7 @@ export class OrganizationBillingMetadataResponse extends BaseResponse { isOnSecretsManagerStandalone: boolean; isSubscriptionUnpaid: boolean; hasSubscription: boolean; + isSubscriptionCanceled: boolean; constructor(response: any) { super(response); @@ -14,5 +15,6 @@ export class OrganizationBillingMetadataResponse extends BaseResponse { this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone"); this.isSubscriptionUnpaid = this.getResponseProperty("IsSubscriptionUnpaid"); this.hasSubscription = this.getResponseProperty("HasSubscription"); + this.isSubscriptionCanceled = this.getResponseProperty("IsSubscriptionCanceled"); } } diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index cb69f294409..7ce5602f3cc 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -10,6 +10,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ToastService } from "@bitwarden/components"; import { ApiService } from "../../abstractions/api.service"; +import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { BillingApiServiceAbstraction } from "../../billing/abstractions"; import { PaymentMethodType } from "../../billing/enums"; import { ExpandedTaxInfoUpdateRequest } from "../../billing/models/request/expanded-tax-info-update.request"; @@ -214,6 +215,19 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } + async restartSubscription( + organizationId: string, + request: OrganizationCreateRequest, + ): Promise { + return await this.apiService.send( + "POST", + "/organizations/" + organizationId + "/billing/restart-subscription", + request, + true, + false, + ); + } + private async execute(request: () => Promise): Promise { try { return await request(); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index a0b3782f1ad..80fabe92eb3 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -127,6 +127,25 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs return response; } + async restartSubscription( + organizationId: string, + subscription: SubscriptionInformation, + ): Promise { + const request = new OrganizationCreateRequest(); + + const organizationKeys = await this.makeOrganizationKeys(); + + this.setOrganizationKeys(request, organizationKeys); + + this.setOrganizationInformation(request, subscription.organization); + + this.setPlanInformation(request, subscription.plan); + + this.setPaymentInformation(request, subscription.payment); + + await this.billingApiService.restartSubscription(organizationId, request); + } + private async makeOrganizationKeys(): Promise { const [encryptedKey, key] = await this.keyService.makeOrgKey(); const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key); From 926d7d807da5228115e01f1e663a3775c1395f12 Mon Sep 17 00:00:00 2001 From: Cy Okeke Date: Fri, 20 Dec 2024 18:20:32 +0100 Subject: [PATCH 2/4] Changes to gide the select plan --- .../organizations/change-plan-dialog.component.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 3ff20310f23..29f1dd7360c 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -6,7 +6,11 @@

{{ "upgradePlans" | i18n }}

- {{ "selectAPlan" | i18n }} + {{ "selectAPlan" | i18n }}
- {{ "selectAPlan" | i18n }} + {{ + "selectAPlan" | i18n + }}
Date: Fri, 20 Dec 2024 18:58:05 +0100 Subject: [PATCH 4/4] Remove the 20 percent for cancel org --- .../app/billing/organizations/change-plan-dialog.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index cdf6b6ec9cf..d1734f85338 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -13,6 +13,7 @@