[AC-1911] Clients: Create components to manage client organization seat allocation (#8505)

* implementing the clients changes

* resolve pr comments on message.json

* moved the method to billing-api.service

* move the request and response files to billing folder

* remove the adding existing orgs

* resolve the routing issue

* resolving the pr comments

* code owner changes

* fix the assignedseat

* resolve the warning message

* resolve the error on update

* passing the right id

* resolve the unassign value

* removed unused logservice

* Adding the loader on submit button
This commit is contained in:
cyprain-okeke 2024-04-02 17:04:02 +01:00 committed by GitHub
parent b9771c1e42
commit 9956f020e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 575 additions and 9 deletions

1
.github/CODEOWNERS vendored
View File

@ -61,6 +61,7 @@ apps/web/src/app/billing @bitwarden/team-billing-dev
libs/angular/src/billing @bitwarden/team-billing-dev
libs/common/src/billing @bitwarden/team-billing-dev
libs/billing @bitwarden/team-billing-dev
bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev
## Platform team files ##
apps/browser/src/platform @bitwarden/team-platform-dev

View File

@ -4956,6 +4956,9 @@
"addExistingOrganization": {
"message": "Add existing organization"
},
"addNewOrganization": {
"message": "Add new organization"
},
"myProvider": {
"message": "My Provider"
},
@ -7642,5 +7645,38 @@
},
"items": {
"message": "Items"
},
"assignedSeats": {
"message": "Assigned seats"
},
"assigned": {
"message": "Assigned"
},
"used": {
"message": "Used"
},
"remaining": {
"message": "Remaining"
},
"unlinkOrganization": {
"message": "Unlink organization"
},
"manageSeats": {
"message": "MANAGE SEATS"
},
"manageSeatsDescription": {
"message": "Adjustments to seats will be reflected in the next billing cycle."
},
"unassignedSeatsDescription": {
"message": "Unassigned subscription seats"
},
"purchaseSeatDescription": {
"message": "Additional seats purchased"
},
"assignedSeatCannotUpdate": {
"message": "Assigned Seats can not be updated. Please contact your organization owner for assistance."
},
"subscriptionUpdateFailed": {
"message": "Subscription update failed"
}
}

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
@ -13,6 +13,8 @@ import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { PlanType } 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -50,8 +52,14 @@ export class ClientsComponent implements OnInit {
protected actionPromise: Promise<unknown>;
private pagedClientsCount = 0;
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
false,
);
constructor(
private route: ActivatedRoute,
private router: Router,
private providerService: ProviderService,
private apiService: ApiService,
private searchService: SearchService,
@ -64,20 +72,29 @@ export class ClientsComponent implements OnInit {
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction,
private dialogService: DialogService,
private configService: ConfigService,
) {}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
if (enableConsolidatedBilling) {
await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route });
} else {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
});
});
});
}
}
async load() {

View File

@ -4,7 +4,11 @@
<bit-icon [icon]="logo"></bit-icon>
</a>
<bit-nav-item icon="bwi-bank" [text]="'clients' | i18n" route="clients"></bit-nav-item>
<bit-nav-item
icon="bwi-bank"
[text]="'clients' | i18n"
[route]="(enableConsolidatedBilling$ | async) ? 'manage-client-organizations' : 'clients'"
></bit-nav-item>
<bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab">
<bit-nav-item
[text]="'people' | i18n"

View File

@ -37,6 +37,11 @@ export class ProvidersLayoutComponent {
false,
);
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
false,
);
constructor(
private route: ActivatedRoute,
private providerService: ProviderService,

View File

@ -7,6 +7,8 @@ import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/provi
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { ProviderPermissionsGuard } from "./guards/provider-permissions.guard";
@ -64,6 +66,11 @@ const routes: Routes = [
{ path: "", pathMatch: "full", redirectTo: "clients" },
{ path: "clients/create", component: CreateOrganizationComponent },
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
{
path: "manage-client-organizations",
component: ManageClientOrganizationsComponent,
data: { titleId: "manage-client-organizations" },
},
{
path: "manage",
children: [

View File

@ -8,6 +8,9 @@ import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component";
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
import { AddOrganizationComponent } from "./clients/add-organization.component";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
@ -50,6 +53,8 @@ import { SetupComponent } from "./setup/setup.component";
SetupComponent,
SetupProviderComponent,
UserAddEditComponent,
ManageClientOrganizationsComponent,
ManageClientOrganizationSubscriptionComponent,
],
providers: [WebProviderService, ProviderPermissionsGuard],
})

View File

@ -0,0 +1,49 @@
<bit-dialog dialogSize="large" [loading]="loading">
<span bitDialogTitle>
{{ "manageSeats" | i18n }}
<small class="tw-text-muted" *ngIf="clientName">{{ clientName }}</small>
</span>
<div bitDialogContent>
<p>
{{ "manageSeatsDescription" | i18n }}
</p>
<bit-form-field>
<bit-label>
{{ "assignedSeats" | i18n }}
</bit-label>
<input
id="assignedSeats"
type="number"
appAutoFocus
bitInput
required
[(ngModel)]="assignedSeats"
/>
</bit-form-field>
<ng-container *ngIf="remainingOpenSeats > 0">
<p>
<small class="tw-text-muted">{{ unassignedSeats }}</small>
<small class="tw-text-muted">{{ "unassignedSeatsDescription" | i18n }}</small>
</p>
<p>
<small class="tw-text-muted">{{ AdditionalSeatPurchased }}</small>
<small class="tw-text-muted">{{ "purchaseSeatDescription" | i18n }}</small>
</p>
</ng-container>
</div>
<ng-container bitDialogFooter>
<button
type="submit"
bitButton
buttonType="primary"
bitFormButton
(click)="updateSubscription(assignedSeats)"
>
<i class="bwi bwi-refresh bwi-fw" aria-hidden="true"></i>
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@ -0,0 +1,115 @@
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { ProviderSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/provider-subscription-update.request";
import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
type ManageClientOrganizationDialogParams = {
organization: ProviderOrganizationOrganizationDetailsResponse;
};
@Component({
templateUrl: "manage-client-organization-subscription.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ManageClientOrganizationSubscriptionComponent implements OnInit {
loading = true;
providerOrganizationId: string;
providerId: string;
clientName: string;
assignedSeats: number;
unassignedSeats: number;
planName: string;
AdditionalSeatPurchased: number;
remainingOpenSeats: number;
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) protected data: ManageClientOrganizationDialogParams,
private billingApiService: BillingApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
) {
this.providerOrganizationId = data.organization.id;
this.providerId = data.organization.providerId;
this.clientName = data.organization.organizationName;
this.assignedSeats = data.organization.seats;
this.planName = data.organization.plan;
}
async ngOnInit() {
try {
const response = await this.billingApiService.getProviderClientSubscriptions(this.providerId);
this.AdditionalSeatPurchased = this.getPurchasedSeatsByPlan(this.planName, response.plans);
const seatMinimum = this.getProviderSeatMinimumByPlan(this.planName, response.plans);
const assignedByPlan = this.getAssignedByPlan(this.planName, response.plans);
this.remainingOpenSeats = seatMinimum - assignedByPlan;
this.unassignedSeats = Math.abs(this.remainingOpenSeats);
} catch (error) {
this.remainingOpenSeats = 0;
this.AdditionalSeatPurchased = 0;
}
this.loading = false;
}
async updateSubscription(assignedSeats: number) {
this.loading = true;
if (!assignedSeats) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("assignedSeatCannotUpdate"),
);
return;
}
const request = new ProviderSubscriptionUpdateRequest();
request.assignedSeats = assignedSeats;
await this.billingApiService.putProviderClientSubscriptions(
this.providerId,
this.providerOrganizationId,
request,
);
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
this.loading = false;
this.dialogRef.close();
}
getPurchasedSeatsByPlan(planName: string, plans: Plans[]): number {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.purchasedSeats;
} else {
return 0;
}
}
getAssignedByPlan(planName: string, plans: Plans[]): number {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.assignedSeats;
} else {
return 0;
}
}
getProviderSeatMinimumByPlan(planName: string, plans: Plans[]) {
const plan = plans.find((plan) => plan.planName === planName);
if (plan) {
return plan.seatMinimum;
} else {
return 0;
}
}
static open(dialogService: DialogService, data: ManageClientOrganizationDialogParams) {
return dialogService.open(ManageClientOrganizationSubscriptionComponent, { data });
}
}

