chore: minor cleanup in update subscription quantity utils

This commit is contained in:
Charlie Lamb
2025-12-23 14:51:16 +00:00
parent 15a6dd7a2c
commit ffd3291095
20 changed files with 120 additions and 60 deletions

View File

@@ -13,8 +13,8 @@ import type { QuantityUpdateDetails } from "@/internal/billing/v2/typesOld";
import { calculateEntitlementChange } from "./quantityUpdateUtils/calculateEntitlementChange";
import { calculateProrationAmount } from "./quantityUpdateUtils/calculateProrationAmount";
import { calculateQuantityDifferences } from "./quantityUpdateUtils/calculateQuantityDifferences";
import { extractPriceConfig } from "./quantityUpdateUtils/extractPriceConfig";
import { mapStripeSubscriptionItem } from "./quantityUpdateUtils/mapStripeSubscriptionItem";
import { resolvePriceForQuantityUpdate } from "./quantityUpdateUtils/resolvePriceForQuantityUpdate";
/**
* Computes all details needed for a feature quantity update operation.
@@ -46,7 +46,7 @@ export const computeQuantityUpdateDetails = ({
stripeSubscription: Stripe.Subscription;
currentEpochMs: number;
}): QuantityUpdateDetails => {
const { features, org } = ctx;
const { features } = ctx;
const internalFeatureId = updatedOptions.internal_feature_id;
const featureId = updatedOptions.feature_id;
@@ -73,7 +73,7 @@ export const computeQuantityUpdateDetails = ({
updatedOptions,
});
const priceConfiguration = extractPriceConfig({
const priceConfiguration = resolvePriceForQuantityUpdate({
customerProduct,
updatedOptions,
isUpgrade: quantityDifferences.isUpgrade,

View File

@@ -67,7 +67,7 @@ export const computeSubscriptionUpdateNewCustomerProduct = ({
initOptions: {
isCustom: true,
subscriptionId: stripeSubscription?.id,
subscriptionId: stripeSubscription.id,
subscriptionScheduleId: stripeSubscriptionSchedule?.id,
},
});

View File

@@ -1,4 +1,5 @@
import {
findFeatureOptionsByFeature,
InternalError,
OngoingCusProductActionEnum,
type SubscriptionUpdateV0Params,
@@ -42,16 +43,10 @@ export const computeSubscriptionUpdateQuantityPlan = ({
const currentEpochMs = testClockFrozenTime || Date.now();
const quantityUpdateDetails = featureQuantities.new.map((updatedOption) => {
const previousOption = featureQuantities.old.find(
(oldOption) => oldOption.feature_id === updatedOption.feature_id,
);
if (!previousOption) {
throw new Error(
`[Subscription Update] Cannot find previous options for feature: ${updatedOption.feature_id}. ` +
`This feature may not exist in the current subscription.`,
);
}
const previousOption = findFeatureOptionsByFeature({
featureOptions: customerProduct.options,
featureId: updatedOption.feature_id,
});
return computeQuantityUpdateDetails({
ctx,

View File

@@ -4,15 +4,12 @@ import {
type FullCustomerPrice,
findCusPriceByFeature,
InternalError,
OnDecrease,
OnIncrease,
type OnDecrease,
type OnIncrease,
type Price,
priceToProrationConfig,
type UsagePriceConfig,
} from "@autumn/shared";
import {
shouldBillNow,
shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils";
/**
* Extracts and validates price configuration for a quantity update.
@@ -25,7 +22,7 @@ import {
* @returns Price config including proration rules and billing units
* @throws {InternalError} When internal_feature_id is missing or price not found
*/
export const extractPriceConfig = ({
export const resolvePriceForQuantityUpdate = ({
customerProduct,
updatedOptions,
isUpgrade,
@@ -61,16 +58,16 @@ export const extractPriceConfig = ({
const price = customerPrice.price;
const priceConfig = price.config as UsagePriceConfig;
const billingUnitsPerQuantity = priceConfig.billing_units || 1;
const billingUnitsPerQuantity = priceConfig.billing_units ?? 1;
const prorationBehaviorConfig = isUpgrade
? price.proration_config?.on_increase || OnIncrease.ProrateImmediately
: price.proration_config?.on_decrease || OnDecrease.ProrateImmediately;
const shouldApplyProration = shouldProrate(prorationBehaviorConfig);
const shouldFinalizeInvoiceImmediately = shouldBillNow(
const {
prorationBehaviorConfig,
);
shouldApplyProration,
shouldFinalizeInvoiceImmediately,
} = priceToProrationConfig({
price,
isUpgrade,
});
return {
customerPrice,

View File

@@ -41,7 +41,7 @@ export const executeSubscriptionUpdate = async ({
if (shouldUncancelSubscription) {
await executeStripeSubscriptionUncancel({
ctx,
stripeSubscriptionId: stripeSubscription?.id ?? "",
stripeSubscriptionId: stripeSubscription.id,
customerProduct,
});
}
@@ -58,7 +58,7 @@ export const executeSubscriptionUpdate = async ({
ctx,
invoiceAction,
stripeCustomerId: stripeCustomer.id,
stripeSubscriptionId: stripeSubscription?.id ?? "",
stripeSubscriptionId: stripeSubscription.id,
customerProduct,
});
} else {

View File

@@ -5,7 +5,7 @@ export type UpdateSubscriptionContext = {
fullCustomer: FullCustomer;
// product: FullProduct;
customerProduct: FullCusProduct;
stripeSubscription?: Stripe.Subscription;
stripeSubscription: Stripe.Subscription;
stripeSubscriptionSchedule?: Stripe.SubscriptionSchedule;
stripeCustomer: Stripe.Customer;
paymentMethod?: Stripe.PaymentMethod;

View File

@@ -9,6 +9,8 @@ import {
getFeatureInvoiceDescription,
OnDecrease,
priceToInvoiceAmount,
shouldBillNow,
shouldProrate,
type UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
@@ -21,10 +23,6 @@ import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import {
shouldBillNow,
shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";

View File

@@ -9,6 +9,8 @@ import {
getFeatureInvoiceDescription,
OnIncrease,
priceToInvoiceAmount,
shouldBillNow,
shouldProrate,
type UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
@@ -21,10 +23,6 @@ import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import {
shouldBillNow,
shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";

View File

@@ -7,6 +7,7 @@ import {
getFeatureInvoiceDescription,
type PreviewLineItem,
type Price,
shouldProrate,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
@@ -20,7 +21,6 @@ import { newPriceToInvoiceDescription } from "@/internal/invoices/invoiceFormatU
import { getCurContUseItems } from "@/internal/invoices/previewItemUtils/getCurContUseItems.js";
import { getDefaultPriceStr } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { priceToUsageModel } from "@/internal/products/prices/priceUtils/convertPrice.js";
import { shouldProrate } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import {
getBillingType,
getPriceEntitlement,

View File

@@ -4,6 +4,7 @@ import {
type FullEntitlement,
type PreviewLineItem,
type Price,
shouldProrate,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
@@ -15,7 +16,6 @@ import {
getRelatedCusPrice,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { priceToInvoiceItem } from "@/internal/products/prices/priceUtils/priceToInvoiceItem.js";
import { shouldProrate } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { attachParamsToProduct } from "../convertAttachParams.js";
import { getContUseDowngradeItems } from "./getContUseDowngradeItems.js";

View File

@@ -1,15 +1,15 @@
import { constructPreviewItem } from "@/internal/invoices/previewItemUtils/constructPreviewItem.js";
import {
FullEntitlement,
type FullEntitlement,
getFeatureInvoiceDescription,
Organization,
Price,
Product,
UsagePriceConfig,
type Organization,
type Price,
type Product,
shouldProrate,
type UsagePriceConfig,
usageToFeatureName,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import { shouldProrate } from "../prorationConfigUtils.js";
import { constructPreviewItem } from "@/internal/invoices/previewItemUtils/constructPreviewItem.js";
export const getUsageDiffLineItem = ({
prevBalance,
@@ -54,7 +54,7 @@ export const getUsageDiffLineItem = ({
});
if (!isIncrease && !willProrate) {
let featureName = usageToFeatureName({
const featureName = usageToFeatureName({
usage: usageDiff,
feature: ent.feature,
});

View File

@@ -18,6 +18,7 @@ import {
type Price,
PriceType,
type ProductItem,
shouldProrate,
TierInfinite,
UsageModel,
type UsagePriceConfig,
@@ -25,7 +26,6 @@ import {
import { entsAreSame } from "@server/internal/products/entitlements/entitlementUtils";
import { pricesAreSame } from "@server/internal/products/prices/priceInitUtils";
import { getBillingType } from "@server/internal/products/prices/priceUtils";
import { shouldProrate } from "@server/internal/products/prices/priceUtils/prorationConfigUtils";
import { itemCanBeProrated } from "@server/internal/products/product-items/productItemUtils/classifyItem";
import RecaseError from "@server/utils/errorUtils";
import { generateId, notNullish, nullish } from "@server/utils/genUtils";

View File

@@ -6,6 +6,8 @@ import {
type Organization,
type Price,
type Product,
shouldBillNow,
shouldProrate,
type UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
@@ -14,10 +16,6 @@ import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import {
shouldBillNow,
shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
import type { Logger } from "../../external/logtail/logtailUtils";

View File

@@ -9,6 +9,8 @@ import {
type Organization,
type Product,
priceToInvoiceAmount,
shouldProrate,
shouldProrateDowngradeNow,
type UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
@@ -19,10 +21,6 @@ import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { getReplaceables } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getContUsageDowngradeItem.js";
import {
shouldProrate,
shouldProrateDowngradeNow,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { roundUsage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";

View File

@@ -6,12 +6,12 @@ import {
type Organization,
type Price,
priceToInvoiceAmount,
shouldCreateInvoiceItem,
type UsagePriceConfig,
} from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
import { shouldCreateInvoiceItem } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { roundUsage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { getUsageFromBalance } from "../adjustAllowance.js";
import { createUpgradeProrationInvoice } from "./createUpgradeProrationInvoice.js";

View File

@@ -14,4 +14,5 @@ export * from "./invoicingUtils/lineItemBuilders/usagePriceToLineItem";
export * from "./invoicingUtils/lineItemUtils/priceToLineAmount";
export * from "./invoicingUtils/lineItemUtils/tiersToLineAmount";
export * from "./invoicingUtils/prorationUtils/applyProration";
export * from "./invoicingUtils/prorationUtils/prorationConfigUtils";
export * from "./usageUtils/roundUsageToNearestBillingUnit";

View File

@@ -1,5 +1,10 @@
import { OnDecrease, OnIncrease } from "@autumn/shared";
/**
* Determines if an invoice item should be created for a quantity increase.
* @param onIncrease - The proration behavior configuration for the quantity increase.
* @returns True if an invoice item should be created, false otherwise.
*/
export const shouldCreateInvoiceItem = (onIncrease: OnIncrease) => {
return (
onIncrease === OnIncrease.BillImmediately ||
@@ -8,6 +13,11 @@ export const shouldCreateInvoiceItem = (onIncrease: OnIncrease) => {
);
};
/**
* Determines if an invoice item should be billed immediately.
* @param onIncrease - The proration behavior configuration for the quantity increase.
* @returns True if an invoice item should be billed immediately, false otherwise.
*/
export const shouldBillNow = (onIncrease: OnIncrease | OnDecrease) => {
return (
onIncrease === OnIncrease.BillImmediately ||
@@ -16,6 +26,12 @@ export const shouldBillNow = (onIncrease: OnIncrease | OnDecrease) => {
);
};
/**
* Determines if a quantity downgrade should be prorated.
* @param onIncrease - The proration behavior configuration for the quantity increase.
* @param onDecrease - The proration behavior configuration for the quantity decrease.
* @returns True if a quantity downgrade should be prorated, false otherwise.
*/
export const shouldProrateDowngradeNow = ({
onIncrease,
onDecrease,
@@ -36,6 +52,11 @@ export const shouldProrateDowngradeNow = ({
);
};
/**
* Determines if a quantity upgrade or downgrade should be prorated.
* @param onIncrease - The proration behavior configuration for the quantity increase.
* @returns True if a quantity upgrade or downgrade should be prorated, false otherwise.
*/
export const shouldProrate = (onIncrease?: OnIncrease | OnDecrease) => {
if (!onIncrease) {
return true;

View File

@@ -0,0 +1,21 @@
import { InternalError } from "@api/errors";
import type { FeatureOptions } from "@models/cusProductModels/cusProductModels";
export const findFeatureOptionsByFeature = ({
featureOptions,
featureId,
}: {
featureOptions: FeatureOptions[];
featureId: string;
}) => {
const previousOption = featureOptions.find(
(oldOption) => oldOption.feature_id === featureId,
);
if (!previousOption) {
throw new InternalError({
message: `[Find Feature Options By Feature] Cannot find feature options for feature: ${featureId}.`,
});
}
return previousOption;
};

View File

@@ -40,6 +40,7 @@ export * from "./cusProductUtils/cusProductConstants.js";
export * from "./cusProductUtils/cusProductUtils.js";
export * from "./cusProductUtils/filterCusProductUtils.js";
export * from "./cusProductUtils/filterCusProductUtils.js";
export * from "./cusProductUtils/findFeatureOptionsByFeature.js";
export * from "./cusProductUtils/getCusProductFromCustomer.js";
export * from "./cusProductUtils/productIdToCusProduct.js";
// Cus utils

View File

@@ -2,6 +2,11 @@ import type { Feature } from "@models/featureModels/featureModels";
import type { EntitlementWithFeature } from "@models/productModels/entModels/entModels";
import type { UsagePriceConfig } from "@models/productModels/priceModels/priceConfig/usagePriceConfig";
import type { Price } from "@models/productModels/priceModels/priceModels";
import {
OnDecrease,
OnIncrease,
} from "@models/productV2Models/productItemModels/productItemEnums";
import { shouldBillNow, shouldProrate } from "@utils/billingUtils";
import { priceToEnt } from "@utils/productUtils/convertProductUtils";
export const priceToFeature = ({
@@ -28,3 +33,30 @@ export const priceToFeature = ({
const ent = priceToEnt({ price, entitlements: ents ?? [] });
return ent?.feature;
};
export const priceToProrationConfig = ({
price,
isUpgrade,
}: {
price: Price;
isUpgrade: boolean;
}): {
prorationBehaviorConfig: OnIncrease | OnDecrease;
shouldApplyProration: boolean;
shouldFinalizeInvoiceImmediately: boolean;
} => {
const prorationBehaviorConfig = isUpgrade
? (price.proration_config?.on_increase ?? OnIncrease.ProrateImmediately)
: (price.proration_config?.on_decrease ?? OnDecrease.ProrateImmediately);
const shouldApplyProration = shouldProrate(prorationBehaviorConfig);
const shouldFinalizeInvoiceImmediately = shouldBillNow(
prorationBehaviorConfig,
);
return {
prorationBehaviorConfig,
shouldApplyProration,
shouldFinalizeInvoiceImmediately,
};
};