-
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
Merged
cyprain-okeke
merged 16 commits into
main
from
pm-15814-alert-owners-of-reseller-managed-orgs-to-renewal-events
Dec 31, 2024
Merged
Changes from 14 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
4b074a9
Changes for the reseller alert
cyprain-okeke 1362b85
Merge branch 'main' into pm-15814-alert-owners-of-reseller-managed-orโฆ
cyprain-okeke feafd88
Resolve the null error
cyprain-okeke 7b535e1
Refactor the reseller service
cyprain-okeke 11b8712
Fix the a failing test due to null date
cyprain-okeke b3af243
Fix the No overload matches error
cyprain-okeke 277b1df
Resolve the null error
cyprain-okeke ff8b810
Resolve the null error
cyprain-okeke be74b4f
Resolve the null error
cyprain-okeke 66ff472
Merge branch 'main' into pm-15814-alert-owners-of-reseller-managed-orโฆ
cyprain-okeke 7c666ac
Change the date format
cyprain-okeke 427f547
Merge remote-tracking branch 'refs/remotes/origin/pm-15814-alert-owneโฆ
cyprain-okeke 72925ff
Merge branch 'main' into pm-15814-alert-owners-of-reseller-managed-orโฆ
cyprain-okeke f141f8f
Remove unwanted comment
cyprain-okeke 4dfdab9
Refactor changes
cyprain-okeke 05e0d3b
Add the feature flag
cyprain-okeke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
142 changes: 142 additions & 0 deletions
142
apps/web/src/app/billing/services/reseller-warning.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -86,6 +86,10 @@ | |||||||||||||||||
|
||||||||||||||||||
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 @@ | |||||||||||||||||
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 @@ | |||||||||||||||||
private trialFlowService: TrialFlowService, | ||||||||||||||||||
protected billingApiService: BillingApiServiceAbstraction, | ||||||||||||||||||
private organizationBillingService: OrganizationBillingServiceAbstraction, | ||||||||||||||||||
private resellerWarningService: ResellerWarningService, | ||||||||||||||||||
) {} | ||||||||||||||||||
|
||||||||||||||||||
async ngOnInit() { | ||||||||||||||||||
|
@@ -612,6 +619,16 @@ | |||||||||||||||||
}), | ||||||||||||||||||
); | ||||||||||||||||||
|
||||||||||||||||||
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$), | ||||||||||||||||||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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