View File

@ -0,0 +1,90 @@
<app-header>
<bit-search [placeholder]="'search' | i18n" [(ngModel)]="searchText"></bit-search>
<a bitButton routerLink="create" *ngIf="manageOrganizations" buttonType="primary">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "addNewOrganization" | i18n }}
</a>
</app-header>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
*ngIf="!loading && (clients | search: searchText : 'organizationName' : 'id') as searchedClients"
>
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
<ng-container *ngIf="searchedClients.length">
<bit-table
*ngIf="searchedClients?.length >= 1"
[dataSource]="dataSource"
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<ng-container header>
<tr>
<th colspan="2" bitCell bitSortable="organizationName" default>{{ "client" | i18n }}</th>
<th bitCell bitSortable="seats">{{ "assigned" | i18n }}</th>
<th bitCell bitSortable="userCount">{{ "used" | i18n }}</th>
<th bitCell bitSortable="userCount">{{ "remaining" | i18n }}</th>
<th bitCell bitSortable="plan">{{ "billingPlan" | i18n }}</th>
<th></th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let client of rows$ | async">
<td bitCell width="30">
<bit-avatar [text]="client.organizationName" [id]="client.id" size="small"></bit-avatar>
</td>
<td bitCell>
<div class="tw-flex tw-items-center tw-gap-4 tw-break-all">
<a bitLink [routerLink]="['/organizations', client.organizationId]">{{
client.organizationName
}}</a>
</div>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.seats }}</span>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.userCount }}</span>
</td>
<td bitCell class="tw-whitespace-nowrap">
<span>{{ client.seats - client.userCount }}</span>
</td>
<td>
<span>{{ client.plan }}</span>
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="manageSubscription(client)">
<i aria-hidden="true" class="bwi bwi-question-circle"></i>
{{ "manageSubscription" | i18n }}
</button>
<button type="button" bitMenuItem (click)="remove(client)">
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "unlinkOrganization" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
</ng-container>

