diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 3bd5053994..5a46b49f24 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -31,7 +31,6 @@ *ngIf="canAccessBilling$ | async" > -

{{ data.callout.body }}

-
{{ "billingPlan" | i18n }}
@@ -18,7 +9,7 @@
{{ data.status.label }}
- {{ displayedStatus }} + {{ data.status.value }}
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/subscription-status.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts similarity index 68% rename from bitwarden_license/bit-web/src/app/billing/providers/subscription/subscription-status.component.ts rename to bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts index 91cdef10ac..c3ad875136 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/subscription-status.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts @@ -1,5 +1,5 @@ import { DatePipe } from "@angular/common"; -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { ProviderSubscriptionResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -17,47 +17,29 @@ type ComponentData = { severity: "danger" | "warning"; header: string; body: string; - showReinstatementButton: boolean; }; }; @Component({ - selector: "app-subscription-status", - templateUrl: "subscription-status.component.html", + selector: "app-provider-subscription-status", + templateUrl: "provider-subscription-status.component.html", }) -export class SubscriptionStatusComponent { - @Input({ required: true }) providerSubscriptionResponse: ProviderSubscriptionResponse; - @Output() reinstatementRequested = new EventEmitter(); +export class ProviderSubscriptionStatusComponent { + @Input({ required: true }) subscription: ProviderSubscriptionResponse; constructor( private datePipe: DatePipe, private i18nService: I18nService, ) {} - get displayedStatus(): string { - return this.data.status.value; - } - - get planName() { - return this.providerSubscriptionResponse.plans[0]; - } - get status(): string { if (this.subscription.cancelAt && this.subscription.status === "active") { - this.subscription.status = "pending_cancellation"; + return "pending_cancellation"; } return this.subscription.status; } - get isExpired() { - return this.subscription.status !== "active"; - } - - get subscription() { - return this.providerSubscriptionResponse; - } - get data(): ComponentData { const defaultStatusLabel = this.i18nService.t("status"); @@ -66,21 +48,6 @@ export class SubscriptionStatusComponent { const cancellationDateLabel = this.i18nService.t("cancellationDate"); switch (this.status) { - case "free": { - return {}; - } - case "trialing": { - return { - status: { - label: defaultStatusLabel, - value: this.i18nService.t("trial"), - }, - date: { - label: nextChargeDateLabel, - value: this.subscription.currentPeriodEndDate.toDateString(), - }, - }; - } case "active": { return { status: { @@ -89,26 +56,26 @@ export class SubscriptionStatusComponent { }, date: { label: nextChargeDateLabel, - value: this.subscription.currentPeriodEndDate.toDateString(), + value: this.subscription.currentPeriodEndDate, }, }; } case "past_due": { const pastDueText = this.i18nService.t("pastDue"); const suspensionDate = this.datePipe.transform( - this.subscription.suspensionDate, + this.subscription.suspension.suspensionDate, "mediumDate", ); const calloutBody = this.subscription.collectionMethod === "charge_automatically" ? this.i18nService.t( "pastDueWarningForChargeAutomatically", - this.subscription.gracePeriod, + this.subscription.suspension.gracePeriod, suspensionDate, ) : this.i18nService.t( "pastDueWarningForSendInvoice", - this.subscription.gracePeriod, + this.subscription.suspension.gracePeriod, suspensionDate, ); return { @@ -118,13 +85,12 @@ export class SubscriptionStatusComponent { }, date: { label: subscriptionExpiredDateLabel, - value: this.subscription.unpaidPeriodEndDate, + value: this.subscription.suspension.unpaidPeriodEndDate, }, callout: { severity: "warning", header: pastDueText, body: calloutBody, - showReinstatementButton: false, }, }; } @@ -136,13 +102,12 @@ export class SubscriptionStatusComponent { }, date: { label: subscriptionExpiredDateLabel, - value: this.subscription.currentPeriodEndDate.toDateString(), + value: this.subscription.suspension.unpaidPeriodEndDate, }, callout: { severity: "danger", header: this.i18nService.t("unpaidInvoice"), body: this.i18nService.t("toReactivateYourSubscription"), - showReinstatementButton: false, }, }; } @@ -163,7 +128,6 @@ export class SubscriptionStatusComponent { body: this.i18nService.t("subscriptionPendingCanceled") + this.i18nService.t("providerReinstate"), - showReinstatementButton: false, }, }; } @@ -177,18 +141,15 @@ export class SubscriptionStatusComponent { }, date: { label: cancellationDateLabel, - value: this.subscription.currentPeriodEndDate.toDateString(), + value: this.subscription.currentPeriodEndDate, }, callout: { severity: "danger", header: canceledText, body: this.i18nService.t("subscriptionCanceled"), - showReinstatementButton: false, }, }; } } } - - requestReinstatement = () => this.reinstatementRequested.emit(); } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 47f8aa375c..d447495387 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -4,58 +4,85 @@ {{ "loading" | i18n }} - - -
- {{ "details" | i18n }}  {{ "providerDiscount" | i18n: subscription.discountPercentage }} - - - - - - - {{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{ - i.cadence.toLowerCase() - }}) {{ "×" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }} - @ - {{ - getFormattedCost( - i.cost, - i.seatMinimum, - i.purchasedSeats, - subscription.discountPercentage - ) | currency: "$" - }} - - - {{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} /{{ - "month" | i18n - }} -
- - {{ i.cost | currency: "$" }} /{{ "month" | i18n }} - -
- - + + + +
+ {{ "details" | i18n }}  {{ "providerDiscount" | i18n: subscription.discountPercentage }} + + + + + + + {{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{ + i.cadence.toLowerCase() + }}) {{ "×" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }} + @ + {{ + getFormattedCost( + i.cost, + i.seatMinimum, + i.purchasedSeats, + subscription.discountPercentage + ) | currency: "$" + }} + + + {{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} /{{ + "month" | i18n + }} +
+ + {{ i.cost | currency: "$" }} /{{ "month" | i18n }} + +
+ + - - - - Total: {{ totalCost | currency: "$" }} /{{ - "month" | i18n - }} - - -
-
-
-
+ + + + Total: {{ totalCost | currency: "$" }} /{{ + "month" | i18n + }} + + +
+
+
+
+
+ + +

+ {{ "accountCredit" | i18n }} +

+

{{ subscription.accountCredit | currency: "$" }}

+

{{ "creditAppliedDesc" | i18n }}

+ +
+ + +

{{ "taxInformation" | i18n }}

+

{{ "taxInformationDesc" | i18n }}

+ +
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index ca405747bf..d582ad071f 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -2,19 +2,25 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { Subject, concatMap, takeUntil } from "rxjs"; +import { openAddAccountCreditDialog } from "@bitwarden/angular/billing/components"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; +import { TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { ProviderPlanResponse, ProviderSubscriptionResponse, } from "@bitwarden/common/billing/models/response/provider-subscription-response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-provider-subscription", templateUrl: "./provider-subscription.component.html", }) export class ProviderSubscriptionComponent { - subscription: ProviderSubscriptionResponse; providerId: string; + subscription: ProviderSubscriptionResponse; + firstLoaded = false; loading: boolean; private destroy$ = new Subject(); @@ -23,7 +29,10 @@ export class ProviderSubscriptionComponent { constructor( private billingApiService: BillingApiServiceAbstraction, + private dialogService: DialogService, + private i18nService: I18nService, private route: ActivatedRoute, + private toastService: ToastService, ) {} async ngOnInit() { @@ -54,6 +63,23 @@ export class ProviderSubscriptionComponent { this.loading = false; } + addAccountCredit = () => + openAddAccountCreditDialog(this.dialogService, { + data: { + providerId: this.providerId, + }, + }); + + updateTaxInformation = async (taxInformation: TaxInformation) => { + const request = ExpandedTaxInfoUpdateRequest.From(taxInformation); + await this.billingApiService.updateProviderTaxInformation(this.providerId, request); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedTaxInformation"), + }); + }; + getFormattedCost( cost: number, seatMinimum: number, @@ -61,8 +87,7 @@ export class ProviderSubscriptionComponent { discountPercentage: number, ): number { const costPerSeat = cost / (seatMinimum + purchasedSeats); - const discountedCost = costPerSeat - (costPerSeat * discountPercentage) / 100; - return discountedCost; + return costPerSeat - (costPerSeat * discountPercentage) / 100; } getFormattedPlanName(planName: string): string { @@ -83,4 +108,6 @@ export class ProviderSubscriptionComponent { this.destroy$.next(); this.destroy$.complete(); } + + protected readonly TaxInformation = TaxInformation; } diff --git a/libs/common/src/billing/models/response/provider-subscription-response.ts b/libs/common/src/billing/models/response/provider-subscription-response.ts index 4986914cc0..2dc9d4281d 100644 --- a/libs/common/src/billing/models/response/provider-subscription-response.ts +++ b/libs/common/src/billing/models/response/provider-subscription-response.ts @@ -1,29 +1,38 @@ +import { SubscriptionSuspensionResponse } from "@bitwarden/common/billing/models/response/subscription-suspension.response"; +import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; + import { BaseResponse } from "../../../models/response/base.response"; export class ProviderSubscriptionResponse extends BaseResponse { status: string; - currentPeriodEndDate: Date; + currentPeriodEndDate: string; discountPercentage?: number | null; - plans: ProviderPlanResponse[] = []; collectionMethod: string; - unpaidPeriodEndDate?: string; - gracePeriod?: number | null; - suspensionDate?: string; + plans: ProviderPlanResponse[] = []; + accountCredit: number; + taxInformation?: TaxInfoResponse; cancelAt?: string; + suspension?: SubscriptionSuspensionResponse; constructor(response: any) { super(response); this.status = this.getResponseProperty("status"); - this.currentPeriodEndDate = new Date(this.getResponseProperty("currentPeriodEndDate")); + this.currentPeriodEndDate = this.getResponseProperty("currentPeriodEndDate"); this.discountPercentage = this.getResponseProperty("discountPercentage"); this.collectionMethod = this.getResponseProperty("collectionMethod"); - this.unpaidPeriodEndDate = this.getResponseProperty("unpaidPeriodEndDate"); - this.gracePeriod = this.getResponseProperty("gracePeriod"); - this.suspensionDate = this.getResponseProperty("suspensionDate"); - this.cancelAt = this.getResponseProperty("cancelAt"); const plans = this.getResponseProperty("plans"); if (plans != null) { - this.plans = plans.map((i: any) => new ProviderPlanResponse(i)); + this.plans = plans.map((plan: any) => new ProviderPlanResponse(plan)); + } + this.accountCredit = this.getResponseProperty("accountCredit"); + const taxInformation = this.getResponseProperty("taxInformation"); + if (taxInformation != null) { + this.taxInformation = new TaxInfoResponse(taxInformation); + } + this.cancelAt = this.getResponseProperty("cancelAt"); + const suspension = this.getResponseProperty("suspension"); + if (suspension != null) { + this.suspension = new SubscriptionSuspensionResponse(suspension); } } } diff --git a/libs/common/src/billing/models/response/subscription-suspension.response.ts b/libs/common/src/billing/models/response/subscription-suspension.response.ts new file mode 100644 index 0000000000..418e1c443c --- /dev/null +++ b/libs/common/src/billing/models/response/subscription-suspension.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class SubscriptionSuspensionResponse extends BaseResponse { + suspensionDate: string; + unpaidPeriodEndDate: string; + gracePeriod: number; + + constructor(response: any) { + super(response); + + this.suspensionDate = this.getResponseProperty("suspensionDate"); + this.unpaidPeriodEndDate = this.getResponseProperty("unpaidPeriodEndDate"); + this.gracePeriod = this.getResponseProperty("gracePeriod"); + } +}