123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- import {Fragment} from 'react';
- import moment from 'moment-timezone';
- import {Alert} from 'sentry/components/core/alert';
- import List from 'sentry/components/list';
- import ListItem from 'sentry/components/list/listItem';
- import {IconArrow} from 'sentry/icons';
- import {DataCategory} from 'sentry/types/core';
- import {RESERVED_BUDGET_QUOTA} from 'getsentry/constants';
- import {usePlanMigrations} from 'getsentry/hooks/usePlanMigrations';
- import type {DataCategories, Plan, PlanMigration, Subscription} from 'getsentry/types';
- import {formatReservedWithUnits} from 'getsentry/utils/billing';
- import {
- getPlanCategoryName,
- getReservedBudgetDisplayName,
- } from 'getsentry/utils/dataCategory';
- import {displayPriceWithCents} from 'getsentry/views/amCheckout/utils';
- import {
- formatOnDemandBudget,
- isOnDemandBudgetsEqual,
- parseOnDemandBudgets,
- parseOnDemandBudgetsFromSubscription,
- } from 'getsentry/views/onDemandBudgets/utils';
- function getStringForPrice(
- price: number | null | undefined,
- minimumFractionDigits?: number,
- maximumFractionDigits?: number
- ) {
- return price === null
- ? 'None'
- : displayPriceWithCents({
- cents: price ?? 0,
- minimumFractionDigits,
- maximumFractionDigits,
- });
- }
- function formatChangeForCategory({
- category,
- changeTitle,
- oldValue,
- pendingValue,
- oldPlan,
- pendingPlan,
- shouldDistinguishDisplayNames = false,
- }: {
- category: DataCategory;
- changeTitle: string;
- oldPlan: Plan;
- oldValue: string;
- pendingPlan: Plan;
- pendingValue: string;
- shouldDistinguishDisplayNames?: boolean;
- }) {
- const oldUsesDsNames = oldPlan.categories.includes(DataCategory.SPANS_INDEXED);
- const pendingUsesDsNames = pendingPlan.categories.includes(DataCategory.SPANS_INDEXED);
- const oldDisplayName = getPlanCategoryName({
- plan: oldPlan,
- category,
- capitalize: false,
- hadCustomDynamicSampling: oldUsesDsNames,
- });
- const pendingDisplayName = getPlanCategoryName({
- plan: pendingPlan,
- category,
- capitalize: false,
- hadCustomDynamicSampling: pendingUsesDsNames,
- });
- return `${changeTitle} ${shouldDistinguishDisplayNames ? oldDisplayName : pendingDisplayName} — ${oldValue} → ${pendingValue} ${
- shouldDistinguishDisplayNames ? pendingDisplayName : ''
- }`;
- }
- function getRegularChanges(subscription: Subscription) {
- const {pendingChanges} = subscription;
- const changes: string[] = [];
- if (pendingChanges === null) {
- return changes;
- }
- if (Object.keys(pendingChanges).length === 0) {
- return changes;
- }
- if (pendingChanges.plan !== subscription.plan) {
- const old = subscription.planDetails.name;
- const change = pendingChanges.planDetails.name;
- changes.push(`Plan changes — ${old} → ${change}`);
- }
- const oldPlanUsesDsNames = subscription.planDetails.categories.includes(
- DataCategory.SPANS_INDEXED
- );
- const newPlanUsesDsNames = pendingChanges.planDetails.categories.includes(
- DataCategory.SPANS_INDEXED
- );
- if (
- pendingChanges.planDetails.contractInterval !==
- subscription.planDetails.contractInterval
- ) {
- const old = subscription.planDetails.contractInterval;
- const change = pendingChanges.planDetails.contractInterval;
- changes.push(`Contract period — ${old} → ${change}`);
- }
- if (
- pendingChanges.planDetails.billingInterval !==
- subscription.planDetails.billingInterval
- ) {
- const old = subscription.planDetails.billingInterval;
- const change = pendingChanges.planDetails.billingInterval;
- changes.push(`Billing period — ${old} → ${change}`);
- }
- if (
- pendingChanges.reservedEvents !== subscription.reservedEvents ||
- pendingChanges.reserved.errors !== subscription.categories.errors?.reserved
- ) {
- const old = formatReservedWithUnits(
- subscription.reservedEvents || (subscription.categories.errors?.reserved ?? null),
- DataCategory.ERRORS
- );
- const change = formatReservedWithUnits(
- pendingChanges.reservedEvents || (pendingChanges.reserved?.errors ?? null),
- DataCategory.ERRORS
- );
- changes.push(
- formatChangeForCategory({
- category: DataCategory.ERRORS,
- changeTitle: 'Reserved',
- oldValue: old,
- pendingValue: change,
- oldPlan: subscription.planDetails,
- pendingPlan: pendingChanges.planDetails,
- shouldDistinguishDisplayNames: true,
- })
- );
- }
- const categories = [
- ...new Set([
- ...subscription.planDetails.categories,
- ...Object.keys(pendingChanges.reserved ?? {}),
- ]),
- ] as DataCategories[];
- categories.forEach(category => {
- if (category !== 'errors') {
- // Errors and Events handled above
- if (
- (pendingChanges.reserved?.[category] ?? 0) !==
- (subscription.categories?.[category]?.reserved ?? 0)
- ) {
- const categoryEnum = category as DataCategory;
- const oldReserved = subscription.categories?.[category]?.reserved ?? null;
- const pendingReserved = pendingChanges.reserved?.[category] ?? null;
- const old =
- oldReserved === RESERVED_BUDGET_QUOTA
- ? 'reserved budget'
- : formatReservedWithUnits(oldReserved, categoryEnum);
- const change =
- pendingReserved === RESERVED_BUDGET_QUOTA
- ? 'reserved budget'
- : formatReservedWithUnits(pendingReserved, categoryEnum);
- changes.push(
- formatChangeForCategory({
- category: categoryEnum,
- changeTitle: 'Reserved',
- oldValue: old,
- pendingValue: change,
- oldPlan: subscription.planDetails,
- pendingPlan: pendingChanges.planDetails,
- shouldDistinguishDisplayNames: pendingReserved !== RESERVED_BUDGET_QUOTA,
- })
- );
- }
- }
- });
- if (pendingChanges.customPrice !== subscription.customPrice) {
- const old = getStringForPrice(subscription.customPrice);
- const change = getStringForPrice(pendingChanges.customPrice);
- changes.push(`Custom price (ACV) — ${old} → ${change}`);
- }
- categories.forEach(category => {
- if (
- (pendingChanges.customPrices?.[category as DataCategories] ?? 0) !==
- (subscription.categories?.[category as DataCategories]?.customPrice ?? 0)
- ) {
- const old = getStringForPrice(
- subscription.categories?.[category as DataCategories]?.customPrice
- );
- const change = getStringForPrice(
- pendingChanges.customPrices?.[category as DataCategories]
- );
- changes.push(
- formatChangeForCategory({
- category: category as DataCategory,
- changeTitle: 'Custom price for',
- oldValue: old,
- pendingValue: change,
- oldPlan: subscription.planDetails,
- pendingPlan: pendingChanges.planDetails,
- })
- );
- }
- });
- if (pendingChanges.customPricePcss !== subscription.customPricePcss) {
- const old = getStringForPrice(subscription.customPricePcss);
- const change = getStringForPrice(pendingChanges.customPricePcss);
- changes.push(`Custom price for PCSS — ${old} → ${change}`);
- }
- const oldBudgets = subscription.reservedBudgets;
- const oldCpeByCategory: Record<string, number> = {};
- oldBudgets?.forEach(budget => {
- Object.entries(budget.categories).forEach(([category, info]) => {
- if (info?.reservedCpe) {
- oldCpeByCategory[category] = info.reservedCpe;
- }
- });
- });
- categories.forEach(category => {
- if (
- (pendingChanges.reservedCpe?.[category] ?? null) !==
- (oldCpeByCategory[category] ?? null)
- ) {
- const old = getStringForPrice(oldCpeByCategory[category] ?? null, 8, 8);
- const change = getStringForPrice(
- pendingChanges.reservedCpe?.[category] ?? null,
- 8,
- 8
- );
- changes.push(
- formatChangeForCategory({
- category: category as DataCategory,
- changeTitle: 'Reserved cost-per-event for',
- oldValue: old,
- pendingValue: change,
- oldPlan: subscription.planDetails,
- pendingPlan: pendingChanges.planDetails,
- })
- );
- }
- });
- const oldBudgetsChanges: string[] = [];
- const newBudgetsChanges: string[] = [];
- oldBudgets?.forEach(budget => {
- const budgetName = getReservedBudgetDisplayName({
- plan: subscription.planDetails,
- categories: Object.keys(budget.categories),
- hadCustomDynamicSampling: oldPlanUsesDsNames,
- });
- oldBudgetsChanges.push(
- `${getStringForPrice(budget.reservedBudget)} for ${budgetName}`
- );
- });
- pendingChanges.reservedBudgets.forEach(budget => {
- const budgetName = getReservedBudgetDisplayName({
- plan: pendingChanges.planDetails,
- categories: Object.keys(budget.categories),
- hadCustomDynamicSampling: newPlanUsesDsNames,
- });
- newBudgetsChanges.push(
- `${getStringForPrice(budget.reservedBudget)} for ${budgetName}`
- );
- });
- if (oldBudgetsChanges.length > 0 || newBudgetsChanges.length > 0) {
- changes.push(
- `Reserved budgets — ${
- oldBudgetsChanges.length > 0 ? oldBudgetsChanges.join(', ') : 'None'
- } → ${newBudgetsChanges.length > 0 ? newBudgetsChanges.join(', ') : 'None'}`
- );
- }
- return changes;
- }
- function getOnDemandChanges(subscription: Subscription) {
- const {pendingChanges} = subscription;
- const changes: React.ReactNode[] = [];
- if (pendingChanges === null) {
- return changes;
- }
- if (Object.keys(pendingChanges).length === 0) {
- return changes;
- }
- if (subscription.onDemandBudgets && pendingChanges.onDemandBudgets) {
- const pendingOnDemandBudgets = parseOnDemandBudgets(pendingChanges.onDemandBudgets);
- const currentOnDemandBudgets = parseOnDemandBudgetsFromSubscription(subscription);
- if (!isOnDemandBudgetsEqual(pendingOnDemandBudgets, currentOnDemandBudgets)) {
- const current = formatOnDemandBudget(
- subscription.planDetails,
- subscription.planTier,
- currentOnDemandBudgets,
- subscription.planDetails.onDemandCategories
- );
- const change = formatOnDemandBudget(
- pendingChanges.planDetails,
- subscription.planTier,
- pendingOnDemandBudgets,
- pendingChanges.planDetails.onDemandCategories
- );
- changes.push(
- <span>
- On-demand budget — {current} → {change}
- </span>
- );
- }
- } else if (pendingChanges.onDemandMaxSpend !== subscription.onDemandMaxSpend) {
- const old = getStringForPrice(subscription.onDemandMaxSpend);
- const change = getStringForPrice(pendingChanges.onDemandMaxSpend);
- changes.push(
- <span>
- On-demand maximum — {old} → {change}
- </span>
- );
- }
- return changes;
- }
- type Change = {
- effectiveDate: string;
- items: React.ReactNode[];
- };
- function getChanges(subscription: Subscription, planMigrations: PlanMigration[]) {
- const {pendingChanges} = subscription;
- const changeSet: Change[] = [];
- if (pendingChanges === null) {
- return changeSet;
- }
- const activeMigration = planMigrations.find(
- ({dateApplied, cohort}) => dateApplied === null && cohort?.nextPlan
- );
- const {onDemandEffectiveDate} = pendingChanges;
- const effectiveDate = activeMigration?.effectiveAt ?? pendingChanges.effectiveDate;
- const regularChanges = getRegularChanges(subscription);
- if (regularChanges.length > 0) {
- changeSet.push({effectiveDate, items: regularChanges});
- }
- const onDemandChanges = getOnDemandChanges(subscription);
- if (onDemandChanges.length) {
- changeSet.push({effectiveDate: onDemandEffectiveDate, items: onDemandChanges});
- }
- return changeSet;
- }
- function PendingChanges({subscription}: any) {
- const {pendingChanges} = subscription;
- const {planMigrations, isLoading} = usePlanMigrations();
- if (isLoading) {
- return null;
- }
- if (typeof pendingChanges !== 'object' || pendingChanges === null) {
- return null;
- }
- const changes = getChanges(subscription, planMigrations);
- if (!changes.length) {
- return null;
- }
- return (
- <Fragment>
- <Alert.Container>
- <Alert type="info" showIcon>
- This account has pending changes to the subscription
- </Alert>
- </Alert.Container>
- <List>
- {changes.map((change, changeIdx) => (
- <ListItem key={changeIdx}>
- <p>
- The following changes will take effect on{' '}
- <strong>{moment(change.effectiveDate).format('ll')}</strong>:
- </p>
- <List symbol={<IconArrow direction="right" size="xs" />}>
- {change.items.map((item, itemIdx) => (
- <ListItem key={itemIdx}>{item}</ListItem>
- ))}
- </List>
- </ListItem>
- ))}
- </List>
- </Fragment>
- );
- }
- export default PendingChanges;
|