View File

@ -0,0 +1,160 @@
import { SelectionModel } from "@angular/cdk/collections";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogService, TableDataSource } from "@bitwarden/components";
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-organization-subscription.component";
@Component({
templateUrl: "manage-client-organizations.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ManageClientOrganizationsComponent implements OnInit {
providerId: string;
loading = true;
manageOrganizations = false;
set searchText(search: string) {
this.selection.clear();
this.dataSource.filter = search;
}
clients: ProviderOrganizationOrganizationDetailsResponse[];
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
protected didScroll = false;
protected pageSize = 100;
protected actionPromise: Promise<unknown>;
private pagedClientsCount = 0;
selection = new SelectionModel<string>(true, []);
protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>();
constructor(
private route: ActivatedRoute,
private providerService: ProviderService,
private apiService: ApiService,
private searchService: SearchService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private validationService: ValidationService,
private webProviderService: WebProviderService,
private dialogService: DialogService,
) {}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
});
});
}
async load() {
const response = await this.apiService.getProviderClients(this.providerId);
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
this.dataSource.data = this.clients;
this.manageOrganizations =
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
this.loading = false;
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.resetPaging();
}
return !searching && this.clients && this.clients.length > this.pageSize;
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
async resetPaging() {
this.pagedClients = [];
this.loadMore();
}
loadMore() {
if (!this.clients || this.clients.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedClients.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
pagedSize = this.pagedClientsCount;
}
if (this.clients.length > pagedLength) {
this.pagedClients = this.pagedClients.concat(
this.clients.slice(pagedLength, pagedLength + pagedSize),
);
}
this.pagedClientsCount = this.pagedClients.length;
this.didScroll = this.pagedClients.length > this.pageSize;
}
async manageSubscription(organization: ProviderOrganizationOrganizationDetailsResponse) {
if (organization == null) {
return;
}
const dialogRef = ManageClientOrganizationSubscriptionComponent.open(this.dialogService, {
organization: organization,
});
await firstValueFrom(dialogRef.closed);
await this.load();
}
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
const confirmed = await this.dialogService.openSimpleDialog({
title: organization.organizationName,
content: { key: "detachOrganizationConfirmation" },
type: "warning",
});
if (!confirmed) {
return false;
}
this.actionPromise = this.webProviderService.detachOrganization(
this.providerId,
organization.id,
);
try {
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("detachedOrganization", organization.organizationName),
);
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
}

