-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PM-15814]Alert owners of reseller-managed orgs to renewal events #12607
Changes from 13 commits
4b074a9
1362b85
feafd88
7b535e1
11b8712
b3af243
277b1df
ff8b810
be74b4f
66ff472
7c666ac
427f547
72925ff
f141f8f
4dfdab9
05e0d3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import { Injectable } from "@angular/core"; | ||
|
||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; | ||
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; | ||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; | ||
|
||
export interface ResellerWarning { | ||
type: "info" | "warning"; | ||
message: string; | ||
} | ||
|
||
@Injectable({ providedIn: "root" }) | ||
export class ResellerWarningService { | ||
private readonly RENEWAL_WARNING_DAYS = 14; | ||
private readonly GRACE_PERIOD_DAYS = 30; | ||
|
||
constructor(private i18nService: I18nService) {} | ||
|
||
getWarning( | ||
organization: Organization, | ||
organizationBillingMetadata: OrganizationBillingMetadataResponse, | ||
): ResellerWarning | null { | ||
if (!organization.hasReseller) { | ||
return null; // If no reseller, return null immediately | ||
} | ||
|
||
// Check for past due warning first (highest priority) | ||
if (this.shouldShowPastDueWarning(organizationBillingMetadata)) { | ||
const gracePeriodEnd = this.getGracePeriodEndDate(organizationBillingMetadata.invoiceDueDate); | ||
if (!gracePeriodEnd) { | ||
return null; | ||
} | ||
return { | ||
type: "warning", | ||
message: this.i18nService.t( | ||
"resellerPastDueWarning", | ||
organization.providerName, | ||
this.formatDate(gracePeriodEnd), | ||
), | ||
} as ResellerWarning; | ||
} | ||
|
||
// Check for open invoice warning | ||
if (this.shouldShowInvoiceWarning(organizationBillingMetadata)) { | ||
const invoiceCreatedDate = organizationBillingMetadata.invoiceCreatedDate; | ||
const invoiceDueDate = organizationBillingMetadata.invoiceDueDate; | ||
if (!invoiceCreatedDate || !invoiceDueDate) { | ||
return null; | ||
} | ||
return { | ||
type: "info", | ||
message: this.i18nService.t( | ||
"resellerOpenInvoiceWarning", | ||
organization.providerName, | ||
this.formatDate(organizationBillingMetadata.invoiceCreatedDate), | ||
this.formatDate(organizationBillingMetadata.invoiceDueDate), | ||
), | ||
} as ResellerWarning; | ||
} | ||
|
||
// Check for renewal warning | ||
if (this.shouldShowRenewalWarning(organizationBillingMetadata)) { | ||
const subPeriodEndDate = organizationBillingMetadata.subPeriodEndDate; | ||
if (!subPeriodEndDate) { | ||
return null; | ||
} | ||
|
||
return { | ||
type: "info", | ||
message: this.i18nService.t( | ||
"resellerRenewalWarning", | ||
organization.providerName, | ||
this.formatDate(organizationBillingMetadata.subPeriodEndDate), | ||
), | ||
} as ResellerWarning; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
private shouldShowRenewalWarning( | ||
organizationBillingMetadata: OrganizationBillingMetadataResponse, | ||
): boolean { | ||
if ( | ||
!organizationBillingMetadata.hasSubscription || | ||
!organizationBillingMetadata.subPeriodEndDate | ||
) { | ||
return false; | ||
} | ||
const renewalDate = new Date(organizationBillingMetadata.subPeriodEndDate); | ||
const daysUntilRenewal = Math.ceil( | ||
(renewalDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24), | ||
); | ||
return daysUntilRenewal <= this.RENEWAL_WARNING_DAYS; | ||
} | ||
|
||
private shouldShowInvoiceWarning( | ||
organizationBillingMetadata: OrganizationBillingMetadataResponse, | ||
): boolean { | ||
if ( | ||
!organizationBillingMetadata.hasOpenInvoice || | ||
!organizationBillingMetadata.invoiceDueDate | ||
) { | ||
return false; | ||
} | ||
const invoiceDueDate = new Date(organizationBillingMetadata.invoiceDueDate); | ||
return invoiceDueDate > new Date(); | ||
} | ||
|
||
private shouldShowPastDueWarning( | ||
organizationBillingMetadata: OrganizationBillingMetadataResponse, | ||
): boolean { | ||
if ( | ||
!organizationBillingMetadata.hasOpenInvoice || | ||
!organizationBillingMetadata.invoiceDueDate | ||
) { | ||
return false; | ||
} | ||
const invoiceDueDate = new Date(organizationBillingMetadata.invoiceDueDate); | ||
return invoiceDueDate <= new Date() && !organizationBillingMetadata.isSubscriptionUnpaid; | ||
} | ||
|
||
private getGracePeriodEndDate(dueDate: Date | null): Date | null { | ||
if (!dueDate) { | ||
return null; | ||
} | ||
const gracePeriodEnd = new Date(dueDate); | ||
gracePeriodEnd.setDate(gracePeriodEnd.getDate() + this.GRACE_PERIOD_DAYS); | ||
return gracePeriodEnd; | ||
} | ||
|
||
private formatDate(date: Date | null): string { | ||
if (!date) { | ||
return "N/A"; | ||
} | ||
return new Date(date).toLocaleDateString("en-US", { | ||
month: "short", | ||
day: "2-digit", | ||
year: "numeric", | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -86,6 +86,10 @@ import { | |||||||||||||||||
|
||||||||||||||||||
import { GroupApiService, GroupView } from "../../admin-console/organizations/core"; | ||||||||||||||||||
import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component"; | ||||||||||||||||||
import { | ||||||||||||||||||
ResellerWarning, | ||||||||||||||||||
ResellerWarningService, | ||||||||||||||||||
} from "../../billing/services/reseller-warning.service"; | ||||||||||||||||||
import { TrialFlowService } from "../../billing/services/trial-flow.service"; | ||||||||||||||||||
import { FreeTrial } from "../../core/types/free-trial"; | ||||||||||||||||||
import { SharedModule } from "../../shared"; | ||||||||||||||||||
|
@@ -187,6 +191,8 @@ export class VaultComponent implements OnInit, OnDestroy { | |||||||||||||||||
private hasSubscription$ = new BehaviorSubject<boolean>(false); | ||||||||||||||||||
protected currentSearchText$: Observable<string>; | ||||||||||||||||||
protected freeTrial$: Observable<FreeTrial>; | ||||||||||||||||||
protected resellerWarning$: Observable<ResellerWarning | null>; | ||||||||||||||||||
protected resellerWarning: ResellerWarning; | ||||||||||||||||||
/** | ||||||||||||||||||
* A list of collections that the user can assign items to and edit those items within. | ||||||||||||||||||
* @protected | ||||||||||||||||||
|
@@ -259,6 +265,7 @@ export class VaultComponent implements OnInit, OnDestroy { | |||||||||||||||||
private trialFlowService: TrialFlowService, | ||||||||||||||||||
protected billingApiService: BillingApiServiceAbstraction, | ||||||||||||||||||
private organizationBillingService: OrganizationBillingServiceAbstraction, | ||||||||||||||||||
private resellerWarningService: ResellerWarningService, | ||||||||||||||||||
) {} | ||||||||||||||||||
|
||||||||||||||||||
async ngOnInit() { | ||||||||||||||||||
|
@@ -592,6 +599,7 @@ export class VaultComponent implements OnInit, OnDestroy { | |||||||||||||||||
.subscribe(); | ||||||||||||||||||
|
||||||||||||||||||
this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe(); | ||||||||||||||||||
//this.resellerWarning1$.pipe(takeUntil(this.destroy$)).subscribe(); | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be removed if it's not needed anymore There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like leftover commented out code, okay to remove? |
||||||||||||||||||
|
||||||||||||||||||
this.freeTrial$ = combineLatest([ | ||||||||||||||||||
organization$, | ||||||||||||||||||
|
@@ -612,6 +620,16 @@ export class VaultComponent implements OnInit, OnDestroy { | |||||||||||||||||
}), | ||||||||||||||||||
); | ||||||||||||||||||
|
||||||||||||||||||
this.resellerWarning$ = combineLatest([organization$]).pipe( | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we only need
Suggested change
|
||||||||||||||||||
filter(([org]) => org.isOwner), | ||||||||||||||||||
switchMap(([org]) => | ||||||||||||||||||
combineLatest([of(org), this.billingApiService.getOrganizationBillingMetadata(org.id)]), | ||||||||||||||||||
), | ||||||||||||||||||
map(([org, organizationMetaData]) => { | ||||||||||||||||||
return this.resellerWarningService.getWarning(org, organizationMetaData); | ||||||||||||||||||
}), | ||||||||||||||||||
); | ||||||||||||||||||
|
||||||||||||||||||
firstSetup$ | ||||||||||||||||||
.pipe( | ||||||||||||||||||
switchMap(() => this.refresh$), | ||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this variable is used anywhere