diff --git a/apps/web/src/app/billing/individual/user-subscription.component.html b/apps/web/src/app/billing/individual/user-subscription.component.html index dca77dbf950..4c600b421c2 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.html +++ b/apps/web/src/app/billing/individual/user-subscription.component.html @@ -90,17 +90,6 @@ {{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }} - - - - {{ "customBillingStart" | i18n }} - - {{ "billingHistory" | i18n }} - - {{ "customBillingEnd" | i18n }} - - - diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 5b4b7cf49ef..abb5fd06428 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -205,10 +205,6 @@ export class UserSubscriptionComponent implements OnInit { return this.sub != null ? this.sub.upcomingInvoice : null; } - get discount() { - return this.sub != null ? this.sub.discount : null; - } - get storagePercentage() { return this.sub != null && this.sub.maxStorageGb ? +(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index bfb94a389ed..62d17a7e00d 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -69,7 +69,7 @@ - + {{ i.productName }} - {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ @@ -79,17 +79,6 @@ {{ i.quantity * i.amount | currency : "$" }} /{{ i.interval | i18n }} - - - - {{ "customBillingStart" | i18n }} - - {{ "billingHistory" | i18n }} - - {{ "customBillingEnd" | i18n }} - - - @@ -150,6 +139,7 @@ diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index eb7180f7c83..d9e81e5f6b0 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -134,12 +134,24 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy return this.sub != null ? this.sub.subscription : null; } + get subscriptionLineItems() { + return this.lineItems.map((lineItem: BillingSubscriptionItemResponse) => ({ + name: lineItem.name, + amount: this.discountPrice(lineItem.amount), + quantity: lineItem.quantity, + interval: lineItem.interval, + sponsoredSubscriptionItem: lineItem.sponsoredSubscriptionItem, + addonSubscriptionItem: lineItem.addonSubscriptionItem, + productName: lineItem.productName, + })); + } + get nextInvoice() { return this.sub != null ? this.sub.upcomingInvoice : null; } - get discount() { - return this.sub != null ? this.sub.discount : null; + get customerDiscount() { + return this.sub != null ? this.sub.customerDiscount : null; } get isExpired() { @@ -168,11 +180,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } get storageGbPrice() { - return this.sub.plan.PasswordManager.additionalStoragePricePerGb; + return this.discountPrice(this.sub.plan.PasswordManager.additionalStoragePricePerGb); } get seatPrice() { - return this.sub.plan.PasswordManager.seatPrice; + return this.discountPrice(this.sub.plan.PasswordManager.seatPrice); } get seats() { @@ -183,12 +195,14 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy return { seatCount: this.sub.smSeats, maxAutoscaleSeats: this.sub.maxAutoscaleSmSeats, - seatPrice: this.sub.plan.SecretsManager.seatPrice, + seatPrice: this.discountPrice(this.sub.plan.SecretsManager.seatPrice), maxAutoscaleServiceAccounts: this.sub.maxAutoscaleSmServiceAccounts, additionalServiceAccounts: this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount, interval: this.sub.plan.isAnnual ? "year" : "month", - additionalServiceAccountPrice: this.sub.plan.SecretsManager.additionalPricePerServiceAccount, + additionalServiceAccountPrice: this.discountPrice( + this.sub.plan.SecretsManager.additionalPricePerServiceAccount + ), baseServiceAccountCount: this.sub.plan.SecretsManager.baseServiceAccount, }; } @@ -382,6 +396,15 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } }; + discountPrice = (price: number) => { + const discount = + !!this.customerDiscount && this.customerDiscount.active + ? price * (this.customerDiscount.percentOff / 100) + : 0; + + return price - discount; + }; + get showChangePlanButton() { return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan; } diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.html b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.html index 84c74ee4282..2f3a2c08e30 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.html +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.html @@ -4,5 +4,6 @@ [selectedPlan]="plan" [upgradeOrganization]="false" [showSubmitButton]="true" + [customerDiscount]="customerDiscount" > diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts index 2942a67560f..ebde4ab2536 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts @@ -6,6 +6,7 @@ import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin- import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request"; +import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -19,6 +20,7 @@ import { secretsManagerSubscribeFormFactory } from "../shared"; export class SecretsManagerSubscribeStandaloneComponent { @Input() plan: PlanResponse; @Input() organization: Organization; + @Input() customerDiscount: BillingCustomerDiscount; @Output() onSubscribe = new EventEmitter(); formGroup = secretsManagerSubscribeFormFactory(this.formBuilder); diff --git a/apps/web/src/app/billing/shared/sm-subscribe.component.ts b/apps/web/src/app/billing/shared/sm-subscribe.component.ts index 1aa6c1bccb5..85836bf17f2 100644 --- a/apps/web/src/app/billing/shared/sm-subscribe.component.ts +++ b/apps/web/src/app/billing/shared/sm-subscribe.component.ts @@ -3,6 +3,7 @@ import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { Subject, startWith, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; +import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { ProductType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -36,6 +37,7 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy { @Input() upgradeOrganization: boolean; @Input() showSubmitButton = false; @Input() selectedPlan: PlanResponse; + @Input() customerDiscount: BillingCustomerDiscount; logo = SecretsManagerLogo; productTypes = ProductType; @@ -63,6 +65,15 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy { this.destroy$.complete(); } + discountPrice = (price: number) => { + const discount = + !!this.customerDiscount && this.customerDiscount.active + ? price * (this.customerDiscount.percentOff / 100) + : 0; + + return price - discount; + }; + get product() { return this.selectedPlan.product; } @@ -84,8 +95,8 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy { get monthlyCostPerServiceAccount() { return this.selectedPlan.isAnnual - ? this.selectedPlan.SecretsManager.additionalPricePerServiceAccount / 12 - : this.selectedPlan.SecretsManager.additionalPricePerServiceAccount; + ? this.discountPrice(this.selectedPlan.SecretsManager.additionalPricePerServiceAccount) / 12 + : this.discountPrice(this.selectedPlan.SecretsManager.additionalPricePerServiceAccount); } get maxUsers() { @@ -98,7 +109,7 @@ export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy { get monthlyCostPerUser() { return this.selectedPlan.isAnnual - ? this.selectedPlan.SecretsManager.seatPrice / 12 - : this.selectedPlan.SecretsManager.seatPrice; + ? this.discountPrice(this.selectedPlan.SecretsManager.seatPrice) / 12 + : this.discountPrice(this.selectedPlan.SecretsManager.seatPrice); } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 5b4b2ac8625..3f7b1d1b05e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7273,12 +7273,6 @@ "alreadyHaveAccount": { "message": "Already have an account?" }, - "customBillingStart": { - "message": "Custom billing is not reflected. Visit the " - }, - "customBillingEnd": { - "message": " page for latest invoicing." - }, "typePasskey": { "message": "Passkey" }, diff --git a/libs/common/src/billing/models/response/organization-subscription.response.ts b/libs/common/src/billing/models/response/organization-subscription.response.ts index a86adbabe7c..404540c6165 100644 --- a/libs/common/src/billing/models/response/organization-subscription.response.ts +++ b/libs/common/src/billing/models/response/organization-subscription.response.ts @@ -1,9 +1,9 @@ import { OrganizationResponse } from "../../../admin-console/models/response/organization.response"; +import { BaseResponse } from "../../../models/response/base.response"; import { BillingSubscriptionResponse, BillingSubscriptionUpcomingInvoiceResponse, - BillingCustomerDiscount, } from "./subscription.response"; export class OrganizationSubscriptionResponse extends OrganizationResponse { @@ -11,7 +11,7 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse { storageGb: number; subscription: BillingSubscriptionResponse; upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse; - discount: BillingCustomerDiscount; + customerDiscount: BillingCustomerDiscount; expiration: string; expirationWithoutGracePeriod: string; secretsManagerBeta: boolean; @@ -27,10 +27,30 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse { upcomingInvoice == null ? null : new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice); - const discount = this.getResponseProperty("Discount"); - this.discount = discount == null ? null : new BillingCustomerDiscount(discount); + const customerDiscount = this.getResponseProperty("CustomerDiscount"); + this.customerDiscount = + customerDiscount == null ? null : new BillingCustomerDiscount(customerDiscount); this.expiration = this.getResponseProperty("Expiration"); this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod"); this.secretsManagerBeta = this.getResponseProperty("SecretsManagerBeta"); } } + +export class BillingCustomerDiscount extends BaseResponse { + id: string; + active: boolean; + percentOff?: number; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.active = this.getResponseProperty("Active"); + this.percentOff = this.getResponseProperty("PercentOff"); + } + + discountPrice = (price: number) => { + const discount = this !== null && this.active ? price * (this.percentOff / 100) : 0; + + return price - discount; + }; +} diff --git a/libs/common/src/billing/models/response/subscription.response.ts b/libs/common/src/billing/models/response/subscription.response.ts index fd84cf493d4..d6bff87adb0 100644 --- a/libs/common/src/billing/models/response/subscription.response.ts +++ b/libs/common/src/billing/models/response/subscription.response.ts @@ -6,7 +6,6 @@ export class SubscriptionResponse extends BaseResponse { maxStorageGb: number; subscription: BillingSubscriptionResponse; upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse; - discount: BillingCustomerDiscount; license: any; expiration: string; usingInAppPurchase: boolean; @@ -21,13 +20,11 @@ export class SubscriptionResponse extends BaseResponse { this.usingInAppPurchase = this.getResponseProperty("UsingInAppPurchase"); const subscription = this.getResponseProperty("Subscription"); const upcomingInvoice = this.getResponseProperty("UpcomingInvoice"); - const discount = this.getResponseProperty("Discount"); this.subscription = subscription == null ? null : new BillingSubscriptionResponse(subscription); this.upcomingInvoice = upcomingInvoice == null ? null : new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice); - this.discount = discount == null ? null : new BillingCustomerDiscount(discount); } } @@ -89,14 +86,3 @@ export class BillingSubscriptionUpcomingInvoiceResponse extends BaseResponse { this.amount = this.getResponseProperty("Amount"); } } - -export class BillingCustomerDiscount extends BaseResponse { - id: string; - active: boolean; - - constructor(response: any) { - super(response); - this.id = this.getResponseProperty("Id"); - this.active = this.getResponseProperty("Active"); - } -}