View File

@ -1,5 +1,7 @@
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request";
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
export abstract class BillingApiServiceAbstraction {
cancelOrganizationSubscription: (
@ -8,4 +10,10 @@ export abstract class BillingApiServiceAbstraction {
) => Promise<void>;
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>;
getProviderClientSubscriptions: (providerId: string) => Promise<ProviderSubscriptionResponse>;
putProviderClientSubscriptions: (
providerId: string,
organizationId: string,
request: ProviderSubscriptionUpdateRequest,
) => Promise<any>;
}

View File

@ -0,0 +1,3 @@
export class ProviderSubscriptionUpdateRequest {
assignedSeats: number;
}

View File

@ -0,0 +1,38 @@
import { BaseResponse } from "../../../models/response/base.response";
export class ProviderSubscriptionResponse extends BaseResponse {
status: string;
currentPeriodEndDate: Date;
discountPercentage?: number | null;
plans: Plans[] = [];
constructor(response: any) {
super(response);
this.status = this.getResponseProperty("status");
this.currentPeriodEndDate = new Date(this.getResponseProperty("currentPeriodEndDate"));
this.discountPercentage = this.getResponseProperty("discountPercentage");
const plans = this.getResponseProperty("plans");
if (plans != null) {
this.plans = plans.map((i: any) => new Plans(i));
}
}
}
export class Plans extends BaseResponse {
planName: string;
seatMinimum: number;
assignedSeats: number;
purchasedSeats: number;
cost: number;
cadence: string;
constructor(response: any) {
super(response);
this.planName = this.getResponseProperty("PlanName");
this.seatMinimum = this.getResponseProperty("SeatMinimum");
this.assignedSeats = this.getResponseProperty("AssignedSeats");
this.purchasedSeats = this.getResponseProperty("PurchasedSeats");
this.cost = this.getResponseProperty("Cost");
this.cadence = this.getResponseProperty("Cadence");
}
}

View File

@ -2,6 +2,8 @@ import { ApiService } from "../../abstractions/api.service";
import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction";
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request";
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
export class BillingApiService implements BillingApiServiceAbstraction {
constructor(private apiService: ApiService) {}
@ -34,4 +36,29 @@ export class BillingApiService implements BillingApiServiceAbstraction {
return new OrganizationBillingStatusResponse(r);
}
async getProviderClientSubscriptions(providerId: string): Promise<ProviderSubscriptionResponse> {
const r = await this.apiService.send(
"GET",
"/providers/" + providerId + "/billing/subscription",
null,
true,
true,
);
return new ProviderSubscriptionResponse(r);
}
async putProviderClientSubscriptions(
providerId: string,
organizationId: string,
request: ProviderSubscriptionUpdateRequest,
): Promise<any> {
return await this.apiService.send(
"PUT",
"/providers/" + providerId + "/organizations/" + organizationId,
request,
true,
false,
);
}
}

View File

@ -7,6 +7,7 @@ export enum FeatureFlag {
KeyRotationImprovements = "key-rotation-improvements",
FlexibleCollectionsMigration = "flexible-collections-migration",
ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners",
EnableConsolidatedBilling = "enable-consolidated-billing",
}
// Replace this with a type safe lookup of the feature flag values in PM-2282