Files
cfw-autumn/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts
2026-02-25 19:39:10 +00:00

219 lines
5.4 KiB
TypeScript

import {
type AttachReplaceable,
BillingType,
type FullCusProduct,
type FullCustomerEntitlement,
type FullEntitlement,
getFeatureInvoiceDescription,
type LegacyPreviewLineItem,
type Price,
shouldProrate,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { findCusEnt } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import {
getExistingUsageFromCusProducts,
getRelatedCusPrice,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { newPriceToInvoiceDescription } from "@/internal/invoices/invoiceFormatUtils.js";
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 {
getBillingType,
getPriceEntitlement,
getPriceForOverage,
} from "@/internal/products/prices/priceUtils.js";
import { attachParamsToProduct } from "../convertAttachParams.js";
import { intervalsAreSame } from "../getAttachConfig.js";
import { priceToContUseItem } from "./priceToContUseItem.js";
const getContUseNewItems = async ({
price,
ent,
attachParams,
prevCusEnt,
}: {
price: Price;
ent: FullEntitlement;
attachParams: AttachParams;
prevCusEnt?: FullCustomerEntitlement;
}) => {
const { org, features } = attachParams;
const newProduct = attachParamsToProduct({ attachParams });
const intervalsSame = intervalsAreSame({ attachParams });
let usage = getExistingUsageFromCusProducts({
entitlement: ent,
cusProducts: attachParams.cusProducts,
entities: attachParams.entities,
carryExistingUsages: undefined,
internalEntityId: attachParams.internalEntityId,
});
const description = newPriceToInvoiceDescription({
org,
price,
product: newProduct,
quantity: usage,
});
if (usage === 0) {
return {
price_id: price.id,
price: getDefaultPriceStr({ org, price, ent, features }),
amount: undefined,
description,
usage_model: priceToUsageModel(price),
feature_id: ent.feature_id,
} as LegacyPreviewLineItem;
} else {
/*
For example, free plan comes with 3 users, and usage is 3
- Pro plan is 0 free, $10 per user
- We need to calculate usage for the pro plan, and then charge for that
---
- When allowance increases though, how do we handle it? Don't allow negative overage.
*/
// let overage = new Decimal(usage).sub(ent.allowance!).toNumber();
let overage = Math.max(
new Decimal(usage).sub(ent.allowance!).toNumber(),
0,
);
if (
intervalsSame &&
prevCusEnt &&
!shouldProrate(price.proration_config?.on_decrease)
) {
const isDowngrade = ent.allowance! > prevCusEnt.entitlement.allowance!;
const prevBalance = prevCusEnt.balance!;
if (isDowngrade && prevBalance < 0) {
overage = new Decimal(prevBalance).abs().toNumber();
usage = ent.allowance! - prevBalance;
}
}
const amount = getPriceForOverage({ price, overage });
const description = getFeatureInvoiceDescription({
feature: ent.feature,
usage: usage,
prodName: newProduct.name,
});
return {
price_id: price.id,
price: "",
description,
amount,
usage_model: priceToUsageModel(price),
feature_id: ent.feature_id,
} as LegacyPreviewLineItem;
}
};
export const getContUseInvoiceItems = async ({
cusProduct,
sub,
attachParams,
logger,
}: {
cusProduct?: FullCusProduct;
sub?: Stripe.Subscription;
attachParams: AttachParams;
logger: any;
}) => {
const cusPrices = cusProduct ? cusProduct.customer_prices : [];
const cusEnts = cusProduct ? cusProduct.customer_entitlements : [];
const product = attachParamsToProduct({ attachParams });
const curItems = sub
? await getCurContUseItems({
sub,
attachParams,
})
: [];
const newEnts = product.entitlements;
const oldItems: LegacyPreviewLineItem[] = [];
const newItems: LegacyPreviewLineItem[] = [];
const replaceables: AttachReplaceable[] = [];
for (const price of product.prices) {
const billingType = getBillingType(price.config);
if (billingType !== BillingType.InArrearProrated) {
continue;
}
const ent = getPriceEntitlement(price, newEnts);
const prevCusEnt = findCusEnt({
cusEnts,
feature: ent.feature,
});
const prevCusPrice = prevCusEnt
? getRelatedCusPrice(prevCusEnt, cusPrices)!
: undefined;
const curItem = curItems.find(
(item) => item.price_id === prevCusPrice?.price.id,
);
// TO DELETE
if (!prevCusEnt || !sub || !curItem) {
const newItem = await getContUseNewItems({
price,
ent,
attachParams,
prevCusEnt,
});
// const prevItem = curItems.find(
// (item) => item.price_id === prevCusPrice?.price.id,
// );
newItems.push(newItem);
if (curItem) {
oldItems.push(curItem);
}
continue;
}
const {
oldItem,
newItems: newItems_,
replaceables: replaceables_,
} = await priceToContUseItem({
price,
ent,
prevCusEnt,
attachParams,
sub,
logger,
curItem: curItem!,
});
if (oldItem) {
oldItems.push(oldItem);
}
newItems.push(...newItems_.filter((item) => item.amount !== 0));
replaceables.push(...replaceables_);
}
// console.log("Replaceables:", replaceables);
return {
oldItems,
newItems,
replaceables,
};
};