From 9180d75244fce4fd73697196128dfc878f4f72ef Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Tue, 7 Apr 2026 07:57:24 -0400 Subject: [PATCH] [PM-34685][Defect] Subscription status for organizations not updating with feature flag enabled (#20018) --- ...ganization-subscription-cloud.component.ts | 9 +- .../subscription-status.component.spec.ts | 114 ++++++++++++++++++ .../subscription-status.component.ts | 17 ++- 3 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/app/billing/organizations/subscription-status.component.spec.ts 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 323a190fe1c..2428fe9e514 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 @@ -331,9 +331,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } get subscriptionMarkedForCancel() { - return ( - this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate - ); + if (!this.subscription || this.subscription.cancelled) { + return false; + } + + const { status, cancelAtEndDate, cancelledDate } = this.subscription; + return cancelAtEndDate || (status === "active" && !!cancelledDate); } cancelSubscription = async () => { diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.spec.ts b/apps/web/src/app/billing/organizations/subscription-status.component.spec.ts new file mode 100644 index 00000000000..1f837ce5059 --- /dev/null +++ b/apps/web/src/app/billing/organizations/subscription-status.component.spec.ts @@ -0,0 +1,114 @@ +import { DatePipe } from "@angular/common"; +import { TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; + +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { BillingSubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { SubscriptionStatusComponent } from "./subscription-status.component"; + +function makeSubscription( + overrides: Partial = {}, +): BillingSubscriptionResponse { + return { + status: "active", + cancelAtEndDate: false, + cancelledDate: null, + cancelled: false, + items: [], + ...overrides, + } as BillingSubscriptionResponse; +} + +function makeOrgResponse( + subscription: BillingSubscriptionResponse | null, +): OrganizationSubscriptionResponse { + return { subscription } as OrganizationSubscriptionResponse; +} + +describe("SubscriptionStatusComponent", () => { + let component: SubscriptionStatusComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + SubscriptionStatusComponent, + { provide: I18nService, useValue: mock() }, + { provide: DatePipe, useValue: mock() }, + ], + }); + + component = TestBed.inject(SubscriptionStatusComponent); + }); + + describe("status getter", () => { + it("returns 'free' when there is no subscription", () => { + component.organizationSubscriptionResponse = makeOrgResponse(null); + expect(component.status).toBe("free"); + }); + + it("returns the subscription status when no cancellation is pending", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ status: "active" }), + ); + expect(component.status).toBe("active"); + }); + + it("returns 'pending_cancellation' when cancelAtEndDate is true", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ cancelAtEndDate: true }), + ); + expect(component.status).toBe("pending_cancellation"); + }); + + it("returns 'pending_cancellation' when cancelledDate is set (phase-based cancellation)", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ cancelledDate: "2026-05-01" }), + ); + expect(component.status).toBe("pending_cancellation"); + }); + + it("returns 'canceled' even when cancelAtEndDate is true", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ status: "canceled", cancelAtEndDate: true }), + ); + expect(component.status).toBe("canceled"); + }); + + it("returns 'canceled' even when cancelledDate is set", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ status: "canceled", cancelledDate: "2026-05-01" }), + ); + expect(component.status).toBe("canceled"); + }); + + it("returns 'trialing' when status is trialing", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ status: "trialing" }), + ); + expect(component.status).toBe("trialing"); + }); + + it("returns 'past_due' when status is past_due", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ status: "past_due" }), + ); + expect(component.status).toBe("past_due"); + }); + + it("returns 'unpaid' when status is unpaid", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ status: "unpaid" }), + ); + expect(component.status).toBe("unpaid"); + }); + + it("returns 'incomplete_expired' when status is incomplete_expired", () => { + component.organizationSubscriptionResponse = makeOrgResponse( + makeSubscription({ status: "incomplete_expired" }), + ); + expect(component.status).toBe("incomplete_expired"); + }); + }); +}); diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.ts b/apps/web/src/app/billing/organizations/subscription-status.component.ts index 54a309a441b..e22d8d0f0c8 100644 --- a/apps/web/src/app/billing/organizations/subscription-status.component.ts +++ b/apps/web/src/app/billing/organizations/subscription-status.component.ts @@ -53,11 +53,18 @@ export class SubscriptionStatusComponent { } get status(): string { - return this.subscription - ? this.subscription.status != "canceled" && this.subscription.cancelAtEndDate - ? "pending_cancellation" - : this.subscription.status - : "free"; + if (!this.subscription) { + return "free"; + } + + const { status, cancelAtEndDate, cancelledDate } = this.subscription; + const pendingCancellation = cancelAtEndDate || (status === "active" && !!cancelledDate); + + if (status !== "canceled" && pendingCancellation) { + return "pending_cancellation"; + } + + return status; } get subscription() {