chore: clean up item change text methods
This commit is contained in:
@@ -197,8 +197,14 @@ export * from "./utils/productDisplayUtils.js";
|
||||
export * from "./utils/productUtils/convertProductUtils.js";
|
||||
export * from "./utils/productUtils/priceToInvoiceAmount.js";
|
||||
export * from "./utils/productUtils/productUtils.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils.ts/compareItemUtils.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils.ts/compareProductUtils.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/buildEditsForItem.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/compareItemUtils.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/compareProductUtils.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/generateItemChanges.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/generatePrepaidChanges.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/generateTrialChanges.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/generateVersionChanges.js";
|
||||
export * from "./utils/productV2Utils/compareProductUtils/itemEditTypes.js";
|
||||
export * from "./utils/productV2Utils/productItemUtils/convertItemUtils.js";
|
||||
export * from "./utils/productV2Utils/productItemUtils/getProductItemRes.js";
|
||||
export * from "./utils/productV2Utils/productItemUtils/itemIntervalUtils.js";
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
import { type ProductItem, UsageModel } from "@autumn/shared";
|
||||
import type { ItemEdit } from "../types/summary";
|
||||
import {
|
||||
type ProductItem,
|
||||
UsageModel,
|
||||
} from "../../../models/productV2Models/productItemModels/productItemModels.js";
|
||||
import type { ItemEdit } from "./itemEditTypes.js";
|
||||
|
||||
/** Format tier threshold value for display */
|
||||
function formatTierValue(value: string | number | "inf"): string {
|
||||
return value === "inf" ? "Infinite" : String(value);
|
||||
}
|
||||
|
||||
/** Builds a list of granular edits for a single product item comparison */
|
||||
export function buildEditsForItem({
|
||||
item,
|
||||
originalItem,
|
||||
prepaidQuantity,
|
||||
initialPrepaidQuantity,
|
||||
updatedItem,
|
||||
originalPrepaidQuantity,
|
||||
updatedPrepaidQuantity,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
originalItem?: ProductItem;
|
||||
prepaidQuantity?: number;
|
||||
initialPrepaidQuantity?: number;
|
||||
updatedItem: ProductItem;
|
||||
originalPrepaidQuantity?: number;
|
||||
updatedPrepaidQuantity?: number;
|
||||
}): ItemEdit[] {
|
||||
const edits: ItemEdit[] = [];
|
||||
const isPrepaid = item.usage_model === UsageModel.Prepaid;
|
||||
const isPrepaid = updatedItem.usage_model === UsageModel.Prepaid;
|
||||
|
||||
if (originalItem) {
|
||||
if (
|
||||
originalItem.price !== item.price &&
|
||||
originalItem.price !== updatedItem.price &&
|
||||
originalItem.price !== null &&
|
||||
originalItem.price !== undefined &&
|
||||
item.price !== null &&
|
||||
item.price !== undefined
|
||||
updatedItem.price !== null &&
|
||||
updatedItem.price !== undefined
|
||||
) {
|
||||
const oldPrice = originalItem.price;
|
||||
const newPrice = item.price;
|
||||
const isUpgrade = newPrice < oldPrice;
|
||||
const newPrice = updatedItem.price;
|
||||
const isUpgrade = newPrice > oldPrice;
|
||||
edits.push({
|
||||
id: `price-${item.feature_id}`,
|
||||
id: `price-${updatedItem.feature_id}`,
|
||||
type: "config",
|
||||
icon: "price",
|
||||
label: "Price",
|
||||
description: isUpgrade
|
||||
? `Price decreased from $${oldPrice} to $${newPrice}`
|
||||
: `Price increased from $${oldPrice} to $${newPrice}`,
|
||||
? `Price increased from $${oldPrice} to $${newPrice}`
|
||||
: `Price decreased from $${oldPrice} to $${newPrice}`,
|
||||
oldValue: `$${oldPrice}`,
|
||||
newValue: `$${newPrice}`,
|
||||
isUpgrade,
|
||||
@@ -46,7 +49,7 @@ export function buildEditsForItem({
|
||||
}
|
||||
|
||||
const oldTiers = originalItem.tiers ?? [];
|
||||
const newTiers = item.tiers ?? [];
|
||||
const newTiers = updatedItem.tiers ?? [];
|
||||
|
||||
for (let i = 0; i < newTiers.length; i++) {
|
||||
const newTier = newTiers[i];
|
||||
@@ -57,7 +60,7 @@ export function buildEditsForItem({
|
||||
const prevTierTo = i === 0 ? "0" : (oldTiers[i - 1]?.to ?? "0");
|
||||
const prevLabel = formatTierValue(prevTierTo);
|
||||
edits.push({
|
||||
id: `tier-${item.feature_id}-${i}`,
|
||||
id: `tier-${updatedItem.feature_id}-${i}`,
|
||||
type: "config",
|
||||
icon: "tier",
|
||||
label: "Pricing Tier",
|
||||
@@ -67,15 +70,15 @@ export function buildEditsForItem({
|
||||
isUpgrade: true,
|
||||
});
|
||||
} else if (oldTier.amount !== newTier.amount) {
|
||||
const isUpgrade = newTier.amount < oldTier.amount;
|
||||
const isUpgrade = newTier.amount > oldTier.amount;
|
||||
edits.push({
|
||||
id: `tier-${item.feature_id}-${i}`,
|
||||
id: `tier-${updatedItem.feature_id}-${i}`,
|
||||
type: "config",
|
||||
icon: "tier",
|
||||
label: "Pricing Tier",
|
||||
description: isUpgrade
|
||||
? `Tier price decreased from $${oldTier.amount} to $${newTier.amount} (up to ${tierLabel})`
|
||||
: `Tier price increased from $${oldTier.amount} to $${newTier.amount} (up to ${tierLabel})`,
|
||||
? `Tier price increased from $${oldTier.amount} to $${newTier.amount} (up to ${tierLabel})`
|
||||
: `Tier price decreased from $${oldTier.amount} to $${newTier.amount} (up to ${tierLabel})`,
|
||||
oldValue: `$${oldTier.amount}`,
|
||||
newValue: `$${newTier.amount}`,
|
||||
isUpgrade,
|
||||
@@ -87,7 +90,7 @@ export function buildEditsForItem({
|
||||
newTier.to === "inf" ||
|
||||
(oldTier.to !== "inf" && Number(newTier.to) > Number(oldTier.to));
|
||||
edits.push({
|
||||
id: `tier-${item.feature_id}-${i}-threshold`,
|
||||
id: `tier-${updatedItem.feature_id}-${i}-threshold`,
|
||||
type: "config",
|
||||
icon: "tier",
|
||||
label: "Pricing Tier",
|
||||
@@ -105,7 +108,7 @@ export function buildEditsForItem({
|
||||
const oldTier = oldTiers[i];
|
||||
const tierLabel = formatTierValue(oldTier.to);
|
||||
edits.push({
|
||||
id: `tier-${item.feature_id}-${i}-removed`,
|
||||
id: `tier-${updatedItem.feature_id}-${i}-removed`,
|
||||
type: "config",
|
||||
icon: "tier",
|
||||
label: "Pricing Tier",
|
||||
@@ -117,7 +120,7 @@ export function buildEditsForItem({
|
||||
}
|
||||
|
||||
const oldUsage = originalItem.included_usage ?? 0;
|
||||
const newUsage = item.included_usage ?? 0;
|
||||
const newUsage = updatedItem.included_usage ?? 0;
|
||||
if (oldUsage !== newUsage) {
|
||||
const formatUsageValue = (val: string | number) =>
|
||||
val === "inf" ? "unlimited" : String(val);
|
||||
@@ -129,7 +132,7 @@ export function buildEditsForItem({
|
||||
newUsage === "inf" ? Number.POSITIVE_INFINITY : Number(newUsage);
|
||||
const isUpgrade = newNum > oldNum;
|
||||
edits.push({
|
||||
id: `usage-${item.feature_id}`,
|
||||
id: `usage-${updatedItem.feature_id}`,
|
||||
type: "config",
|
||||
icon: "usage",
|
||||
label: "Included Usage",
|
||||
@@ -143,11 +146,11 @@ export function buildEditsForItem({
|
||||
}
|
||||
|
||||
const oldUnits = originalItem.billing_units ?? 1;
|
||||
const newUnits = item.billing_units ?? 1;
|
||||
const newUnits = updatedItem.billing_units ?? 1;
|
||||
if (oldUnits !== newUnits) {
|
||||
const isUpgrade = newUnits > oldUnits;
|
||||
edits.push({
|
||||
id: `units-${item.feature_id}`,
|
||||
id: `units-${updatedItem.feature_id}`,
|
||||
type: "config",
|
||||
icon: "units",
|
||||
label: "Billing Units",
|
||||
@@ -163,21 +166,21 @@ export function buildEditsForItem({
|
||||
|
||||
if (
|
||||
isPrepaid &&
|
||||
initialPrepaidQuantity !== undefined &&
|
||||
prepaidQuantity !== undefined &&
|
||||
prepaidQuantity !== initialPrepaidQuantity
|
||||
originalPrepaidQuantity !== undefined &&
|
||||
updatedPrepaidQuantity !== undefined &&
|
||||
updatedPrepaidQuantity !== originalPrepaidQuantity
|
||||
) {
|
||||
const isUpgrade = prepaidQuantity > initialPrepaidQuantity;
|
||||
const isUpgrade = updatedPrepaidQuantity > originalPrepaidQuantity;
|
||||
edits.push({
|
||||
id: `prepaid-${item.feature_id}`,
|
||||
id: `prepaid-${updatedItem.feature_id}`,
|
||||
type: "prepaid",
|
||||
icon: "prepaid",
|
||||
label: "Prepaid Quantity",
|
||||
description: isUpgrade
|
||||
? `Prepaid quantity increased from ${initialPrepaidQuantity} to ${prepaidQuantity}`
|
||||
: `Prepaid quantity decreased from ${initialPrepaidQuantity} to ${prepaidQuantity}`,
|
||||
oldValue: `${initialPrepaidQuantity} Prepaid`,
|
||||
newValue: `${prepaidQuantity} Prepaid`,
|
||||
? `Prepaid quantity increased from ${originalPrepaidQuantity} to ${updatedPrepaidQuantity}`
|
||||
: `Prepaid quantity decreased from ${originalPrepaidQuantity} to ${updatedPrepaidQuantity}`,
|
||||
oldValue: `${originalPrepaidQuantity} Prepaid`,
|
||||
newValue: `${updatedPrepaidQuantity} Prepaid`,
|
||||
isUpgrade,
|
||||
editable: true,
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Feature } from "../../../models/featureModels/featureModels.js";
|
||||
import type { PriceTier } from "../../../models/productV2Models/productItemModels/productItemModels.js";
|
||||
import {
|
||||
type Feature,
|
||||
type PriceTier,
|
||||
type ProductItem,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import type { SummaryItem } from "../types/summary";
|
||||
} from "../../../models/productV2Models/productItemModels/productItemModels.js";
|
||||
import type { ItemEdit } from "./itemEditTypes.js";
|
||||
|
||||
function hasValue<T>(value: T | null | undefined): value is T {
|
||||
return value !== null && value !== undefined;
|
||||
@@ -37,18 +37,19 @@ function formatTierPricing({
|
||||
: `$${firstPricedTier.amount} per unit`;
|
||||
}
|
||||
|
||||
/** Generates edit items for product item additions, removals, and modifications */
|
||||
export function generateItemChanges({
|
||||
originalItems,
|
||||
customizedItems,
|
||||
updatedItems,
|
||||
features,
|
||||
prepaidOptions,
|
||||
}: {
|
||||
originalItems: ProductItem[] | undefined;
|
||||
customizedItems: ProductItem[] | null;
|
||||
updatedItems: ProductItem[] | null;
|
||||
features?: Feature[];
|
||||
prepaidOptions?: Record<string, number>;
|
||||
}): SummaryItem[] {
|
||||
if (!customizedItems || !originalItems) return [];
|
||||
}): ItemEdit[] {
|
||||
if (!updatedItems || !originalItems) return [];
|
||||
|
||||
const featureNameMap = new Map<string, string>();
|
||||
if (features) {
|
||||
@@ -65,103 +66,120 @@ export function generateItemChanges({
|
||||
return item.feature_id ?? "Item";
|
||||
};
|
||||
|
||||
const changes: SummaryItem[] = [];
|
||||
const changes: ItemEdit[] = [];
|
||||
|
||||
const originalFeatureMap = new Map(
|
||||
originalItems
|
||||
.filter((item) => item.feature_id)
|
||||
.map((item) => [item.feature_id, item]),
|
||||
);
|
||||
const customizedFeatureMap = new Map(
|
||||
customizedItems
|
||||
const updatedFeatureMap = new Map(
|
||||
updatedItems
|
||||
.filter((item) => item.feature_id)
|
||||
.map((item) => [item.feature_id, item]),
|
||||
);
|
||||
|
||||
for (const [featureId, original] of originalFeatureMap) {
|
||||
const customized = customizedFeatureMap.get(featureId);
|
||||
const updated = updatedFeatureMap.get(featureId);
|
||||
|
||||
if (!customized) {
|
||||
if (!updated) {
|
||||
const oldFormatted = formatItemValue(original);
|
||||
changes.push({
|
||||
id: `item-removed-${featureId}`,
|
||||
type: "item",
|
||||
label: getFeatureName(original),
|
||||
oldValue: formatItemValue(original),
|
||||
icon: "item",
|
||||
description: `${getFeatureName(original)} removed (was ${oldFormatted})`,
|
||||
oldValue: oldFormatted,
|
||||
newValue: null,
|
||||
productItem: original,
|
||||
isUpgrade: false,
|
||||
});
|
||||
} else if (hasItemChanged(original, customized)) {
|
||||
} else if (
|
||||
hasItemChanged({ originalItem: original, updatedItem: updated })
|
||||
) {
|
||||
const oldValueFormatted = formatChangedItemValue({
|
||||
item: original,
|
||||
original,
|
||||
customized,
|
||||
originalItem: original,
|
||||
updatedItem: updated,
|
||||
});
|
||||
const newValueFormatted = formatChangedItemValue({
|
||||
item: customized,
|
||||
original,
|
||||
customized,
|
||||
item: updated,
|
||||
originalItem: original,
|
||||
updatedItem: updated,
|
||||
});
|
||||
changes.push({
|
||||
id: `item-modified-${featureId}`,
|
||||
type: "item",
|
||||
label: getFeatureName(original),
|
||||
icon: "item",
|
||||
description: `${getFeatureName(original)} changed from ${oldValueFormatted} to ${newValueFormatted}`,
|
||||
oldValue: oldValueFormatted,
|
||||
newValue: newValueFormatted,
|
||||
productItem: customized,
|
||||
isUpgrade: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [featureId, customized] of customizedFeatureMap) {
|
||||
for (const [featureId, updated] of updatedFeatureMap) {
|
||||
if (!originalFeatureMap.has(featureId)) {
|
||||
const prepaidQuantity =
|
||||
featureId && prepaidOptions ? prepaidOptions[featureId] : undefined;
|
||||
|
||||
const isPrepaidWithQuantity =
|
||||
customized.usage_model === UsageModel.Prepaid &&
|
||||
updated.usage_model === UsageModel.Prepaid &&
|
||||
hasValue(prepaidQuantity) &&
|
||||
prepaidQuantity > 0;
|
||||
|
||||
const billingUnits = customized.billing_units ?? 1;
|
||||
const billingUnits = updated.billing_units ?? 1;
|
||||
const displayQuantity = prepaidQuantity
|
||||
? prepaidQuantity * billingUnits
|
||||
: 0;
|
||||
|
||||
const newValue = isPrepaidWithQuantity
|
||||
? displayQuantity
|
||||
: formatItemValue(updated);
|
||||
|
||||
changes.push({
|
||||
id: `item-added-${featureId}`,
|
||||
type: "item",
|
||||
label: getFeatureName(customized),
|
||||
label: getFeatureName(updated),
|
||||
icon: "item",
|
||||
description: `${getFeatureName(updated)} added (${newValue})`,
|
||||
oldValue: isPrepaidWithQuantity ? 0 : null,
|
||||
newValue: isPrepaidWithQuantity
|
||||
? displayQuantity
|
||||
: formatItemValue(customized),
|
||||
productItem: customized,
|
||||
newValue,
|
||||
isUpgrade: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const originalPriceItems = originalItems.filter((item) => !item.feature_id);
|
||||
const customizedPriceItems = customizedItems.filter(
|
||||
(item) => !item.feature_id,
|
||||
);
|
||||
const updatedPriceItems = updatedItems.filter((item) => !item.feature_id);
|
||||
|
||||
if (originalPriceItems.length !== customizedPriceItems.length) {
|
||||
const priceDiff = customizedPriceItems.length - originalPriceItems.length;
|
||||
if (originalPriceItems.length !== updatedPriceItems.length) {
|
||||
const priceDiff = updatedPriceItems.length - originalPriceItems.length;
|
||||
if (priceDiff > 0) {
|
||||
const label = `${priceDiff} Price Item${priceDiff > 1 ? "s" : ""}`;
|
||||
changes.push({
|
||||
id: "price-items-added",
|
||||
type: "item",
|
||||
label: `${priceDiff} Price Item${priceDiff > 1 ? "s" : ""}`,
|
||||
label,
|
||||
icon: "price",
|
||||
description: `${label} added`,
|
||||
oldValue: null,
|
||||
newValue: String(priceDiff),
|
||||
isUpgrade: true,
|
||||
});
|
||||
} else {
|
||||
const label = `${Math.abs(priceDiff)} Price Item${Math.abs(priceDiff) > 1 ? "s" : ""}`;
|
||||
changes.push({
|
||||
id: "price-items-removed",
|
||||
type: "item",
|
||||
label: `${Math.abs(priceDiff)} Price Item${Math.abs(priceDiff) > 1 ? "s" : ""}`,
|
||||
label,
|
||||
icon: "price",
|
||||
description: `${label} removed`,
|
||||
oldValue: String(Math.abs(priceDiff)),
|
||||
newValue: null,
|
||||
isUpgrade: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -169,16 +187,19 @@ export function generateItemChanges({
|
||||
return changes;
|
||||
}
|
||||
|
||||
function hasItemChanged(
|
||||
original: ProductItem,
|
||||
customized: ProductItem,
|
||||
): boolean {
|
||||
function hasItemChanged({
|
||||
originalItem,
|
||||
updatedItem,
|
||||
}: {
|
||||
originalItem: ProductItem;
|
||||
updatedItem: ProductItem;
|
||||
}): boolean {
|
||||
return (
|
||||
original.price !== customized.price ||
|
||||
original.included_usage !== customized.included_usage ||
|
||||
JSON.stringify(original.tiers) !== JSON.stringify(customized.tiers) ||
|
||||
original.billing_units !== customized.billing_units ||
|
||||
original.interval !== customized.interval
|
||||
originalItem.price !== updatedItem.price ||
|
||||
originalItem.included_usage !== updatedItem.included_usage ||
|
||||
JSON.stringify(originalItem.tiers) !== JSON.stringify(updatedItem.tiers) ||
|
||||
originalItem.billing_units !== updatedItem.billing_units ||
|
||||
originalItem.interval !== updatedItem.interval
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,28 +230,23 @@ function formatItemValue(item: ProductItem): string {
|
||||
return `$${item.price}`;
|
||||
}
|
||||
|
||||
// Boolean features have no price/tiers/usage - just "enabled" when present
|
||||
if (parts.length === 0) return "Enabled";
|
||||
return parts.join(" + ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format only the parts of an item that changed between original and customized.
|
||||
* Avoids showing unchanged values like "+ $10 per unit" when only included_usage changed.
|
||||
*/
|
||||
function formatChangedItemValue({
|
||||
item,
|
||||
original,
|
||||
customized,
|
||||
originalItem,
|
||||
updatedItem,
|
||||
}: {
|
||||
item: ProductItem;
|
||||
original: ProductItem;
|
||||
customized: ProductItem;
|
||||
originalItem: ProductItem;
|
||||
updatedItem: ProductItem;
|
||||
}): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
const includedUsageChanged =
|
||||
original.included_usage !== customized.included_usage;
|
||||
originalItem.included_usage !== updatedItem.included_usage;
|
||||
if (includedUsageChanged) {
|
||||
if (item.included_usage === "inf") {
|
||||
parts.push("Unlimited");
|
||||
@@ -239,32 +255,31 @@ function formatChangedItemValue({
|
||||
}
|
||||
}
|
||||
|
||||
const priceChanged = original.price !== customized.price;
|
||||
const priceChanged = originalItem.price !== updatedItem.price;
|
||||
if (priceChanged && hasValue(item.price)) {
|
||||
const billingUnits = item.billing_units ?? 1;
|
||||
parts.push(formatPriceWithUnits(item.price, billingUnits));
|
||||
}
|
||||
|
||||
const tiersChanged =
|
||||
JSON.stringify(original.tiers) !== JSON.stringify(customized.tiers);
|
||||
JSON.stringify(originalItem.tiers) !== JSON.stringify(updatedItem.tiers);
|
||||
if (tiersChanged && item.tiers?.length) {
|
||||
const billingUnits = item.billing_units ?? 1;
|
||||
parts.push(formatTierPricing({ tiers: item.tiers, billingUnits }));
|
||||
}
|
||||
|
||||
const billingUnitsChanged =
|
||||
original.billing_units !== customized.billing_units;
|
||||
originalItem.billing_units !== updatedItem.billing_units;
|
||||
if (billingUnitsChanged && !priceChanged && hasValue(item.price)) {
|
||||
const billingUnits = item.billing_units ?? 1;
|
||||
parts.push(formatPriceWithUnits(item.price, billingUnits));
|
||||
}
|
||||
|
||||
const intervalChanged = original.interval !== customized.interval;
|
||||
const intervalChanged = originalItem.interval !== updatedItem.interval;
|
||||
if (intervalChanged && item.interval) {
|
||||
parts.push(`${item.interval}`);
|
||||
}
|
||||
|
||||
// Boolean features or edge cases (null vs undefined) - just "enabled" when present
|
||||
if (parts.length === 0) return "Enabled";
|
||||
return parts.join(" + ");
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ProductItem } from "../../../models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { formatAmount } from "../../common/formatUtils/formatAmount.js";
|
||||
import type { ItemEdit } from "./itemEditTypes.js";
|
||||
|
||||
/** Generates edit items for prepaid quantity changes */
|
||||
export function generatePrepaidChanges({
|
||||
prepaidItems,
|
||||
originalOptions,
|
||||
updatedOptions,
|
||||
currency = "usd",
|
||||
}: {
|
||||
prepaidItems: ProductItem[];
|
||||
originalOptions: Record<string, number>;
|
||||
updatedOptions: Record<string, number>;
|
||||
currency?: string;
|
||||
}): ItemEdit[] {
|
||||
return prepaidItems
|
||||
.map((item) => {
|
||||
const featureId = item.feature_id ?? "";
|
||||
const oldQuantity = originalOptions[featureId] ?? 0;
|
||||
const newQuantity = updatedOptions[featureId] ?? 0;
|
||||
|
||||
if (oldQuantity === newQuantity) return null;
|
||||
|
||||
const billingUnits = item.billing_units ?? 1;
|
||||
const oldDisplayQuantity = oldQuantity * billingUnits;
|
||||
const newDisplayQuantity = newQuantity * billingUnits;
|
||||
|
||||
const unitPrice = item.price ?? null;
|
||||
const costDelta =
|
||||
unitPrice !== null ? (newQuantity - oldQuantity) * unitPrice : null;
|
||||
|
||||
const featureName = item.feature?.name ?? "Items";
|
||||
const isUpgrade = newQuantity > oldQuantity;
|
||||
|
||||
let description = `Prepaid quantity changed from ${oldDisplayQuantity} to ${newDisplayQuantity}`;
|
||||
if (costDelta !== null && costDelta !== 0) {
|
||||
const formattedCost = formatAmount({
|
||||
amount: Math.abs(costDelta),
|
||||
currency,
|
||||
minFractionDigits: 2,
|
||||
amountFormatOptions: { currencyDisplay: "narrowSymbol" },
|
||||
});
|
||||
description += ` (${costDelta > 0 ? "+" : "-"}${formattedCost})`;
|
||||
}
|
||||
|
||||
return {
|
||||
id: `prepaid-${featureId}`,
|
||||
type: "prepaid" as const,
|
||||
label: featureName,
|
||||
icon: "prepaid" as const,
|
||||
description,
|
||||
oldValue: oldDisplayQuantity,
|
||||
newValue: newDisplayQuantity,
|
||||
isUpgrade,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as ItemEdit[];
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { addDays, formatDuration, intervalToDuration } from "date-fns";
|
||||
import type { FullCusProduct } from "../../../models/cusProductModels/cusProductModels.js";
|
||||
import type { FreeTrialDuration } from "../../../models/productModels/freeTrialModels/freeTrialEnums.js";
|
||||
import { isCustomerProductTrialing } from "../../cusProductUtils/classifyCustomerProduct/classifyCustomerProduct.js";
|
||||
import {
|
||||
type FreeTrialDuration,
|
||||
type FullCusProduct,
|
||||
getRemainingTrialDays,
|
||||
getTrialLengthInDays,
|
||||
isCustomerProductTrialing,
|
||||
} from "@autumn/shared";
|
||||
import { addDays, formatDuration, intervalToDuration } from "date-fns";
|
||||
import type { SummaryItem } from "../types/summary";
|
||||
} from "../../productUtils/freeTrialUtils.js";
|
||||
import type { ItemEdit } from "./itemEditTypes.js";
|
||||
|
||||
function formatDaysAsReadable(totalDays: number): string {
|
||||
if (totalDays <= 0) return "0 days";
|
||||
@@ -21,6 +21,7 @@ function formatDaysAsReadable(totalDays: number): string {
|
||||
});
|
||||
}
|
||||
|
||||
/** Generates edit items for trial period changes */
|
||||
export function generateTrialChanges({
|
||||
customerProduct,
|
||||
removeTrial,
|
||||
@@ -31,20 +32,23 @@ export function generateTrialChanges({
|
||||
removeTrial: boolean;
|
||||
trialLength: number | null;
|
||||
trialDuration: FreeTrialDuration;
|
||||
}): SummaryItem[] {
|
||||
}): ItemEdit[] {
|
||||
const isCurrentlyTrialing = isCustomerProductTrialing(customerProduct);
|
||||
const remainingDays = getRemainingTrialDays({
|
||||
trialEndsAt: customerProduct.trial_ends_at,
|
||||
});
|
||||
const changes: SummaryItem[] = [];
|
||||
const changes: ItemEdit[] = [];
|
||||
|
||||
if (removeTrial && isCurrentlyTrialing) {
|
||||
changes.push({
|
||||
id: "trial-remove",
|
||||
type: "trial",
|
||||
label: "End Trial",
|
||||
icon: "trial",
|
||||
description: "Trial ended",
|
||||
oldValue: "Active",
|
||||
newValue: null,
|
||||
isUpgrade: false,
|
||||
});
|
||||
return changes;
|
||||
}
|
||||
@@ -53,25 +57,35 @@ export function generateTrialChanges({
|
||||
const newTrialDays = getTrialLengthInDays({ trialLength, trialDuration });
|
||||
|
||||
if (isCurrentlyTrialing && remainingDays !== null) {
|
||||
// Skip if no actual change (same number of days)
|
||||
if (newTrialDays === remainingDays) return changes;
|
||||
|
||||
const isExtending = newTrialDays > remainingDays;
|
||||
const oldFormatted = formatDaysAsReadable(remainingDays);
|
||||
const newFormatted = formatDaysAsReadable(newTrialDays);
|
||||
|
||||
changes.push({
|
||||
id: isExtending ? "trial-extend" : "trial-shorten",
|
||||
type: "trial",
|
||||
label: isExtending ? "Extend Trial" : "Shorten Trial",
|
||||
oldValue: formatDaysAsReadable(remainingDays),
|
||||
newValue: formatDaysAsReadable(newTrialDays),
|
||||
icon: "trial",
|
||||
description: isExtending
|
||||
? `Trial extended from ${oldFormatted} to ${newFormatted}`
|
||||
: `Trial shortened from ${oldFormatted} to ${newFormatted}`,
|
||||
oldValue: oldFormatted,
|
||||
newValue: newFormatted,
|
||||
isUpgrade: isExtending,
|
||||
});
|
||||
} else {
|
||||
// Not currently trialing - adding new trial
|
||||
const newFormatted = formatDaysAsReadable(newTrialDays);
|
||||
changes.push({
|
||||
id: "trial-add",
|
||||
type: "trial",
|
||||
label: "Free Trial",
|
||||
icon: "trial",
|
||||
description: `Free trial added for ${newFormatted}`,
|
||||
oldValue: null,
|
||||
newValue: formatDaysAsReadable(newTrialDays),
|
||||
newValue: newFormatted,
|
||||
isUpgrade: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ItemEdit } from "./itemEditTypes.js";
|
||||
|
||||
/** Generates edit items for plan version changes */
|
||||
export function generateVersionChanges({
|
||||
originalVersion,
|
||||
updatedVersion,
|
||||
}: {
|
||||
originalVersion: number;
|
||||
updatedVersion: number;
|
||||
}): ItemEdit[] {
|
||||
if (updatedVersion === originalVersion) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isUpgrade = updatedVersion > originalVersion;
|
||||
|
||||
return [
|
||||
{
|
||||
id: "version-change",
|
||||
type: "version",
|
||||
label: "Plan Version",
|
||||
icon: "version",
|
||||
description: isUpgrade
|
||||
? `Plan version upgraded from v${originalVersion} to v${updatedVersion}`
|
||||
: `Plan version downgraded from v${originalVersion} to v${updatedVersion}`,
|
||||
oldValue: originalVersion,
|
||||
newValue: updatedVersion,
|
||||
isUpgrade,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/** Type of change being made */
|
||||
export type EditType = "config" | "prepaid" | "trial" | "version" | "item";
|
||||
|
||||
/** Icon type for visual representation of the edit */
|
||||
export type EditIconType =
|
||||
| "price"
|
||||
| "tier"
|
||||
| "usage"
|
||||
| "units"
|
||||
| "prepaid"
|
||||
| "trial"
|
||||
| "version"
|
||||
| "item";
|
||||
|
||||
/** Represents a single edit/change to a subscription or product item */
|
||||
export interface ItemEdit {
|
||||
id: string;
|
||||
type: EditType;
|
||||
label: string;
|
||||
/** Icon type for the edit */
|
||||
icon: EditIconType;
|
||||
/** Full sentence description for display */
|
||||
description: string;
|
||||
oldValue: string | number | null;
|
||||
newValue: string | number | null;
|
||||
/** Whether this is an upgrade (true) or downgrade (false) */
|
||||
isUpgrade: boolean;
|
||||
/** Whether this edit has an inline editor (prepaid quantity) */
|
||||
editable?: boolean;
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CompactValueChangeProps {
|
||||
oldValue: string | number | null;
|
||||
newValue: string | number | null;
|
||||
isUpgrade?: boolean;
|
||||
}
|
||||
|
||||
export function CompactValueChange({
|
||||
oldValue,
|
||||
newValue,
|
||||
isUpgrade = true,
|
||||
}: CompactValueChangeProps) {
|
||||
return (
|
||||
<span className="text-xs flex items-center gap-1">
|
||||
<span className="text-red-500">{oldValue}</span>
|
||||
<span className={cn(isUpgrade ? "text-red-500" : "text-green-500")}>
|
||||
{oldValue}
|
||||
</span>
|
||||
<span className="text-t3">→</span>
|
||||
<span className="text-green-500">{newValue}</span>
|
||||
<span className={cn(isUpgrade ? "text-green-500" : "text-red-500")}>
|
||||
{newValue}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,16 @@ import type {
|
||||
ProductItem,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import { featureToOptions, UsageModel } from "@autumn/shared";
|
||||
import {
|
||||
buildEditsForItem,
|
||||
featureToOptions,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import { PencilSimpleIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import { BasePriceDisplay } from "@/views/products/plan/components/plan-card/BasePriceDisplay";
|
||||
import type { UseUpdateSubscriptionForm } from "../hooks/useUpdateSubscriptionForm";
|
||||
import { buildEditsForItem } from "../utils/buildEditsForItem";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
import { SubscriptionItemRow } from "./SubscriptionItemRow";
|
||||
|
||||
@@ -96,10 +99,10 @@ export function EditPlanSection({
|
||||
const isCreated =
|
||||
!originalItem && originalItems && originalItems.length > 0;
|
||||
const edits = buildEditsForItem({
|
||||
item,
|
||||
updatedItem: item,
|
||||
originalItem,
|
||||
prepaidQuantity: currentPrepaidQuantity,
|
||||
initialPrepaidQuantity,
|
||||
updatedPrepaidQuantity: currentPrepaidQuantity,
|
||||
originalPrepaidQuantity: initialPrepaidQuantity,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
getProductItemDisplay,
|
||||
type ItemEdit,
|
||||
type ProductItem,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
@@ -21,7 +22,6 @@ import { cn } from "@/lib/utils";
|
||||
import { PlanFeatureIcon } from "@/views/products/plan/components/plan-card/PlanFeatureIcon";
|
||||
import { CustomDotIcon } from "@/views/products/plan/components/plan-card/PlanFeatureRow";
|
||||
import type { UseUpdateSubscriptionForm } from "../hooks/useUpdateSubscriptionForm";
|
||||
import type { ItemEdit } from "../types/summary";
|
||||
import { getEditIcon } from "../utils/getEditIcon";
|
||||
import { CompactValueChange } from "./CompactValueChange";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
@@ -56,9 +56,23 @@ function EditRow({
|
||||
return (
|
||||
<span className="text-xs text-t3">
|
||||
{prefix}
|
||||
<span className="text-red-500 font-medium">{oldVal}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
edit.isUpgrade ? "text-red-500" : "text-green-500",
|
||||
)}
|
||||
>
|
||||
{oldVal}
|
||||
</span>
|
||||
{middle}
|
||||
<span className="text-green-500 font-medium">{newVal}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
edit.isUpgrade ? "text-green-500" : "text-red-500",
|
||||
)}
|
||||
>
|
||||
{newVal}
|
||||
</span>
|
||||
<span className="text-t4">{suffix}</span>
|
||||
</span>
|
||||
);
|
||||
@@ -78,7 +92,11 @@ function EditRow({
|
||||
{showRing ? (
|
||||
renderDescription()
|
||||
) : (
|
||||
<CompactValueChange oldValue={edit.oldValue} newValue={edit.newValue} />
|
||||
<CompactValueChange
|
||||
oldValue={edit.oldValue}
|
||||
newValue={edit.newValue}
|
||||
isUpgrade={edit.isUpgrade}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -155,6 +173,7 @@ export function SubscriptionItemRow({
|
||||
<CompactValueChange
|
||||
oldValue={singleEdit.oldValue}
|
||||
newValue={singleEdit.newValue}
|
||||
isUpgrade={singleEdit.isUpgrade}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,55 +1,72 @@
|
||||
import { formatAmount } from "@autumn/shared";
|
||||
import { CalendarIcon, GitBranchIcon, WrenchIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PlanFeatureIcon } from "@/views/products/plan/components/plan-card/PlanFeatureIcon";
|
||||
import { CustomDotIcon } from "@/views/products/plan/components/plan-card/PlanFeatureRow";
|
||||
import type { SummaryItem } from "../types/summary";
|
||||
import type { EditIconType, ItemEdit } from "@autumn/shared";
|
||||
import {
|
||||
CalendarIcon,
|
||||
CurrencyDollarIcon,
|
||||
GitBranchIcon,
|
||||
HashIcon,
|
||||
PackageIcon,
|
||||
StackIcon,
|
||||
TagIcon,
|
||||
WrenchIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export function SummaryItemRow({
|
||||
item,
|
||||
currency,
|
||||
}: {
|
||||
item: SummaryItem;
|
||||
currency: string;
|
||||
}) {
|
||||
const renderIcons = () => {
|
||||
if (item.type === "prepaid" && item.productItem) {
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-1 shrink-0">
|
||||
<PlanFeatureIcon item={item.productItem} position="left" />
|
||||
<CustomDotIcon />
|
||||
<PlanFeatureIcon item={item.productItem} position="right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === "trial") {
|
||||
function getIcon(iconType: EditIconType) {
|
||||
const iconProps = { size: 16, weight: "duotone" as const };
|
||||
switch (iconType) {
|
||||
case "trial":
|
||||
return (
|
||||
<div className="text-blue-500">
|
||||
<CalendarIcon size={16} weight="duotone" />
|
||||
<CalendarIcon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === "version") {
|
||||
case "version":
|
||||
return (
|
||||
<div className="text-purple-500">
|
||||
<GitBranchIcon size={16} weight="duotone" />
|
||||
<GitBranchIcon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === "item") {
|
||||
case "item":
|
||||
return (
|
||||
<div className="text-amber-500">
|
||||
<WrenchIcon size={16} weight="duotone" />
|
||||
<WrenchIcon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
case "prepaid":
|
||||
return (
|
||||
<div className="text-cyan-500">
|
||||
<PackageIcon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
case "price":
|
||||
return (
|
||||
<div className="text-green-500">
|
||||
<CurrencyDollarIcon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
case "tier":
|
||||
return (
|
||||
<div className="text-orange-500">
|
||||
<StackIcon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
case "usage":
|
||||
return (
|
||||
<div className="text-indigo-500">
|
||||
<HashIcon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
case "units":
|
||||
return (
|
||||
<div className="text-pink-500">
|
||||
<TagIcon {...iconProps} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function SummaryItemRow({ item }: { item: ItemEdit }) {
|
||||
const renderChangeIndicator = () => {
|
||||
if (item.newValue === null) {
|
||||
return (
|
||||
@@ -87,7 +104,7 @@ export function SummaryItemRow({
|
||||
return (
|
||||
<div className="flex items-center w-full h-10 px-3 rounded-xl input-base">
|
||||
<div className="flex flex-row items-center flex-1 gap-2 min-w-0 overflow-hidden">
|
||||
{renderIcons()}
|
||||
{getIcon(item.icon)}
|
||||
|
||||
<p className="whitespace-nowrap truncate flex-1 min-w-0 text-body">
|
||||
{item.label}
|
||||
@@ -96,23 +113,6 @@ export function SummaryItemRow({
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{renderChangeIndicator()}
|
||||
|
||||
{item.costDelta !== undefined && item.costDelta !== 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs",
|
||||
item.costDelta > 0 ? "text-t2" : "text-green-600",
|
||||
)}
|
||||
>
|
||||
{item.costDelta > 0 ? "+" : ""}
|
||||
{formatAmount({
|
||||
amount: item.costDelta,
|
||||
currency,
|
||||
minFractionDigits: 2,
|
||||
amountFormatOptions: { currencyDisplay: "narrowSymbol" },
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import type { FullCusProduct } from "@autumn/shared";
|
||||
import {
|
||||
type FullCusProduct,
|
||||
generateTrialChanges,
|
||||
generateVersionChanges,
|
||||
type ItemEdit,
|
||||
} from "@autumn/shared";
|
||||
import { useStore } from "@tanstack/react-form";
|
||||
import { useMemo } from "react";
|
||||
import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import type { UseUpdateSubscriptionForm } from "../hooks/useUpdateSubscriptionForm";
|
||||
import type { SummaryItem } from "../types/summary";
|
||||
import { generateTrialChanges } from "../utils/generateTrialChanges";
|
||||
import { generateVersionChanges } from "../utils/generateVersionChanges";
|
||||
import { SummaryItemRow } from "./SummaryItemRow";
|
||||
|
||||
interface UpdateSubscriptionSummaryProps {
|
||||
form: UseUpdateSubscriptionForm;
|
||||
customerProduct: FullCusProduct;
|
||||
currentVersion: number;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
export function UpdateSubscriptionSummary({
|
||||
form,
|
||||
customerProduct,
|
||||
currentVersion,
|
||||
currency = "usd",
|
||||
}: UpdateSubscriptionSummaryProps) {
|
||||
const formValues = useStore(form.store, (state) => state.values);
|
||||
|
||||
const changes = useMemo((): SummaryItem[] => {
|
||||
const changes = useMemo((): ItemEdit[] => {
|
||||
const trialChanges = generateTrialChanges({
|
||||
customerProduct,
|
||||
removeTrial: formValues.removeTrial,
|
||||
@@ -32,8 +32,8 @@ export function UpdateSubscriptionSummary({
|
||||
});
|
||||
|
||||
const versionChanges = generateVersionChanges({
|
||||
currentVersion,
|
||||
selectedVersion: formValues.version,
|
||||
originalVersion: currentVersion,
|
||||
updatedVersion: formValues.version,
|
||||
});
|
||||
|
||||
return [...versionChanges, ...trialChanges];
|
||||
@@ -52,7 +52,7 @@ export function UpdateSubscriptionSummary({
|
||||
<SheetSection title="Changes" withSeparator>
|
||||
<div className="space-y-2">
|
||||
{changes.map((change) => (
|
||||
<SummaryItemRow key={change.id} item={change} currency={currency} />
|
||||
<SummaryItemRow key={change.id} item={change} />
|
||||
))}
|
||||
</div>
|
||||
</SheetSection>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { Feature, FullCusProduct, ProductItem } from "@autumn/shared";
|
||||
import {
|
||||
type Feature,
|
||||
type FullCusProduct,
|
||||
generateItemChanges,
|
||||
generatePrepaidChanges,
|
||||
generateTrialChanges,
|
||||
generateVersionChanges,
|
||||
type ProductItem,
|
||||
} from "@autumn/shared";
|
||||
import { useMemo } from "react";
|
||||
import type { PrepaidItemWithFeature } from "@/hooks/stores/useProductStore";
|
||||
import type { UpdateSubscriptionForm } from "../updateSubscriptionFormSchema";
|
||||
import { generateItemChanges } from "../utils/generateItemChanges";
|
||||
import { generatePrepaidChanges } from "../utils/generatePrepaidChanges";
|
||||
import { generateTrialChanges } from "../utils/generateTrialChanges";
|
||||
import { generateVersionChanges } from "../utils/generateVersionChanges";
|
||||
|
||||
export function useHasSubscriptionChanges({
|
||||
formValues,
|
||||
@@ -35,15 +39,15 @@ export function useHasSubscriptionChanges({
|
||||
if (trialChanges.length > 0) return true;
|
||||
|
||||
const versionChanges = generateVersionChanges({
|
||||
currentVersion,
|
||||
selectedVersion: formValues.version,
|
||||
originalVersion: currentVersion,
|
||||
updatedVersion: formValues.version,
|
||||
});
|
||||
|
||||
if (versionChanges.length > 0) return true;
|
||||
|
||||
const itemChanges = generateItemChanges({
|
||||
originalItems,
|
||||
customizedItems: formValues.items,
|
||||
updatedItems: formValues.items,
|
||||
features,
|
||||
prepaidOptions: formValues.prepaidOptions,
|
||||
});
|
||||
@@ -58,8 +62,8 @@ export function useHasSubscriptionChanges({
|
||||
|
||||
const prepaidChanges = generatePrepaidChanges({
|
||||
prepaidItems,
|
||||
currentOptions: formValues.prepaidOptions,
|
||||
initialOptions: initialPrepaidOptions,
|
||||
updatedOptions: formValues.prepaidOptions,
|
||||
originalOptions: initialPrepaidOptions,
|
||||
}).filter((change) => {
|
||||
const featureId = change.id.replace("prepaid-", "");
|
||||
return !newlyAddedFeatureIds.has(featureId);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Context
|
||||
|
||||
// Components
|
||||
export * from "./components/EditPlanSection";
|
||||
export * from "./components/FreeTrialSection";
|
||||
@@ -17,10 +15,7 @@ export * from "./hooks/useUpdateSubscriptionMutation";
|
||||
export * from "./hooks/useUpdateSubscriptionRequestBody";
|
||||
|
||||
// Types
|
||||
export * from "./types/summary";
|
||||
export * from "./updateSubscriptionFormSchema";
|
||||
|
||||
// Utils
|
||||
export * from "./utils/generateItemChanges";
|
||||
export * from "./utils/generateVersionChanges";
|
||||
export * from "./utils/getFreeTrial";
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { ProductItem } from "@autumn/shared";
|
||||
|
||||
export interface SummaryItem {
|
||||
id: string;
|
||||
type: "prepaid" | "trial" | "version" | "item";
|
||||
label: string;
|
||||
oldValue: string | number | null;
|
||||
newValue: string | number | null;
|
||||
costDelta?: number;
|
||||
currency?: string;
|
||||
/** The product item for rendering icons (prepaid and item changes) */
|
||||
productItem?: ProductItem;
|
||||
}
|
||||
|
||||
export type EditIconType = "price" | "tier" | "usage" | "units" | "prepaid";
|
||||
|
||||
export interface ItemEdit {
|
||||
id: string;
|
||||
type: "config" | "prepaid";
|
||||
label: string;
|
||||
/** Icon type for the edit */
|
||||
icon: EditIconType;
|
||||
/** Full sentence description for accordion display */
|
||||
description: string;
|
||||
oldValue: string | number | null;
|
||||
newValue: string | number | null;
|
||||
/** Whether this is an upgrade (true) or downgrade (false) */
|
||||
isUpgrade: boolean;
|
||||
/** Whether this edit has an inline editor (prepaid quantity) */
|
||||
editable?: boolean;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { PrepaidItemWithFeature } from "@/hooks/stores/useProductStore";
|
||||
import type { SummaryItem } from "../types/summary";
|
||||
|
||||
export function generatePrepaidChanges({
|
||||
prepaidItems,
|
||||
currentOptions,
|
||||
initialOptions,
|
||||
currency,
|
||||
}: {
|
||||
prepaidItems: PrepaidItemWithFeature[];
|
||||
currentOptions: Record<string, number>;
|
||||
initialOptions: Record<string, number>;
|
||||
currency?: string;
|
||||
}): SummaryItem[] {
|
||||
return prepaidItems
|
||||
.map((item) => {
|
||||
const featureId = item.feature_id ?? "";
|
||||
const oldQuantity = initialOptions[featureId] ?? 0;
|
||||
const newQuantity = currentOptions[featureId] ?? 0;
|
||||
|
||||
if (oldQuantity === newQuantity) return null;
|
||||
|
||||
const billingUnits = item.billing_units ?? 1;
|
||||
const oldDisplayQuantity = oldQuantity * billingUnits;
|
||||
const newDisplayQuantity = newQuantity * billingUnits;
|
||||
|
||||
const unitPrice = item.price ?? null;
|
||||
const costDelta =
|
||||
unitPrice !== null
|
||||
? (newQuantity - oldQuantity) * unitPrice
|
||||
: undefined;
|
||||
|
||||
const featureName = item.feature?.name ?? "Items";
|
||||
|
||||
return {
|
||||
id: `prepaid-${featureId}`,
|
||||
type: "prepaid" as const,
|
||||
label: featureName,
|
||||
oldValue: oldDisplayQuantity,
|
||||
newValue: newDisplayQuantity,
|
||||
costDelta,
|
||||
currency,
|
||||
productItem: item,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as SummaryItem[];
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { SummaryItem } from "../types/summary";
|
||||
|
||||
export function generateVersionChanges({
|
||||
currentVersion,
|
||||
selectedVersion,
|
||||
}: {
|
||||
currentVersion: number;
|
||||
selectedVersion: number;
|
||||
}): SummaryItem[] {
|
||||
if (selectedVersion === currentVersion) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: "version-change",
|
||||
type: "version",
|
||||
label: "Plan Version",
|
||||
oldValue: currentVersion,
|
||||
newValue: selectedVersion,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { EditIconType } from "@autumn/shared";
|
||||
import {
|
||||
CurrencyDollarIcon,
|
||||
HashIcon,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
TagIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { EditIconType } from "../types/summary";
|
||||
|
||||
export function getEditIcon(iconType: EditIconType, isUpgrade: boolean) {
|
||||
const iconProps = {
|
||||
|
||||
@@ -259,7 +259,6 @@ function SheetContent({
|
||||
form={form}
|
||||
customerProduct={customerProduct}
|
||||
currentVersion={currentVersion}
|
||||
currency={previewQuery.data?.currency}
|
||||
/>
|
||||
|
||||
<UpdateSubscriptionPreviewSection
|
||||
|
||||
Reference in New Issue
Block a user