fixing combined interval stuff

This commit is contained in:
John Yeo
2025-08-16 07:17:52 -07:00
parent f3ef6e1596
commit 42c3e73334
136 changed files with 3170 additions and 2043 deletions

View File

@@ -47,7 +47,7 @@
"@supabase/supabase-js": "^2.46.2",
"@upstash/redis": "^1.35.1",
"ai": "^4.3.10",
"autumn-js": "^0.1.4",
"autumn-js": "^0.1.8",
"axios": "^1.8.3",
"better-auth": "^1.2.9",
"body-parser": "^1.20.3",
@@ -85,7 +85,7 @@
"react": "^18.2.0",
"recaseai": "^0.0.37",
"resend": "^4.1.1",
"stripe": "^17.5.0",
"stripe": "18.4.0",
"svix": "^1.45.1",
"tsc-alias": "^1.8.16",
"ws": "^8.18.0",
@@ -1059,7 +1059,7 @@
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
"@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="],
@@ -2475,7 +2475,7 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"stripe": ["stripe@17.7.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw=="],
"stripe": ["stripe@18.4.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-LKFeDnDYo4U/YzNgx2Lc9PT9XgKN0JNF1iQwZxgkS4lOw5NunWCnzyH5RhTlD3clIZnf54h7nyMWkS8VXPmtTQ=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
@@ -2695,8 +2695,6 @@
"@autumn/vite/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
"@autumn/vite/stripe": ["stripe@18.4.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-LKFeDnDYo4U/YzNgx2Lc9PT9XgKN0JNF1iQwZxgkS4lOw5NunWCnzyH5RhTlD3clIZnf54h7nyMWkS8VXPmtTQ=="],
"@autumn/vite/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"@axiomhq/js/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
@@ -3035,6 +3033,8 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
@@ -3175,8 +3175,6 @@
"@autumn/vite/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"@autumn/vite/stripe/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@browserbasehq/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],

View File

@@ -86,7 +86,7 @@
"react": "^18.2.0",
"recaseai": "^0.0.37",
"resend": "^4.1.1",
"stripe": "^17.5.0",
"stripe": "^18.4.0",
"svix": "^1.45.1",
"tsc-alias": "^1.8.16",
"ws": "^8.18.0",

View File

@@ -8,15 +8,13 @@ if [[ "$1" == *"setup"* ]]; then
MOCHA_PARALLEL=true $MOCHA_SETUP
fi
$MOCHA_CMD \
'tests/attach/basic/*.ts' \
'tests/attach/upgrade/*.ts' \
'tests/attach/downgrade/*.ts' \
'tests/attach/addOn/*.ts'
# $MOCHA_CMD \
# 'tests/attach/basic/*.ts' \
# 'tests/attach/upgrade/*.ts' \
# 'tests/attach/downgrade/*.ts' \
# 'tests/attach/addOn/*.ts'
$MOCHA_CMD \
'tests/attach/checkout/*.ts' \
'tests/attach/entities/*.ts' \
'tests/attach/free/*.ts'\
# 'tests/attach/basic/basic2.ts' \

View File

@@ -23,6 +23,7 @@ import {
} from "./createStripeArrearProrated.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
export const checkCurStripePrice = async ({
price,
@@ -98,7 +99,6 @@ export const createStripePriceIFNotExist = async ({
const billingType = getBillingType(price.config!);
// let config = price.config! as UsagePriceConfig;
let { stripePrice, stripeProd } = await checkCurStripePrice({
price,
stripeCli,
@@ -117,12 +117,6 @@ export const createStripePriceIFNotExist = async ({
billingType == BillingType.OneOff
) {
if (!stripePrice) {
// logger.info("Creating stripe fixed price: ", {
// data: {
// price,
// stripePrice,
// },
// });
await createStripeFixedPrice({
db,
stripeCli,
@@ -205,5 +199,28 @@ export const createStripePriceIFNotExist = async ({
internalEntityId,
useCheckout,
});
if (!config.stripe_empty_price_id) {
logger.info(`Creating stripe empty price`);
const emptyPrice = await stripeCli.prices.create({
// product: stripeProd!.id,
product: config.stripe_product_id || stripeProd?.id,
unit_amount: 0,
currency: org.default_currency || "usd",
recurring: {
...(billingIntervalToStripe({
interval: price.config!.interval!,
intervalCount: price.config!.interval_count!,
}) as any),
},
});
config.stripe_empty_price_id = emptyPrice.id;
await PriceService.update({
db,
id: price.id!,
update: { config },
});
}
}
};

View File

@@ -20,6 +20,30 @@ import {
priceToUsageInAdvance,
} from "./priceToUsageInAdvance.js";
import { priceToInArrearProrated } from "./priceToArrearProrated.js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
export const getEmptyPriceItem = ({
price,
org,
}: {
price: Price;
org: Organization;
}) => {
return {
price_data: {
product: price.config!.stripe_product_id!,
unit_amount: 0,
currency: org.default_currency || "usd",
recurring: {
...billingIntervalToStripe({
interval: price.config!.interval!,
intervalCount: price.config!.interval_count!,
}),
},
},
quantity: 1,
};
};
// GET STRIPE LINE / SUB ITEM
export const priceToStripeItem = ({
@@ -101,11 +125,22 @@ export const priceToStripeItem = ({
const priceId = config.stripe_price_id;
if (withEntity && !isCheckout) {
return null;
return {
lineItem: {
price: config.stripe_empty_price_id,
quantity: 1,
},
};
}
if (apiVersion === APIVersion.v1_4 && !isCheckout) {
return null;
return {
lineItem: {
// lineItem: getEmptyPriceItem({ price, org }),
price: config.stripe_empty_price_id,
quantity: 1,
},
};
}
if (!priceId) {

View File

@@ -29,9 +29,15 @@ export const getFullStripeInvoice = async ({
const invoice = await stripeCli.invoices.retrieve(stripeId, {
expand: ["discounts", "discounts.coupon"],
});
return invoice;
};
export const invoiceToSubId = ({ invoice }: { invoice: Stripe.Invoice }) => {
const subId = invoice.parent?.subscription_details?.subscription;
return subId as string | undefined;
};
export const payForInvoice = async ({
stripeCli,
paymentMethod,
@@ -88,7 +94,7 @@ export const payForInvoice = async ({
};
} catch (error: any) {
logger.error(
`❌ Stripe error: Failed to pay invoice: ${error?.message || error}`,
`❌ Stripe error: Failed to pay invoice: ${error?.message || error}`
);
if (voidIfFailed) {
@@ -143,13 +149,13 @@ export const updateInvoiceIfExists = async ({
// TODO: Can optimize this function...
const existingInvoice = await InvoiceService.getByStripeId({
db,
stripeId: invoice.id,
stripeId: invoice.id!,
});
if (existingInvoice) {
await InvoiceService.updateByStripeId({
db,
stripeId: invoice.id,
stripeId: invoice.id!,
updates: {
status: invoice.status as InvoiceStatus,
hosted_invoice_url: invoice.hosted_invoice_url,
@@ -181,7 +187,7 @@ export const getInvoiceDiscounts = ({
let autumnDiscounts = expandedInvoice.discounts.map((discount: any) => {
const amountOff = discount.coupon.amount_off;
const amountUsed = totalDiscountAmounts?.find(
(item) => item.discount === discount.id,
(item) => item.discount === discount.id
)?.amount;
let autumnDiscount: InvoiceDiscount = {

View File

@@ -11,8 +11,8 @@ import {
import { differenceInSeconds } from "date-fns";
// import { ProrationBehavior } from "@/internal/customers/change-product/handleUpgrade.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { stripeToAutumnInterval } from "./utils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getEarliestPeriodEnd } from "./stripeSubUtils/convertSubUtils.js";
export const getFullStripeSub = async ({
stripeCli,
@@ -252,10 +252,9 @@ export const subIsPrematurelyCanceled = (sub: Stripe.Subscription) => {
return false;
}
return (
differenceInSeconds(sub.current_period_end * 1000, sub.cancel_at! * 1000) >
20
);
const periodEnd = getEarliestPeriodEnd({ sub });
return differenceInSeconds(periodEnd * 1000, sub.cancel_at! * 1000) > 20;
};
export const autumnToStripeProrationBehavior = ({

View File

@@ -0,0 +1,49 @@
import Stripe from "stripe";
export const getLatestPeriodEnd = ({ sub }: { sub: Stripe.Subscription }) => {
if (sub.items.data.length == 0) {
return Date.now();
}
return sub.items.data.reduce((acc, item) => {
return Math.max(acc, item.current_period_end);
}, sub.items.data[0].current_period_end);
};
export const getEarliestPeriodEnd = ({ sub }: { sub: Stripe.Subscription }) => {
if (sub.items.data.length == 0) {
return Date.now();
}
return sub.items.data.reduce((acc, item) => {
return Math.min(acc, item.current_period_end);
}, sub.items.data[0].current_period_end);
};
export const getEarliestPeriodStart = ({
sub,
}: {
sub: Stripe.Subscription;
}) => {
if (sub.items.data.length == 0) {
return Date.now();
}
return sub.items.data.reduce((acc, item) => {
return Math.min(acc, item.current_period_start);
}, sub.items.data[0].current_period_start);
};
export const subToPeriodStartEnd = ({ sub }: { sub?: Stripe.Subscription }) => {
if (!sub || sub.items.data.length == 0) {
return {
start: Date.now(),
end: Date.now(),
};
}
return {
start: getEarliestPeriodStart({ sub }),
end: getEarliestPeriodEnd({ sub }),
};
};

View File

@@ -15,6 +15,7 @@ import { formatUnixToDateTime, generateId } from "@/utils/genUtils.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getAlignedIntervalUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { getEarliestPeriodEnd } from "./convertSubUtils.js";
// Get payment method
@@ -67,31 +68,34 @@ export const createStripeSub = async ({
const { items, prices, usageFeatures } = itemSet;
let subItems = items.filter(
(i: any, index: number) =>
prices[index].config!.interval !== BillingInterval.OneOff
);
// let subItems = items.filter(
// (i: any, index: number) =>
// prices[index].config!.interval !== BillingInterval.OneOff
// );
let invoiceItems = items.filter(
(i: any, index: number) =>
prices[index].config!.interval === BillingInterval.OneOff
);
// let invoiceItems = items.filter(
// (i: any, index: number) =>
// prices[index].config!.interval === BillingInterval.OneOff
// );
try {
const subscription = await stripeCli.subscriptions.create({
...paymentMethodData,
customer: customer.processor.id,
items: subItems as any,
items: items as any,
// items: subItems as any,
billing_mode: { type: "flexible" },
trial_end: freeTrialToStripeTimestamp({ freeTrial, now }),
payment_behavior: "error_if_incomplete",
add_invoice_items: invoiceItems,
// add_invoice_items: invoiceItems,
collection_method: invoiceOnly ? "send_invoice" : "charge_automatically",
days_until_due: invoiceOnly ? 30 : undefined,
billing_cycle_anchor: billingCycleAnchorUnix
? Math.floor(billingCycleAnchorUnix / 1000)
: undefined,
coupon: reward ? reward.id : undefined,
// coupon: reward ? reward.id : undefined,
discounts: reward ? [{ coupon: reward.id }] : undefined,
expand: ["latest_invoice"],
});
@@ -101,11 +105,13 @@ export const createStripeSub = async ({
(subscription.latest_invoice as Stripe.Invoice).status === "draft"
) {
subscription.latest_invoice = await stripeCli.invoices.finalizeInvoice(
(subscription.latest_invoice as Stripe.Invoice).id
(subscription.latest_invoice as Stripe.Invoice).id!
);
}
// Store
const earliestPeriodEnd = getEarliestPeriodEnd({ sub: subscription });
await SubService.createSub({
db,
sub: {
@@ -116,8 +122,8 @@ export const createStripeSub = async ({
usage_features: usageFeatures,
org_id: org.id,
env: customer.env,
current_period_start: subscription.current_period_start,
current_period_end: subscription.current_period_end,
current_period_start: earliestPeriodEnd,
current_period_end: earliestPeriodEnd,
},
});

View File

@@ -18,6 +18,7 @@ import {
InsertReplaceable,
AttachReplaceable,
ErrCode,
AttachConfig,
} from "@autumn/shared";
import { priceToStripeItem } from "../priceToStripeItem/priceToStripeItem.js";
import { getArrearItems } from "./getStripeSubItems/getArrearItems.js";
@@ -32,6 +33,7 @@ import {
intervalKeyToPrice,
priceToIntervalKey,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
const getIntervalToPrices = (prices: Price[]) => {
const intervalToPrices: Record<string, Price[]> = {};
@@ -209,3 +211,94 @@ export const getStripeSubItems = async ({
return itemSets;
};
export const getStripeSubItems2 = async ({
attachParams,
config,
}: {
attachParams: AttachParams;
config: AttachConfig;
}) => {
const {
prices,
entitlements,
optionsList,
cusProducts,
customer,
internalEntityId,
apiVersion,
} = attachParams;
const subItems: any[] = [];
const invoiceItems: any[] = [];
const usageFeatures: any[] = [];
for (const price of prices) {
const priceEnt = getPriceEntitlement(price, entitlements);
const options = getEntOptions(optionsList, priceEnt);
let existingUsage = getExistingUsageFromCusProducts({
entitlement: priceEnt,
cusProducts,
entities: customer.entities,
carryExistingUsages: config.carryUsage,
internalEntityId,
});
let replaceables = priceEnt
? attachParams.replaceables.filter((r) => r.ent.id === priceEnt.id)
: [];
existingUsage += replaceables.length;
let product = getProductForPrice(price, attachParams.products)!;
if (!product) {
logger.error(
`Couldn't find product for price ${price.internal_product_id}`,
{
data: {
products: attachParams.products,
price,
},
}
);
throw new RecaseError({
code: ErrCode.ProductNotFound,
message: `Price internal product ID: ${price.internal_product_id} not found in products`,
statusCode: 400,
});
}
const stripeItem = priceToStripeItem({
price,
product,
org: attachParams.org,
options,
isCheckout: config.onlyCheckout,
relatedEnt: priceEnt,
existingUsage,
withEntity: notNullish(internalEntityId),
apiVersion: attachParams.apiVersion,
});
if (isUsagePrice({ price })) {
usageFeatures.push(priceEnt.feature.internal_id);
}
if (!stripeItem) {
continue;
}
const { lineItem } = stripeItem;
// subItems.push(lineItem);
if (price.config.interval === BillingInterval.OneOff) {
invoiceItems.push(lineItem);
} else {
subItems.push(lineItem);
}
}
return { subItems, invoiceItems, usageFeatures };
};

View File

@@ -25,6 +25,7 @@ const autumnStripePricesMatch = ({
);
};
// TO FIX
export const findStripeItemForPrice = ({
price,
stripeItems,
@@ -39,7 +40,7 @@ export const findStripeItemForPrice = ({
}) => {
return stripeItems.find(
(
si: Stripe.SubscriptionItem | Stripe.InvoiceLineItem | Stripe.LineItem,
si: Stripe.SubscriptionItem | Stripe.InvoiceLineItem | Stripe.LineItem
) => {
const config = price.config as UsagePriceConfig;
@@ -54,28 +55,37 @@ export const findStripeItemForPrice = ({
config.stripe_product_id == si.price?.product
);
}
return (
config.stripe_price_id == si.price?.id ||
config.stripe_product_id == si.price?.product
);
},
}
);
};
export const findPriceInStripeItems = ({
prices,
subItem,
lineItem,
billingType,
}: {
prices: Price[];
subItem: Stripe.SubscriptionItem | Stripe.InvoiceLineItem;
subItem?: Stripe.SubscriptionItem;
lineItem?: Stripe.InvoiceItem;
billingType?: BillingType;
}) => {
return prices.find((p: Price) => {
let config = p.config;
let itemMatch =
config.stripe_price_id == subItem.price?.id ||
config.stripe_product_id == subItem.price?.product;
let itemMatch;
if (subItem) {
itemMatch =
config.stripe_price_id == subItem.price?.id ||
config.stripe_product_id == subItem.price?.product;
}
if (lineItem) {
const priceDetails = lineItem.pricing?.price_details;
itemMatch =
config.stripe_price_id == priceDetails?.price ||
config.stripe_product_id == priceDetails?.product;
}
const priceBillingType = getBillingType(config);
let billingTypeMatch = billingType ? priceBillingType == billingType : true;
@@ -95,7 +105,7 @@ export const findStripePriceFromPrices = ({
autumnStripePricesMatch({
stripePrice: p,
autumnPrice,
}),
})
);
};

View File

@@ -25,9 +25,9 @@ export const undoSubUpdate = async ({
(item) =>
!prevItems.some((prevItem) =>
curSub.items.data.some(
(curItem) => curItem.price.id === item.price.id,
),
),
(curItem) => curItem.price.id === item.price.id
)
)
)
.map((item) => {
return {
@@ -57,19 +57,43 @@ export const createProrationInvoice = async ({
}) => {
const { stripeCli, customer, paymentMethod } = attachParams;
let items = await stripeCli.invoices.listUpcomingLines({
subscription: curSub.id,
let proratedItems = [];
// How to retrieve upcoming invoice items?
const items = await stripeCli.invoiceItems.list({
customer: customer.processor.id,
pending: true,
});
let proratedItems = items.data.filter(
(item) => item.proration || item.type === "invoiceitem",
);
if (proratedItems.length == 0) {
if (items.data.length == 0) {
logger.info(`No items to prorate, skipping invoice creation`);
return null;
}
// console.log(
// "Upcoming invoice:",
// items.data.map((item) => item.description)
// );
// throw new Error("Not implemented");
// const proratedItems = items.data.filter(
// (item) => item.proration || item.parent?.type === "invoiceitem"
// );
// console.log("Preview invoice items:", items.lines.data);
// let items = await stripeCli.invoices.listUpcomingLines({
// subscription: curSub.id,
// });
// let proratedItems = items.data.filter(
// (item) => item.proration || item.type === "invoiceitem",
// );
// if (proratedItems.length == 0) {
// logger.info(`No items to prorate, skipping invoice creation`);
// return null;
// }
let invoice = await stripeCli.invoices.create({
customer: customer.processor.id,
subscription: curSub.id,
@@ -78,7 +102,7 @@ export const createProrationInvoice = async ({
if (invoiceOnly) return invoice;
await stripeCli.invoices.finalizeInvoice(invoice.id, {
await stripeCli.invoices.finalizeInvoice(invoice.id!, {
auto_advance: false,
});
@@ -86,7 +110,7 @@ export const createProrationInvoice = async ({
const { invoice: subInvoice } = await payForInvoice({
stripeCli,
paymentMethod: paymentMethod || null,
invoiceId: invoice.id,
invoiceId: invoice.id!,
logger,
voidIfFailed: true,
});

View File

@@ -20,7 +20,7 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js";
import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSets.js";
import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js";
import { getEntityInvoiceDescription } from "@/internal/entities/entityUtils/entityInvoiceUtils.js";
import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js";
export const handleCheckoutSessionCompleted = async ({
req,
@@ -100,7 +100,7 @@ export const handleCheckoutSessionCompleted = async ({
});
// Create other subscriptions
const { subs, invoiceIds } = await handleRemainingSets({
const { invoiceIds } = await handleRemainingSets({
stripeCli,
db,
org,
@@ -114,15 +114,12 @@ export const handleCheckoutSessionCompleted = async ({
for (const product of products) {
const anchorToUnix = checkoutSub
? checkoutSub.current_period_end * 1000
? getEarliestPeriodEnd({ sub: checkoutSub! }) * 1000
: undefined;
const subIds = subs.length > 0 ? subs.map((s) => s.id) : undefined;
await createFullCusProduct({
db,
attachParams: attachToInsertParams(attachParams, product),
subscriptionIds: subIds,
subscriptionIds: [checkoutSub?.id!],
anchorToUnix,
scenario: AttachScenario.New,
logger,

View File

@@ -1,14 +1,6 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
APIVersion,
BillingType,
CusProductStatus,
Organization,
UsagePriceConfig,
} from "@autumn/shared";
import { APIVersion, BillingType } from "@autumn/shared";
import Stripe from "stripe";
import { AppEnv } from "@autumn/shared";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { constructSub } from "@/internal/subscriptions/subUtils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
@@ -17,6 +9,8 @@ import {
findPriceFromStripeId,
} from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import { getArrearItems } from "../../stripeSubUtils/getStripeSubItems/getArrearItems.js";
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js";
export const handleCheckoutSub = async ({
stripeCli,
@@ -37,6 +31,8 @@ export const handleCheckoutSub = async ({
return;
}
const { start, end } = subToPeriodStartEnd({ sub: subscription });
await SubService.createSub({
db,
sub: constructSub({
@@ -44,8 +40,8 @@ export const handleCheckoutSub = async ({
usageFeatures: attachParams.itemSets?.[0]?.usageFeatures || [],
orgId: org.id,
env: attachParams.customer.env,
currentPeriodStart: subscription.current_period_start,
currentPeriodEnd: subscription.current_period_end,
currentPeriodStart: start,
currentPeriodEnd: end,
}),
});
@@ -88,20 +84,22 @@ export const handleCheckoutSub = async ({
id: item.id,
deleted: true,
});
itemsUpdate.push(getEmptyPriceItem({ price: arrearPrice, org }) as any);
}
}
let deletedCount = itemsUpdate.filter((item) => item.deleted).length;
if (deletedCount === curSubItems.length) {
itemsUpdate = itemsUpdate.concat(
getArrearItems({
prices: attachParams.prices,
interval: attachParams.itemSets?.[0]?.interval,
intervalCount: attachParams.itemSets?.[0]?.intervalCount,
org,
})
);
}
// let deletedCount = itemsUpdate.filter((item) => item.deleted).length;
// if (deletedCount === curSubItems.length) {
// itemsUpdate = itemsUpdate.concat(
// getArrearItems({
// prices: attachParams.prices,
// interval: attachParams.itemSets?.[0]?.interval,
// intervalCount: attachParams.itemSets?.[0]?.intervalCount,
// org,
// })
// );
// }
if (itemsUpdate.length > 0) {
await stripeCli.subscriptions.update(subscription.id, {
@@ -109,38 +107,11 @@ export const handleCheckoutSub = async ({
});
}
// for (const item of subscription.items.data) {
// let stripePriceId = item.price.id;
// let arrearProratedPrice = findPriceFromPlaceholderId({
// prices: attachParams.prices,
// placeholderId: stripePriceId,
// });
// if (arrearProratedPrice) {
// let config = arrearProratedPrice.config as UsagePriceConfig;
// await stripeCli.subscriptionItems.update(item.id, {
// price: config.stripe_price_id!,
// quantity: 0,
// });
// continue;
// }
// let arrearPrice = findPriceFromStripeId({
// prices: attachParams.prices,
// stripePriceId,
// billingType: BillingType.UsageInArrear,
// });
// if (
// arrearPrice &&
// (attachParams.internalEntityId ||
// attachParams.apiVersion == APIVersion.v1_4)
// ) {
// await stripeCli.subscriptionItems.del(item.id);
// continue;
// }
// }
if (subscription.billing_mode.type !== "flexible") {
await stripeCli.subscriptions.migrate(subscription.id, {
billing_mode: { type: "flexible" },
});
}
return subscription;
};

View File

@@ -66,47 +66,14 @@ export const handleRemainingSets = async ({
const itemSets = attachParams.itemSets;
let remainingSets = itemSets ? itemSets.slice(1) : [];
let subs: Stripe.Subscription[] = [];
const remainingItems = remainingSets.flatMap((set) => set.items);
let invoiceIds: string[] = [checkoutSession.invoice as string];
if (checkoutSub) {
subs.push(checkoutSub);
}
if (!remainingSets || remainingSets.length == 0 || !checkoutSub) {
return {
subs,
invoiceIds,
};
}
const firstSetStart = checkoutSub?.current_period_end;
for (const itemSet of remainingSets) {
const filteredItems = filterUsagePrices({
itemSet,
attachParams,
if (remainingItems.length > 0) {
await stripeCli.subscriptions.update(checkoutSub!.id, {
items: remainingItems,
});
itemSet.items = filteredItems;
const subscription = (await createStripeSub({
db,
stripeCli,
customer: attachParams.customer,
org,
itemSet,
freeTrial: attachParams.freeTrial, // add free trial to subscription...
anchorToUnix: firstSetStart * 1000,
})) as Stripe.Subscription;
subs.push(subscription);
const latestInvoice = subscription.latest_invoice as Stripe.Invoice;
invoiceIds.push(latestInvoice.id);
}
return {
subs,
invoiceIds,
};
return { invoiceIds };
};

View File

@@ -8,7 +8,6 @@ import {
FullCusProduct,
FullCustomerEntitlement,
FullCustomerPrice,
LoggerAction,
Organization,
} from "@autumn/shared";
import Stripe from "stripe";
@@ -25,11 +24,16 @@ import { EntityService } from "@/internal/api/entities/EntityService.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getFullStripeInvoice } from "../../stripeInvoiceUtils.js";
import {
getFullStripeInvoice,
invoiceToSubId,
} from "../../stripeInvoiceUtils.js";
import { handleUsagePrices } from "./handleUsagePrices.js";
import { handleContUsePrices } from "./handleContUsePrices.js";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { handlePrepaidPrices } from "./handlePrepaidPrices.js";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
const handleInArrearProrated = async ({
db,
@@ -69,7 +73,10 @@ const handleInArrearProrated = async ({
// console.log("Sub period end:\t", formatUnixToDateTime(usageSub.current_period_end * 1000));
// Check if invoice is for new subscription period by comparing billing period
const isNewPeriod = invoice.period_start !== usageSub.current_period_start;
const { start: periodStart, end: periodEnd } = subToPeriodStartEnd({
sub: usageSub,
});
const isNewPeriod = invoice.period_start !== periodStart;
if (!isNewPeriod) {
logger.info("Invoice is not for new subscription period, skipping...");
return;
@@ -209,20 +216,28 @@ export const sendUsageAndReset = async ({
continue;
}
let usageBasedSub = await getUsageBasedSub({
db,
// let usageBasedSub = await getUsageBasedSub({
// db,
// stripeCli,
// subIds: activeProduct.subscription_ids || [],
// feature: relatedCusEnt.entitlement.feature,
// stripeSubs,
// });
const usageBasedSub = await cusProductToSub({
cusProduct: activeProduct,
stripeCli,
subIds: activeProduct.subscription_ids || [],
feature: relatedCusEnt.entitlement.feature,
stripeSubs,
});
if (!usageBasedSub || usageBasedSub.id != invoice.subscription) {
const subId = invoiceToSubId({ invoice });
if (!usageBasedSub || usageBasedSub.id != subId) {
continue;
}
// If trial just ended, skip
if (usageBasedSub.trial_end == usageBasedSub.current_period_start) {
const { start, end } = subToPeriodStartEnd({ sub: usageBasedSub });
if (usageBasedSub.trial_end == start) {
logger.info(`Trial just ended, skipping usage invoice.created`);
continue;
}
@@ -288,13 +303,15 @@ export const handleInvoiceCreated = async ({
const stripeCli = createStripeCli({ org, env });
const invoice = await getFullStripeInvoice({
stripeCli,
stripeId: data.id,
stripeId: data.id!,
});
if (invoice.subscription) {
const subId = invoiceToSubId({ invoice });
if (subId) {
const activeProducts = await CusProductService.getByStripeSubId({
db,
stripeSubId: invoice.subscription as string,
stripeSubId: subId,
orgId: org.id,
env,
inStatuses: [
@@ -343,7 +360,7 @@ export const handleInvoiceCreated = async ({
}
if (entDetails && feature) {
await stripeCli.invoices.update(invoice.id, {
await stripeCli.invoices.update(invoice.id!, {
description: `${getFeatureName({
feature,
plural: false,

View File

@@ -18,6 +18,10 @@ import {
RolloverConfig,
} from "@autumn/shared";
import Stripe from "stripe";
import {
getEarliestPeriodEnd,
subToPeriodStartEnd,
} from "../../stripeSubUtils/convertSubUtils.js";
export const handlePrepaidPrices = async ({
db,
@@ -38,7 +42,9 @@ export const handlePrepaidPrices = async ({
invoice: Stripe.Invoice;
logger: any;
}) => {
const isNewPeriod = invoice.period_start !== usageSub.current_period_start;
const { start, end } = subToPeriodStartEnd({ sub: usageSub });
const isNewPeriod = invoice.period_start !== start;
if (!isNewPeriod) {
return;
}
@@ -80,9 +86,11 @@ export const handlePrepaidPrices = async ({
const ent = cusEnt.entitlement;
// const end = getEarliestPeriodEnd({ sub: usageSub });
let rolloverUpdate = getRolloverUpdates({
cusEnt,
nextResetAt: usageSub.current_period_end * 1000,
nextResetAt: end * 1000,
});
// console.log("🔍 rolloverUpdate", rolloverUpdate);
@@ -159,7 +167,7 @@ export const handlePrepaidPrices = async ({
id: cusEnt.id,
updates: {
...resetUpdate,
next_reset_at: usageSub.current_period_end * 1000,
next_reset_at: end * 1000,
},
});
};

View File

@@ -20,6 +20,7 @@ import { findStripeItemForPrice } from "../../stripeSubUtils/stripeSubItemUtils.
import { getRolloverUpdates } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.js";
import { RolloverService } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.js";
import { notNullish } from "@/utils/genUtils.js";
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
export const handleUsagePrices = async ({
db,
@@ -81,7 +82,7 @@ export const handleUsagePrices = async ({
if (isNewUsageMethod) {
let invoiceItem = getInvoiceItemForUsage({
stripeInvoiceId: invoice.id,
stripeInvoiceId: invoice.id!,
price,
customer,
currency: invoice.currency,
@@ -134,21 +135,20 @@ export const handleUsagePrices = async ({
allowance: ent.interval == EntInterval.Lifetime ? 0 : ent.allowance!,
});
const { end } = subToPeriodStartEnd({ sub: usageSub });
await CusEntService.update({
db,
id: relatedCusEnt.id,
updates: {
...resetBalancesUpdate,
adjustment: 0,
next_reset_at: relatedCusEnt.next_reset_at
? usageSub.current_period_end * 1000
: null,
next_reset_at: relatedCusEnt.next_reset_at ? end * 1000 : null,
},
});
let rolloverUpdate = getRolloverUpdates({
cusEnt: relatedCusEnt,
nextResetAt: usageSub.current_period_end * 1000,
nextResetAt: end * 1000,
});
if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
@@ -156,9 +156,6 @@ export const handleUsagePrices = async ({
db,
rows: rolloverUpdate.toInsert,
fullCusEnt: relatedCusEnt,
// rolloverConfig: ent.rollover as RolloverConfig,
// cusEntID: ent.id,
// entityMode: notNullish(ent.entity_feature_id),
});
}

View File

@@ -12,6 +12,7 @@ import { CusProductService } from "@/internal/customers/cusProducts/CusProductSe
import {
getFullStripeInvoice,
getStripeExpandedInvoice,
invoiceToSubId,
updateInvoiceIfExists,
} from "../stripeInvoiceUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -34,19 +35,21 @@ export const handleInvoiceFinalized = async ({
const stripeCli = createStripeCli({ org, env });
const invoice = await getFullStripeInvoice({
stripeCli,
stripeId: data.id,
stripeId: data.id!,
});
if (invoice.subscription) {
const subId = invoiceToSubId({ invoice });
if (subId) {
const stripeCli = createStripeCli({ org, env });
const expandedInvoice = await getStripeExpandedInvoice({
stripeCli,
stripeInvoiceId: invoice.id,
stripeInvoiceId: invoice.id!,
});
const activeProducts = await CusProductService.getByStripeSubId({
db,
stripeSubId: invoice.subscription as string,
stripeSubId: subId,
orgId: org.id,
env,
inStatuses: [CusProductStatus.Active],
@@ -66,7 +69,7 @@ export const handleInvoiceFinalized = async ({
}
let prices = activeProducts.flatMap((cp) =>
cp.customer_prices.map((cpr: FullCustomerPrice) => cpr.price),
cp.customer_prices.map((cpr: FullCustomerPrice) => cpr.price)
);
let invoiceItems = await getInvoiceItems({

View File

@@ -13,6 +13,7 @@ import { nullish } from "@/utils/genUtils.js";
import {
getFullStripeInvoice,
getInvoiceDiscounts,
invoiceToSubId,
updateInvoiceIfExists,
} from "../stripeInvoiceUtils.js";
import { getStripeSubs } from "../stripeSubUtils.js";
@@ -151,7 +152,7 @@ export const handleInvoicePaid = async ({
const stripeCli = createStripeCli({ org, env });
const invoice = await getFullStripeInvoice({
stripeCli,
stripeId: invoiceData.id,
stripeId: invoiceData.id!,
});
if (invoice.metadata?.autumn_metadata_id) {
@@ -173,11 +174,12 @@ export const handleInvoicePaid = async ({
logger,
});
if (invoice.subscription) {
const subId = invoiceToSubId({ invoice });
if (subId) {
// Get customer product
const activeCusProducts = await CusProductService.getByStripeSubId({
db,
stripeSubId: invoice.subscription as string,
stripeSubId: subId,
orgId: org.id,
env,
});

View File

@@ -18,6 +18,10 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { getFullStripeSub } from "../stripeSubUtils.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import {
getEarliestPeriodEnd,
getEarliestPeriodStart,
} from "../stripeSubUtils/convertSubUtils.js";
export const handleSubCreated = async ({
db,
@@ -57,14 +61,16 @@ export const handleSubCreated = async ({
scheduleId: subscription.schedule as string,
});
const earliestPeriodStart = getEarliestPeriodStart({ sub: subscription });
const earliestPeriodEnd = getEarliestPeriodEnd({ sub: subscription });
if (autumnSub) {
await SubService.updateFromScheduleId({
db,
scheduleId: subscription.schedule as string,
updates: {
stripe_id: subscription.id,
current_period_start: subscription.current_period_start,
current_period_end: subscription.current_period_end,
current_period_start: earliestPeriodStart,
current_period_end: earliestPeriodEnd,
},
});
} else {
@@ -88,8 +94,8 @@ export const handleSubCreated = async ({
usage_features: subUsageFeatures,
org_id: org.id,
env: env,
current_period_start: subscription.current_period_start,
current_period_end: subscription.current_period_end,
current_period_start: earliestPeriodStart,
current_period_end: earliestPeriodEnd,
},
});
}

View File

@@ -26,35 +26,24 @@ export const handleSubscriptionScheduleCanceled = async ({
env,
});
if (cusProductsOnSchedule.length === 0) {
console.log(" - subscription_schedule.canceled: no cus products found");
return;
}
if (cusProductsOnSchedule.length === 0) return;
console.log("Handling subscription_schedule.canceled");
console.log(
" - Found",
cusProductsOnSchedule.length,
"cus products on schedule",
);
for (const cusProduct of cusProductsOnSchedule) {
console.log(" - Cus product", cusProduct.product.name, cusProduct.status);
const stripeCli = createStripeCli({ org, env });
if (cusProduct.status === CusProductStatus.Scheduled) {
let otherScheduledIds = cusProduct.scheduled_ids?.filter(
(id: string) => id !== schedule.id,
);
// let otherScheduledIds = cusProduct.scheduled_ids?.filter(
// (id: string) => id !== schedule.id
// );
for (const id of otherScheduledIds || []) {
try {
await stripeCli.subscriptionSchedules.cancel(id);
console.log(" - Cancelled scheduled id", id);
} catch (error) {
console.error("Failed to cancel subscription schedule:", id, error);
}
}
// for (const id of otherScheduledIds || []) {
// try {
// await stripeCli.subscriptionSchedules.cancel(id);
// console.log(" - Cancelled scheduled id", id);
// } catch (error) {
// console.error("Failed to cancel subscription schedule:", id, error);
// }
// }
await CusProductService.delete({
db,
@@ -67,7 +56,7 @@ export const handleSubscriptionScheduleCanceled = async ({
cusProductId: cusProduct.id,
updates: {
scheduled_ids: cusProduct.scheduled_ids?.filter(
(id: string) => id !== schedule.id,
(id: string) => id !== schedule.id
),
},
});
@@ -90,7 +79,7 @@ export const handleSubscriptionScheduleCanceled = async ({
} catch (error) {
logger.error(
`handleSubScheduleCanceled: failed to delete from subscriptions table`,
error,
error
);
}
};

View File

@@ -74,7 +74,7 @@ export const handleSubscriptionUpdated = async ({
if (updatedCusProducts.length > 0) {
logger.info(
`✅ Updated ${updatedCusProducts.length} customer product${updatedCusProducts.length === 1 ? '' : 's'} (${updatedCusProducts.map(cp => cp.id).join(', ')}) - Status: ${updatedCusProducts[0].status}${updatedCusProducts[0].canceled_at ? `, Canceled: ${new Date(updatedCusProducts[0].canceled_at).toISOString()}` : ''}`
`✅ Updated ${updatedCusProducts.length} customer product${updatedCusProducts.length === 1 ? "" : "s"} (${updatedCusProducts.map((cp) => cp.id).join(", ")}) - Status: ${updatedCusProducts[0].status}${updatedCusProducts[0].canceled_at ? `, Canceled: ${new Date(updatedCusProducts[0].canceled_at).toISOString()}` : ""}`
);
}
@@ -103,7 +103,7 @@ export const handleSubscriptionUpdated = async ({
} catch (error) {
logger.warn(
`Failed to update sub from stripe. Stripe sub ID: ${subscription.id}, org: ${org.slug}, env: ${env}`,
error,
error
);
}
@@ -116,11 +116,11 @@ export const handleSubscriptionUpdated = async ({
});
const latestInvoice = await stripeCli.invoices.retrieve(
subscription.latest_invoice,
subscription.latest_invoice
);
logger.info(
`Latest invoice billing reason: ${latestInvoice.billing_reason}`,
`Latest invoice billing reason: ${latestInvoice.billing_reason}`
);
logger.info(`Latest invoice status: ${latestInvoice.status}`);
@@ -138,7 +138,7 @@ export const handleSubscriptionUpdated = async ({
latestInvoiceStatus: latestInvoice.status,
latestInvoiceBillingReason: latestInvoice.billing_reason,
},
},
}
);
return;
}
@@ -154,7 +154,7 @@ export const handleSubscriptionUpdated = async ({
latestInvoiceStatus: latestInvoice.status,
latestInvoiceBillingReason: latestInvoice.billing_reason,
},
},
}
);
await stripeCli.subscriptions.cancel(subscription.id);
await stripeCli.invoices.voidInvoice(subscription.latest_invoice);
@@ -170,7 +170,7 @@ export const handleSubscriptionUpdated = async ({
latestInvoiceStatus: latestInvoice.status,
latestInvoiceBillingReason: latestInvoice.billing_reason,
},
},
}
);
}
}

View File

@@ -8,6 +8,10 @@ import { CusService } from "@/internal/customers/CusService.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { productToInsertParams } from "@/internal/customers/attach/attachUtils/attachParams/convertToParams.js";
import {
getLatestPeriodEnd,
subToPeriodStartEnd,
} from "../../stripeSubUtils/convertSubUtils.js";
export const handleSubCanceled = async ({
req,
@@ -64,8 +68,9 @@ export const handleSubCanceled = async ({
if (defaultProducts.length == 0) return;
if (defaultProducts.length > 0) {
const { end } = subToPeriodStartEnd({ sub });
const productNames = defaultProducts.map((p) => p.name).join(", ");
const periodEnd = formatUnixToDateTime(sub.current_period_end * 1000);
const periodEnd = formatUnixToDateTime(end * 1000);
logger.info(
`subscription.updated: canceled -> attempting to schedule default products: ${productNames}, period end: ${periodEnd}`
);
@@ -88,10 +93,11 @@ export const handleSubCanceled = async ({
entities,
});
const end = getLatestPeriodEnd({ sub });
let fullCusProduct = await createFullCusProduct({
db,
attachParams: insertParams,
startsAt: sub.current_period_end * 1000,
startsAt: end * 1000,
sendWebhook: false,
logger,
});

View File

@@ -19,7 +19,7 @@ import { componentRouter } from "./components/componentRouter.js";
import { analyticsMiddleware } from "@/middleware/analyticsMiddleware.js";
import rewardRouter from "./rewards/rewardRouter.js";
import expireRouter from "../customers/expire/expireRouter.js";
import cancelRouter from "../customers/cancel/cancelRouter.js";
import { handleSetupPayment } from "../customers/attach/handleSetupPayment.js";
import { internalFeatureRouter } from "../features/internalFeatureRouter.js";
import { analyticsRouter } from "../analytics/analyticsRouter.js";
@@ -56,7 +56,7 @@ apiRouter.use("/redemptions", redemptionRouter);
// Cus Product
apiRouter.use("", attachRouter);
apiRouter.use("/cancel", expireRouter);
apiRouter.use("/cancel", cancelRouter);
apiRouter.use("/entitled", checkRouter);
apiRouter.use("/check", checkRouter);
apiRouter.use("/events", eventsRouter);

View File

@@ -7,10 +7,13 @@ import { createStripeCli } from "@/external/stripe/utils.js";
import { pricesContainRecurring } from "@/internal/products/prices/priceUtils.js";
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import {
getStripeSubItems,
getStripeSubItems2,
} from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { ErrCode } from "@/errors/errCodes.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { APIVersion } from "@autumn/shared";
import { APIVersion, AttachConfig } from "@autumn/shared";
import { SuccessCode } from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
@@ -20,11 +23,13 @@ export const handleCreateCheckout = async ({
req,
res,
attachParams,
config,
returnCheckout = false,
}: {
req: any;
res: any;
attachParams: AttachParams;
config: AttachConfig;
returnCheckout?: boolean;
}) => {
const { db, logtail: logger } = req;

View File

@@ -1,203 +0,0 @@
// import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
// import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
// import { getPricesForCusProduct } from "@/internal/customers/change-product/scheduleUtils.js";
// import { getDowngradePreview } from "@/internal/customers/previews/getDowngradePreview.js";
// import { fullCusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
// import {
// isFreeProduct,
// isOneOff,
// isProductUpgrade,
// } from "@/internal/products/productUtils.js";
// import { formatUnixToDate } from "@/utils/genUtils.js";
// import {
// AppEnv,
// BillingType,
// Feature,
// FullCusProduct,
// FullCustomer,
// FullProduct,
// Organization,
// } from "@autumn/shared";
// import { AttachScenario, CheckProductPreview } from "@autumn/shared";
// import { DrizzleCli } from "@/db/initDrizzle.js";
// import { getBillingType } from "@/internal/products/prices/priceUtils.js";
// import { createStripeCli } from "@/external/stripe/utils.js";
// export const getAttachPreview = async ({
// db,
// customer,
// org,
// env,
// product,
// cusProducts,
// features,
// logger,
// shouldFormat = true,
// }: {
// db: DrizzleCli;
// customer: FullCustomer;
// org: Organization;
// env: AppEnv;
// product: FullProduct;
// cusProducts: FullCusProduct[];
// features: Feature[];
// logger: any;
// shouldFormat?: boolean;
// }) => {
// const stripeCli = createStripeCli({ org, env });
// let paymentMethod: any = null;
// if (customer.processor?.id) {
// paymentMethod = await getCusPaymentMethod({
// stripeCli,
// stripeId: customer.processor?.id,
// errorIfNone: false,
// });
// }
// let { curMainProduct, curScheduledProduct, curSameProduct }: any =
// getExistingCusProducts({
// product,
// cusProducts: cusProducts || [],
// internalEntityId: customer.entity?.internal_id,
// });
// let curPrices = getPricesForCusProduct({
// cusProduct: curMainProduct,
// });
// if (curScheduledProduct?.product.id === product.id) {
// let result: CheckProductPreview = {
// title: "Scheduled product already exists",
// message: "You already have this product scheduled to start soon.",
// scenario: AttachScenario.Scheduled,
// recurring: !isOneOff(product.prices),
// error_on_attach: true,
// product_id: product.id,
// product_name: product.name,
// current_product_name: curMainProduct?.product?.name,
// next_cycle_at: curScheduledProduct.starts_at,
// payment_method: paymentMethod,
// };
// return result;
// } else if (curSameProduct) {
// if (!product.is_add_on && !curScheduledProduct && !isOneOff(curPrices)) {
// let result: CheckProductPreview = {
// title: "Product already attached",
// message: "You already have this product attached.",
// scenario: AttachScenario.Active,
// recurring: !isOneOff(product.prices),
// error_on_attach: true,
// product_id: product.id,
// product_name: product.name,
// payment_method: paymentMethod,
// };
// return result;
// }
// }
// if (isFreeProduct(curPrices)) {
// curMainProduct = null;
// }
// // Case 1: No / free main product
// let prodContainsPrepaid = product.prices.some(
// (p) => getBillingType(p.config) == BillingType.UsageInAdvance,
// );
// if (!curMainProduct || product.is_add_on) {
// // 1a. If both are free, no context
// if (isFreeProduct(product.prices)) {
// return null;
// } else {
// if (!prodContainsPrepaid && !paymentMethod) {
// return null;
// }
// return await getNewProductPreview({
// org,
// product,
// features,
// });
// }
// }
// // Case 2: current and new are same products
// if (curMainProduct?.product.id === product.id) {
// // 2a. If there's a scheduled product
// if (curScheduledProduct) {
// let scheduledProduct = fullCusProductToProduct(curScheduledProduct);
// let scheduledStart = formatUnixToDate(curScheduledProduct?.starts_at);
// let canceledAt = formatUnixToDate(curMainProduct?.canceled_at);
// let message, html;
// message = `Clicking 'confirm' will renew your subscription to ${curMainProduct.product.name}, and you will be continue to be charged on ${scheduledStart}.`;
// let scheduledIsFree = isFreeProduct(scheduledProduct.prices);
// if (!scheduledIsFree) {
// let scheduledMessage = `Your downgrade to ${scheduledProduct.name} which was scheduled to start on ${scheduledStart} will also be reversed.`;
// message += `\n\n${scheduledMessage}`;
// }
// // Get
// let result: CheckProductPreview = {
// title: `Renew subscription to ${curMainProduct.product.name}`,
// message,
// scenario: AttachScenario.Renew,
// product_id: product.id,
// product_name: product.name,
// recurring: !isOneOff(product.prices),
// payment_method: paymentMethod,
// };
// return result;
// }
// // 2b. Can't attach same product
// else return null;
// }
// // Case 3: Current and new products are different
// let isUpgrade = isProductUpgrade({
// prices1: curPrices,
// prices2: product.prices,
// });
// if (isUpgrade) {
// let result = await getUpgradePreview({
// db,
// customer,
// paymentMethod,
// org,
// env,
// product,
// curMainProduct,
// logger,
// features,
// });
// return {
// ...result,
// payment_method: paymentMethod,
// };
// } else {
// let result = await getDowngradePreview({
// customer,
// org,
// env,
// product,
// curMainProduct,
// curScheduledProduct,
// });
// return {
// ...result,
// payment_method: paymentMethod,
// };
// }
// };

View File

@@ -1,343 +0,0 @@
// import {
// checkStripeProductExists,
// isOneOff,
// } from "@/internal/products/productUtils.js";
// import {
// AppEnv,
// AttachScenario,
// BillingType,
// CheckProductPreview,
// Feature,
// FullCusProduct,
// FullCustomer,
// FullProduct,
// Organization,
// } from "@autumn/shared";
// import { AttachParams } from "../cusProducts/AttachParams.js";
// import { createStripeCli } from "@/external/stripe/utils.js";
// import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
// // import { billForRemainingUsages } from "../change-product/billRemainingUsages.js";
// import { formatCurrency } from "./previewUtils.js";
// import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
// import { mapToProductItems } from "@/internal/products/productV2Utils.js";
// import { getOptions } from "@/internal/api/entitled/checkUtils.js";
// import {
// isFeaturePriceItem,
// isPriceItem,
// } from "@/internal/products/product-items/productItemUtils/getItemType.js";
// import { DrizzleCli } from "@/db/initDrizzle.js";
// import { getBillingType } from "@/internal/products/prices/priceUtils.js";
// import { cusProductToPrices } from "../cusProducts/cusProductUtils/convertCusProduct.js";
// import Stripe from "stripe";
// export const isAddProductFlow = ({
// curCusProduct,
// attachParams,
// }: {
// curCusProduct: FullCusProduct;
// attachParams: AttachParams;
// }) => {
// // 1. If current product has trial and new product has trial, cancel and start new subscription
// let trialToTrial =
// curCusProduct.trial_ends_at &&
// curCusProduct.trial_ends_at > Date.now() &&
// attachParams.freeTrial;
// // !disableFreeTrial;
// // let trialToPaid =
// // curCusProduct.trial_ends_at &&
// // curCusProduct.trial_ends_at > Date.now() &&
// // !attachParams.freeTrial &&
// // !newVersion; // Only carry over trial if migrating from one version to another...
// // // 2. If upgrade is free to paid, or paid to free (migration / update)
// // let toFreeProduct = isFreeProduct(attachParams.prices);
// // let paidToFreeProduct =
// // isFreeProduct(curCusProduct.customer_prices.map((cp) => cp.price)) &&
// // !isFreeProduct(attachParams.prices);
// // if (trialToTrial || trialToPaid || toFreeProduct || paidToFreeProduct) {
// // if (trialToTrial) {
// // logger.info(
// // `Upgrading from trial to trial, cancelling and starting new subscription`
// // );
// // } else if (toFreeProduct) {
// // logger.info(
// // `switching to free product, cancelling (if needed) and adding free product`
// // );
// // }
// // }
// };
// const formatMessage = ({
// baseLineItems,
// usageLineItems,
// org,
// product,
// }: {
// baseLineItems: any;
// usageLineItems: any;
// org: Organization;
// product: FullProduct;
// }) => {
// let totalAmount = baseLineItems.reduce(
// (acc: number, item: any) => acc + item.amount,
// 0,
// );
// totalAmount += usageLineItems.reduce(
// (acc: number, item: any) => acc + item.amount,
// 0,
// );
// let addString = org.config.bill_upgrade_immediately
// ? "will be charged to your card immediately"
// : "will be added to your next bill";
// let message = `By clicking confirm, you will upgrade your plan to ${product.name} and the following amount ${addString}:`;
// return { message };
// };
// const createStripeProductAndPrices = async ({
// db,
// org,
// env,
// product,
// logger,
// }: {
// db: DrizzleCli;
// org: Organization;
// env: AppEnv;
// product: FullProduct;
// logger: any;
// }) => {
// if (!product.processor?.id) {
// await checkStripeProductExists({
// db,
// org,
// env,
// product,
// logger,
// });
// }
// let batchPriceUpdates = [];
// for (let price of product.prices) {
// let stripeCli = createStripeCli({ org, env });
// if (!price.config?.stripe_price_id) {
// batchPriceUpdates.push(
// createStripePriceIFNotExist({
// db,
// stripeCli,
// price,
// entitlements: product.entitlements,
// product,
// org,
// logger,
// }),
// );
// }
// }
// await Promise.all(batchPriceUpdates);
// };
// export const getUpgradePreview = async ({
// db,
// paymentMethod,
// customer,
// org,
// env,
// product,
// curMainProduct,
// features,
// logger,
// }: {
// paymentMethod: Stripe.PaymentMethod | null | undefined;
// db: DrizzleCli;
// customer: FullCustomer;
// org: Organization;
// env: AppEnv;
// product: FullProduct;
// curMainProduct: FullCusProduct;
// features: Feature[];
// logger: any;
// }) => {
// // Create stripe product / prices if not exist
// await createStripeProductAndPrices({
// db,
// org,
// env,
// product,
// logger,
// });
// let stripeCli = createStripeCli({ org, env });
// let stripeSubs = await getStripeSubs({
// stripeCli,
// subIds: curMainProduct.subscription_ids,
// });
// let attachParams = {
// stripeCli,
// paymentMethod,
// org,
// customer,
// products: [product],
// features,
// prices: product.prices,
// entitlements: product.entitlements,
// freeTrial: product.free_trial || null,
// cusProducts: customer.customer_products,
// optionsList: [],
// entities: [],
// };
// let updatePreview = (await handleStripeSubUpdate({
// db,
// stripeCli,
// curCusProduct: curMainProduct,
// attachParams,
// stripeSubs,
// logger: null,
// carryExistingUsages: false,
// shouldPreview: true,
// })) as any;
// let curPrices = cusProductToPrices({ cusProduct: curMainProduct });
// let allPrices = [...product.prices, ...curPrices];
// let nextCycleAt = stripeSubs[0].current_period_end * 1000;
// // Fetch next cycle at from annual upgrades...
// for (const item of updatePreview.lines.data) {
// if (item.period.end * 1000 > nextCycleAt) {
// nextCycleAt = item.period.end * 1000;
// }
// }
// let baseLineItems = updatePreview.lines.data
// .filter((item: any) => {
// let price = allPrices.find((p) => {
// let config = p.config;
// return (
// config.stripe_price_id === item.price.id ||
// config.stripe_product_id == item.price.product
// );
// });
// if (!price) {
// return true;
// }
// let isPrepaid =
// getBillingType(price?.config!) === BillingType.UsageInAdvance;
// if (isPrepaid) {
// return false; // Don't show prepaid items in preview
// }
// return true;
// })
// .map((item: any) => {
// return {
// amount: item.amount / 100,
// description: item.description,
// };
// });
// // let usageLineItems =
// // (await billForRemainingUsages({
// // db,
// // logger: console,
// // attachParams,
// // curCusProduct: curMainProduct,
// // newSubs: stripeSubs,
// // shouldPreview: true,
// // })) || [];
// let totalAmount = baseLineItems.reduce(
// (acc: number, item: any) => acc + item.amount,
// 0,
// );
// totalAmount += usageLineItems.reduce(
// (acc: number, item: any) => acc + item.amount,
// 0,
// );
// let items = [...baseLineItems, ...usageLineItems].map((item) => {
// return {
// price: formatCurrency({
// amount: item.amount,
// defaultCurrency: org.default_currency,
// }),
// description: item.description,
// usage_model: isFeaturePriceItem(item) ? item.usage_model : undefined,
// };
// });
// let formattedMessage = formatMessage({
// baseLineItems,
// usageLineItems,
// org,
// product,
// });
// // Get options
// let prodItems = mapToProductItems({
// prices: product.prices,
// entitlements: product.entitlements,
// features,
// });
// let options = getOptions({
// prodItems,
// features,
// });
// let proratedAmount = totalAmount;
// let regularAmount = prodItems
// .filter((i) => isPriceItem(i))
// .reduce((sum, i) => sum + i.price!, 0);
// let dueToday, dueNextCycle;
// if (org.config.bill_upgrade_immediately) {
// dueToday = Number(proratedAmount.toFixed(2));
// dueNextCycle = Number(regularAmount.toFixed(2));
// } else {
// dueToday = 0;
// dueNextCycle = Number((proratedAmount + regularAmount).toFixed(2));
// }
// const result: CheckProductPreview = {
// title: `Upgrade to ${product.name}`,
// message: formattedMessage.message,
// scenario: AttachScenario.Upgrade,
// product_id: product.id,
// product_name: product.name,
// recurring: !isOneOff(product.prices),
// next_cycle_at: nextCycleAt,
// current_product_name: curMainProduct.product.name,
// items,
// options: options as any,
// due_today: {
// price: dueToday,
// currency: org.default_currency || "USD",
// },
// due_next_cycle: {
// price: dueNextCycle,
// currency: org.default_currency || "USD",
// },
// };
// return result;
// };

View File

@@ -0,0 +1,179 @@
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
Customer,
FreeTrial,
Organization,
ErrCode,
BillingInterval,
Reward,
IntervalConfig,
} from "@autumn/shared";
import Stripe from "stripe";
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { formatUnixToDateTime, generateId } from "@/utils/genUtils.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getAlignedIntervalUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
// Get payment method
export const createStripeSub2 = async ({
db,
stripeCli,
// customer,
// org,
// freeTrial,
// invoiceOnly = false,
attachParams,
finalizeInvoice = false,
anchorToUnix,
itemSet,
reward,
earliestInterval,
}: {
db: DrizzleCli;
stripeCli: Stripe;
// customer: Customer;
// freeTrial: FreeTrial | null;
// org: Organization;
// invoiceOnly?: boolean;
attachParams: AttachParams;
finalizeInvoice?: boolean;
anchorToUnix?: number;
itemSet: {
subItems: Stripe.SubscriptionItem[];
invoiceItems: any[];
usageFeatures: string[];
};
reward?: Reward;
earliestInterval?: IntervalConfig | null;
}) => {
const { customer, invoiceOnly, freeTrial, org, now } = attachParams;
let paymentMethod = await getCusPaymentMethod({
stripeCli,
stripeId: customer.processor.id,
errorIfNone: !invoiceOnly, // throw error if no payment method and invoiceOnly is false
});
let paymentMethodData = {};
if (paymentMethod) {
paymentMethodData = {
default_payment_method: paymentMethod.id,
};
}
// Get latest interval
// Get earliest interval
// console.log("Earliest interval", earliestInterval);
// console.log("Anchor to unix", formatUnixToDateTime(anchorToUnix));
const billingCycleAnchorUnix =
anchorToUnix && earliestInterval
? getAlignedIntervalUnix({
alignWithUnix: anchorToUnix,
interval: earliestInterval.interval,
intervalCount: earliestInterval.intervalCount ?? 1,
now,
})
: undefined;
// console.log(
// "Billing cycle anchor unix",
// formatUnixToDateTime(billingCycleAnchorUnix)
// );
// const { items, prices, usageFeatures } = itemSet;
// let subItems = items.filter(
// (i: any, index: number) =>
// prices[index].config!.interval !== BillingInterval.OneOff
// );
// let invoiceItems = items.filter(
// (i: any, index: number) =>
// prices[index].config!.interval === BillingInterval.OneOff
// );
const { subItems, invoiceItems, usageFeatures } = itemSet;
try {
const subscription = await stripeCli.subscriptions.create({
...paymentMethodData,
customer: customer.processor.id,
items: subItems as any,
// items: subItems as any,
billing_mode: { type: "flexible" },
trial_end: freeTrialToStripeTimestamp({ freeTrial, now }),
payment_behavior: "error_if_incomplete",
add_invoice_items: invoiceItems,
collection_method: invoiceOnly ? "send_invoice" : "charge_automatically",
days_until_due: invoiceOnly ? 30 : undefined,
billing_cycle_anchor: billingCycleAnchorUnix
? Math.floor(billingCycleAnchorUnix / 1000)
: undefined,
// coupon: reward ? reward.id : undefined,
discounts: reward ? [{ coupon: reward.id }] : undefined,
expand: ["latest_invoice"],
});
// console.log("Latest invoice:", subscription.latest_invoice);
// subscription.latest_invoice = await stripeCli.invoices.retrieve(
// subscription.latest_invoice as string
// );
const latestInvoice = subscription.latest_invoice as Stripe.Invoice;
if (
invoiceOnly &&
finalizeInvoice &&
latestInvoice &&
latestInvoice.status === "draft"
) {
subscription.latest_invoice = await stripeCli.invoices.finalizeInvoice(
(subscription.latest_invoice as Stripe.Invoice).id!
);
}
// Store
const earliestPeriodEnd = getEarliestPeriodEnd({ sub: subscription });
await SubService.createSub({
db,
sub: {
id: generateId("sub"),
stripe_id: subscription.id,
stripe_schedule_id: subscription.schedule as string,
created_at: subscription.created * 1000,
usage_features: usageFeatures,
org_id: org.id,
env: customer.env,
current_period_start: earliestPeriodEnd,
current_period_end: earliestPeriodEnd,
},
});
return subscription;
} catch (error: any) {
console.log("Warning: Failed to create stripe subscription");
console.log("Error code:", error.code);
console.log("Message:", error.message);
console.log("Decline code:", error.decline_code);
throw new RecaseError({
code: ErrCode.CreateStripeSubscriptionFailed,
message: `Create stripe subscription failed ${
error.code ? `(${error.code})` : ""
}: ${error.message || ""}`,
statusCode: 500,
});
}
};

View File

@@ -1,5 +1,6 @@
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachConfig, FullProduct, Product, products } from "@autumn/shared";
@@ -17,17 +18,21 @@ export const getMergeCusProduct = async ({
let mergeCusProduct = undefined;
if (!config.disableMerge && !freeTrial) {
mergeCusProduct = cusProducts?.find((cp) =>
products.some((p) => p.group == cp.product.group),
products.some((p) => p.group == cp.product.group)
);
}
let mergeSubs = await getStripeSubs({
const mergeSub = await cusProductToSub({
cusProduct: mergeCusProduct,
stripeCli,
subIds: mergeCusProduct?.subscription_ids,
});
// let mergeSubs = await getStripeSubs({
// stripeCli,
// subIds: mergeCusProduct?.subscription_ids,
// });
return {
mergeCusProduct,
mergeSubs,
mergeSub: mergeSub || undefined,
};
};

View File

@@ -12,6 +12,7 @@ import { handlePaidProduct } from "./handlePaidProduct.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { getDefaultAttachConfig } from "../../attachUtils/getAttachConfig.js";
import { getMergeCusProduct } from "./getMergeCusProduct.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const handleAddProduct = async ({
req,
@@ -46,7 +47,7 @@ export const handleAddProduct = async ({
const batchInsert = [];
const { mergeCusProduct, mergeSubs } = await getMergeCusProduct({
const { mergeCusProduct, mergeSub } = await getMergeCusProduct({
attachParams,
config: config || defaultConfig,
products,
@@ -63,8 +64,9 @@ export const handleAddProduct = async ({
anchorToUnix = curCusProduct.created_at;
}
if (mergeSubs.length > 0) {
anchorToUnix = mergeSubs[0].current_period_end * 1000;
if (mergeSub) {
const { end } = subToPeriodStartEnd({ sub: mergeSub });
anchorToUnix = end * 1000;
}
batchInsert.push(

View File

@@ -1,11 +1,5 @@
import RecaseError from "@/utils/errorUtils.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { createStripeSub } from "@/external/stripe/stripeSubUtils/createStripeSub.js";
import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
import {
AttachParams,
AttachResultSchema,
@@ -14,7 +8,6 @@ import {
attachToInvoiceResponse,
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
@@ -22,11 +15,18 @@ import {
AttachConfig,
AttachScenario,
BillingInterval,
ErrCode,
intervalsDifferent,
SuccessCode,
} from "@autumn/shared";
import Stripe from "stripe";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import {
getEarliestPeriodEnd,
getLatestPeriodEnd,
subToPeriodStartEnd,
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { createStripeSub2 } from "./createStripeSub2.js";
import { addBillingIntervalUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
export const handlePaidProduct = async ({
req,
@@ -56,9 +56,14 @@ export const handlePaidProduct = async ({
freeTrial = null;
}
let itemSets = await getStripeSubItems({
// let itemSets = await getStripeSubItems({
// attachParams,
// carryExistingUsages: config.carryUsage,
// });
const itemSet = await getStripeSubItems2({
attachParams,
carryExistingUsages: config.carryUsage,
config,
});
let subscriptions: Stripe.Subscription[] = [];
@@ -71,96 +76,42 @@ export const handlePaidProduct = async ({
);
}
let mergeSubs = await getStripeSubs({
let mergeSub = await cusProductToSub({
cusProduct: mergeCusProduct!,
stripeCli,
subIds: mergeCusProduct?.subscription_ids,
});
for (let i = 0; i < itemSets.length; i++) {
const itemSet = itemSets[i];
if (itemSet.interval === BillingInterval.OneOff) {
continue;
}
let mergeWithSub = mergeSubs.find((sub) => {
let subInterval = subToAutumnInterval(sub);
return !intervalsDifferent({
intervalA: {
interval: subInterval.interval,
intervalCount: subInterval.intervalCount,
},
intervalB: {
interval: itemSet.interval,
intervalCount: itemSet.intervalCount,
},
});
});
let subscription;
try {
let billingCycleAnchorUnix;
if (org.config.anchor_start_of_month) {
billingCycleAnchorUnix = getNextStartOfMonthUnix({
interval: itemSet.interval,
intervalCount: itemSet.intervalCount,
});
}
if (attachParams.billingAnchor) {
billingCycleAnchorUnix = attachParams.billingAnchor;
}
if (mergeWithSub) {
billingCycleAnchorUnix = mergeWithSub.current_period_end * 1000;
}
subscription = await createStripeSub({
db: req.db,
stripeCli,
customer,
org,
freeTrial,
invoiceOnly,
itemSet,
finalizeInvoice: config.invoiceCheckout,
anchorToUnix: billingCycleAnchorUnix,
reward: i == 0 ? reward : undefined,
now: attachParams.now,
});
let sub = subscription as Stripe.Subscription;
subscriptions.push(sub);
} catch (error: any) {
if (
error instanceof RecaseError &&
!invoiceOnly &&
error.code == ErrCode.CreateStripeSubscriptionFailed
) {
return await handleCreateCheckout({
req,
res,
attachParams,
});
}
throw error;
}
let billingCycleAnchorUnix = undefined;
if (attachParams.billingAnchor) {
billingCycleAnchorUnix = attachParams.billingAnchor;
}
const anchorToUnix =
subscriptions.length > 0
? subscriptions[0].current_period_end * 1000
: mergeSubs.length > 0
? mergeSubs[0].current_period_end * 1000
: undefined;
const earliestInterval = getSmallestInterval({ prices: attachParams.prices });
if (mergeSub) {
const { end } = subToPeriodStartEnd({ sub: mergeSub });
billingCycleAnchorUnix = end * 1000;
}
const newSub = await createStripeSub2({
db: req.db,
stripeCli,
attachParams,
itemSet,
anchorToUnix: billingCycleAnchorUnix,
earliestInterval,
});
subscriptions.push(newSub);
const anchorToUnix = getEarliestPeriodEnd({ sub: newSub }) * 1000;
const batchInsertInvoice: any = [];
for (const sub of subscriptions) {
if (!sub.latest_invoice) continue;
batchInsertInvoice.push(
insertInvoiceFromAttach({
db: req.db,
// invoiceId: sub.latest_invoice as string,
stripeInvoice: sub.latest_invoice as Stripe.Invoice,
attachParams,
logger,
@@ -223,3 +174,76 @@ export const handlePaidProduct = async ({
}
}
};
// for (let i = 0; i < itemSets.length; i++) {
// const itemSet = itemSets[i];
// if (itemSet.interval === BillingInterval.OneOff) {
// continue;
// }
// let mergeWithSub = mergeSubs.find((sub) => {
// let subInterval = subToAutumnInterval(sub);
// return !intervalsDifferent({
// intervalA: {
// interval: subInterval.interval,
// intervalCount: subInterval.intervalCount,
// },
// intervalB: {
// interval: itemSet.interval,
// intervalCount: itemSet.intervalCount,
// },
// });
// });
// let subscription;
// try {
// let billingCycleAnchorUnix;
// if (org.config.anchor_start_of_month) {
// billingCycleAnchorUnix = getNextStartOfMonthUnix({
// interval: itemSet.interval,
// intervalCount: itemSet.intervalCount,
// });
// }
// if (attachParams.billingAnchor) {
// billingCycleAnchorUnix = attachParams.billingAnchor;
// }
// if (mergeWithSub) {
// billingCycleAnchorUnix =
// mergeWithSub.items.data[0].current_period_end * 1000;
// }
// subscription = await createStripeSub({
// db: req.db,
// stripeCli,
// customer,
// org,
// freeTrial,
// invoiceOnly,
// itemSet,
// finalizeInvoice: config.invoiceCheckout,
// anchorToUnix: billingCycleAnchorUnix,
// reward: i == 0 ? reward : undefined,
// now: attachParams.now,
// });
// let sub = subscription as Stripe.Subscription;
// subscriptions.push(sub);
// } catch (error: any) {
// if (
// error instanceof RecaseError &&
// !invoiceOnly &&
// error.code == ErrCode.CreateStripeSubscriptionFailed
// ) {
// return await handleCreateCheckout({
// req,
// res,
// attachParams,
// });
// }
// throw error;
// }
// }

View File

@@ -0,0 +1,68 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { generateId } from "@/utils/genUtils.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import { BillingInterval } from "@autumn/shared";
export const createSubSchedule = async ({
db,
attachParams,
itemSet,
endOfBillingPeriod,
}: {
db: DrizzleCli;
attachParams: AttachParams;
itemSet: {
subItems: any[];
invoiceItems: any[];
usageFeatures: string[];
};
endOfBillingPeriod: number;
}) => {
const { org, customer, paymentMethod } = attachParams;
const { stripeCli } = attachParams;
// let subItems = items.filter(
// (item: any, index: number) =>
// index >= prices.length ||
// prices[index].config!.interval !== BillingInterval.OneOff
// );
// let oneOffItems = items.filter(
// (item: any, index: number) =>
// index < prices.length &&
// prices[index].config!.interval === BillingInterval.OneOff
// );
const { subItems, invoiceItems, usageFeatures } = itemSet;
const newSubscriptionSchedule = await stripeCli.subscriptionSchedules.create({
customer: customer.processor.id,
start_date: endOfBillingPeriod,
billing_mode: { type: "flexible" },
phases: [
{
items: subItems,
default_payment_method: paymentMethod?.id,
add_invoice_items: invoiceItems,
},
],
});
await SubService.createSub({
db,
sub: {
id: generateId("sub"),
stripe_id: null,
stripe_schedule_id: newSubscriptionSchedule.id,
created_at: Date.now(),
usage_features: usageFeatures,
org_id: org.id,
env: customer.env,
current_period_start: null,
current_period_end: null,
},
});
return newSubscriptionSchedule;
};

View File

@@ -0,0 +1,127 @@
import Stripe from "stripe";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import {
AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
APIVersion,
AttachConfig,
AttachScenario,
SuccessCode,
} from "@autumn/shared";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
attachToInsertParams,
isFreeProduct,
} from "@/internal/products/productUtils.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import {
attachParamsToCurCusProduct,
paramsToCurSub,
paramsToCurSubSchedule,
} from "../../attachUtils/convertAttachParams.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { createSubSchedule } from "./createSubSchedule.js";
import { cancelEndOfCycle } from "@/internal/customers/cancel/cancelEndOfCycle.js";
export const handleScheduleFunction2 = async ({
req,
res,
attachParams,
config,
}: {
req: any;
res: any;
attachParams: AttachParams;
config: AttachConfig;
}) => {
const logger = req.logtail;
const product = attachParams.products[0];
const { stripeCli, customer: fullCus } = attachParams;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
const curSub = await paramsToCurSub({ attachParams });
const latestPeriodEnd = getLatestPeriodEnd({ sub: curSub! });
// 1. Cancel current subscription and fetch items from other cus products...?
let { schedule, prices } = await paramsToCurSubSchedule({ attachParams });
const itemSet = await getStripeSubItems2({
attachParams,
config,
});
// await stripeCli.subscriptions.update(curSub!.id!, {
// cancel_at: latestPeriodEnd!,
// });
await cancelEndOfCycle({
req,
cusProduct: curCusProduct!,
fullCus,
});
const newProductFree = isFreeProduct(attachParams.prices);
if (!newProductFree) {
if (schedule) {
// Update current schedule
} else {
// Create new schedule
schedule = await createSubSchedule({
db: req.db,
attachParams,
itemSet,
endOfBillingPeriod: latestPeriodEnd!,
});
// Update current cus products with new schedule id
await CusProductService.updateByStripeSubId({
db: req.db,
stripeSubId: curSub!.id!,
updates: {
scheduled_ids: [schedule!.id],
},
});
await CusProductService.update({
db: req.db,
cusProductId: curCusProduct!.id,
updates: {
canceled_at: latestPeriodEnd! * 1000,
scheduled_ids: [],
},
});
}
}
await createFullCusProduct({
db: req.db,
attachParams: attachToInsertParams(attachParams, product),
startsAt: latestPeriodEnd! * 1000,
subscriptionScheduleIds: schedule ? [schedule.id] : [],
nextResetAt: latestPeriodEnd! * 1000,
disableFreeTrial: true,
isDowngrade: true,
scenario: newProductFree ? AttachScenario.Cancel : AttachScenario.Downgrade,
logger,
});
let apiVersion = attachParams.apiVersion || APIVersion.v1;
if (apiVersion >= APIVersion.v1_1) {
res.status(200).json(
AttachResultSchema.parse({
code: SuccessCode.DowngradeScheduled,
message: `Successfully downgraded from ${curCusProduct!.product.name} to ${product.name}`,
product_ids: [product.id],
customer_id:
attachParams.customer.id || attachParams.customer.internal_id,
})
);
} else {
res.status(200).json({
success: true,
});
}
};

View File

@@ -17,6 +17,7 @@ import { createFullCusProduct } from "@/internal/customers/add-product/createFul
import { cusProductsToSchedules } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const handleScheduleFunction = async ({
req,
@@ -42,7 +43,7 @@ export const handleScheduleFunction = async ({
subIds: curCusProduct.subscription_ids,
});
const latestPeriodEnd = curSubs[0].current_period_end;
const latestPeriodEnd = getLatestPeriodEnd({ sub: curSubs[0] });
// 2. Cancel current subscriptions and fetch items from other cus products (to be added to new schedule)
logger.info("2. Cancelling current subscription (at period end)");

View File

@@ -29,12 +29,12 @@ export const scheduleStripeSub = async ({
let subItems = items.filter(
(item: any, index: number) =>
index >= prices.length ||
prices[index].config!.interval !== BillingInterval.OneOff,
prices[index].config!.interval !== BillingInterval.OneOff
);
let oneOffItems = items.filter(
(item: any, index: number) =>
index < prices.length &&
prices[index].config!.interval === BillingInterval.OneOff,
prices[index].config!.interval === BillingInterval.OneOff
);
const newSubscriptionSchedule = await stripeCli.subscriptionSchedules.create({
@@ -129,7 +129,7 @@ export const handleNewScheduleForItemSet = async ({
...otherSubItems.map((sub: any) => ({
price: sub.price.id,
quantity: sub.quantity,
})),
}))
);
// 3. Create new schedule

View File

@@ -22,6 +22,7 @@ import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/creat
import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { Decimal } from "decimal.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const handleQuantityUpgrade = async ({
req,
@@ -59,13 +60,15 @@ export const handleQuantityUpgrade = async ({
const prorate = shouldProrate(onIncrease);
if (prorate) {
const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
const amount = priceToInvoiceAmount({
price: cusPrice.price,
quantity: difference,
proration: prorate
? {
start: stripeSub.current_period_start * 1000,
end: stripeSub.current_period_end * 1000,
start: start * 1000,
end: end * 1000,
}
: undefined,
now,
@@ -74,7 +77,7 @@ export const handleQuantityUpgrade = async ({
const config = cusPrice.price.config as UsagePriceConfig;
const billingUnits = config.billing_units;
const feature = features.find(
(f: Feature) => f.internal_id == newOptions.internal_feature_id,
(f: Feature) => f.internal_id == newOptions.internal_feature_id
)!;
const product = cusProductToProduct({ cusProduct });
@@ -94,11 +97,11 @@ export const handleQuantityUpgrade = async ({
stripeSubId: stripeSub.id,
stripeCustomerId: stripeSub.customer as string,
periodStart: Math.floor((now || Date.now()) / 1000),
periodEnd: Math.floor(stripeSub.current_period_end * 1000),
periodEnd: Math.floor(end * 1000),
});
logger.info(
`🔥 Creating prepaid invoice item: ${invoiceItem.description} - ${amount}`,
`🔥 Creating prepaid invoice item: ${invoiceItem.description} - ${amount}`
);
await stripeCli.invoiceItems.create(invoiceItem);
@@ -130,7 +133,7 @@ export const handleQuantityUpgrade = async ({
if (cusEnt) {
const incrementBy = new Decimal(difference).mul(billingUnits).toNumber();
logger.info(
`🔥 Incrementing feature ${cusEnt.entitlement.feature.id} balance by ${incrementBy}`,
`🔥 Incrementing feature ${cusEnt.entitlement.feature.id} balance by ${incrementBy}`
);
await CusEntService.increment({
db,

View File

@@ -1,5 +1,5 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
@@ -27,7 +27,8 @@ export const getUsageInvoiceItems = async ({
logger,
attachParams,
cusProduct,
stripeSubs,
// stripeSubs,
sub,
interval,
intervalCount,
}: {
@@ -35,7 +36,8 @@ export const getUsageInvoiceItems = async ({
logger: any;
attachParams: AttachParams;
cusProduct: FullCusProduct;
stripeSubs: Stripe.Subscription[];
// stripeSubs: Stripe.Subscription[];
sub: Stripe.Subscription;
interval?: BillingInterval;
intervalCount?: number;
}) => {
@@ -66,15 +68,16 @@ export const getUsageInvoiceItems = async ({
const cusEnt = getRelatedCusEnt({ cusPrice, cusEnts })!;
const sub = await getUsageBasedSub({
db,
stripeCli,
stripeSubs,
subIds: cusProduct.subscription_ids!,
feature: cusEnt.entitlement.feature,
});
// const sub = await getUsageBasedSub({
// db,
// stripeCli,
// // stripeSubs,
// sub,
// subIds: cusProduct.subscription_ids!,
// feature: cusEnt.entitlement.feature,
// });
if (!sub) continue;
// if (!sub) continue;
if (
interval &&
intervalsDifferent({
@@ -94,7 +97,7 @@ export const getUsageInvoiceItems = async ({
currency: org.default_currency || "usd",
},
period: {
start: sub.current_period_start,
start: sub.items.data[0].current_period_start,
end: Math.floor((attachParams.now || Date.now()) / 1000),
},
};
@@ -112,7 +115,8 @@ export const createUsageInvoiceItems = async ({
db,
attachParams,
cusProduct,
stripeSubs,
// stripeSubs,
sub,
invoiceId,
logger,
interval,
@@ -121,7 +125,8 @@ export const createUsageInvoiceItems = async ({
db: DrizzleCli;
attachParams: AttachParams;
cusProduct: FullCusProduct;
stripeSubs: Stripe.Subscription[];
// stripeSubs: Stripe.Subscription[];
sub: Stripe.Subscription;
invoiceId?: string;
logger: any;
interval?: BillingInterval;
@@ -133,7 +138,8 @@ export const createUsageInvoiceItems = async ({
db,
attachParams,
cusProduct,
stripeSubs,
// stripeSubs,
sub,
interval,
intervalCount,
logger,
@@ -150,7 +156,7 @@ export const createUsageInvoiceItems = async ({
await stripeCli.invoiceItems.create({
...invoiceItem,
invoice: invoiceId ? invoiceId : undefined,
subscription: invoiceId ? undefined : stripeSubs[0].id,
subscription: invoiceId ? undefined : sub.id,
customer: attachParams.customer.processor.id,
});
};

View File

@@ -24,6 +24,7 @@ import {
} from "@/internal/invoices/invoiceUtils.js";
import { updateSubsDiffInt } from "./updateSubsDiffInt.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const handleUpgradeDiffInterval = async ({
req,
@@ -85,13 +86,13 @@ export const handleUpgradeDiffInterval = async ({
// Insert new cus product
logger.info("4. Creating new cus product");
const { end } = subToPeriodStartEnd({ sub: newSubs[0] });
await createFullCusProduct({
db: req.db,
attachParams: attachToInsertParams(attachParams, products[0]),
subscriptionIds: newSubs.map((sub) => sub.id),
anchorToUnix:
newSubs.length > 0 ? newSubs[0].current_period_end * 1000 : undefined,
anchorToUnix: newSubs.length > 0 ? end * 1000 : undefined,
disableFreeTrial: disableTrial,
carryExistingUsages: carryUsage,

View File

@@ -62,7 +62,7 @@ export const updateSubsDiffInt = async ({
db,
attachParams,
cusProduct: curCusProduct,
stripeSubs,
sub: firstSub,
logger,
});

View File

@@ -1,6 +1,8 @@
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import Stripe from "stripe";
export const getNextCycle = (stripeSubs: Stripe.Subscription[]) => {
const nextCycle = stripeSubs[0].current_period_end * 1000;
const { end } = subToPeriodStartEnd({ sub: stripeSubs[0] });
const nextCycle = end * 1000;
return nextCycle;
};

View File

@@ -0,0 +1,139 @@
import {
AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
attachParamsToCurCusProduct,
attachParamToCusProducts,
paramsToCurSub,
} from "../../attachUtils/convertAttachParams.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import {
attachToInsertParams,
isFreeProduct,
} from "@/internal/products/productUtils.js";
import { APIVersion, AttachConfig, CusProductStatus } from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import {
attachToInvoiceResponse,
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { updateStripeSub2 } from "./updateStripeSub2.js";
import { removeCurCusProductItems } from "../../attachUtils/attachUtils.js";
import {
getEarliestPeriodEnd,
subToPeriodStartEnd,
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const handleUpgradeFlow = async ({
req,
res,
attachParams,
config,
}: {
req: ExtendedRequest;
res?: any;
attachParams: AttachParams;
config: AttachConfig;
}) => {
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
const curSub = await paramsToCurSub({ attachParams });
const logger = req.logtail;
if (curCusProduct?.api_version) {
attachParams.apiVersion = curCusProduct.api_version;
}
let sub = curSub;
let latestInvoice = undefined;
const itemSet = await getStripeSubItems2({
attachParams,
config,
});
const newSubItems = await removeCurCusProductItems({
sub: curSub,
cusProduct: curCusProduct!,
subItems: itemSet.subItems,
});
if (newSubItems.length > 0) {
itemSet.subItems = newSubItems;
logger.info(`1. Updating subs with new items`);
const res = await updateStripeSub2({
req,
attachParams,
config,
curSub: curSub!,
itemSet,
});
attachParams.replaceables = res.replaceables || [];
sub = res.updatedSub;
latestInvoice = res.latestInvoice;
}
logger.info(`2. Expiring previous cus product`);
await CusProductService.update({
db: req.db,
cusProductId: curCusProduct!.id,
updates: {
subscription_ids: [],
status: CusProductStatus.Expired,
},
});
if (latestInvoice) {
await insertInvoiceFromAttach({
db: req.db,
attachParams,
stripeInvoice: latestInvoice,
logger,
});
}
logger.info(`3. Creating new cus product`);
const anchorToUnix = sub ? getEarliestPeriodEnd({ sub }) * 1000 : undefined;
await createFullCusProduct({
db: req.db,
attachParams: attachToInsertParams(attachParams, attachParams.products[0]),
subscriptionIds: curCusProduct!.subscription_ids || [],
disableFreeTrial: config.disableTrial,
carryExistingUsages: config.carryUsage,
carryOverTrial: config.carryTrial,
anchorToUnix: anchorToUnix,
logger,
});
if (res) {
let apiVersion = attachParams.org.api_version || APIVersion.v1;
if (apiVersion >= APIVersion.v1_1) {
res.status(200).json(
AttachResultSchema.parse({
customer_id: attachParams.customer.id,
product_ids: attachParams.products.map((p) => p.id),
// invoice: attachParams.invoiceOnly
// ? attachToInvoiceResponse({ invoice: invoices?.[0] })
// : undefined,
invoice: attachParams.invoiceOnly
? attachToInvoiceResponse({ invoice: latestInvoice || undefined })
: undefined,
code: "updated_product_successfully",
message: `Successfully updated product`,
})
);
} else {
res.status(200).json({
success: true,
message: `Successfully updated product`,
});
}
}
};

View File

@@ -0,0 +1,134 @@
import Stripe from "stripe";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { AttachConfig, ProrationBehavior } from "@autumn/shared";
import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js";
import {
createUsageInvoiceItems,
resetUsageBalances,
} from "../upgradeDiffIntFlow/createUsageInvoiceItems.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { createProrationInvoice } from "@/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.js";
import { createAndFilterContUseItems } from "../../attachUtils/getContUseItems/createContUseInvoiceItems.js";
export const updateStripeSub2 = async ({
req,
attachParams,
config,
curSub,
itemSet,
}: {
req: ExtendedRequest;
attachParams: AttachParams;
config: AttachConfig;
curSub: Stripe.Subscription;
itemSet: {
subItems: Stripe.SubscriptionItem[];
invoiceItems: Stripe.InvoiceItem[];
};
}) => {
const { db, logger } = req;
const { curMainProduct } = attachParamToCusProducts({ attachParams });
const { stripeCli, customer, org, paymentMethod } = attachParams;
const { invoiceOnly, proration } = config;
// const { subItems, addInvoiceItems } = await getSubAndInvoiceItems({
// itemSet,
// });
if (curSub.billing_mode.type !== "flexible") {
curSub = await stripeCli.subscriptions.migrate(curSub.id, {
billing_mode: { type: "flexible" },
});
}
// 1. Update subscription
let updatedSub = await stripeCli.subscriptions.update(curSub.id, {
items: itemSet.subItems,
proration_behavior:
proration == ProrationBehavior.None ? "none" : "create_prorations",
// trial_end: trialEnd,
default_payment_method: paymentMethod?.id,
add_invoice_items: itemSet.invoiceItems,
...((invoiceOnly && {
collection_method: "send_invoice",
days_until_due: 30,
}) as any),
payment_behavior: "error_if_incomplete",
expand: ["latest_invoice"],
});
let latestInvoice = updatedSub.latest_invoice as Stripe.Invoice | null;
if (proration == ProrationBehavior.None) {
return {
updatedSub,
latestInvoice: null,
};
}
// 2. Create prorations for single use items
let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({
db,
attachParams,
cusProduct: curMainProduct!,
// stripeSubs: [curSub],
sub: curSub,
logger,
});
// // 3. Create prorations for continuous use items
let { replaceables } = await createAndFilterContUseItems({
attachParams,
curMainProduct: curMainProduct!,
sub: curSub,
// interval: config.sameIntervals ? interval : undefined,
// intervalCount: config.sameIntervals ? intervalCount : undefined,
logger,
});
if (proration === ProrationBehavior.Immediately) {
latestInvoice = await createProrationInvoice({
attachParams,
invoiceOnly,
curSub,
updatedSub,
logger,
});
console.log(`FINALIZED INVOICE ${latestInvoice?.id}`);
console.log(latestInvoice?.lines.data.map((line) => line.description));
}
await resetUsageBalances({
db,
cusEntIds,
cusProduct: curMainProduct!,
});
// await SubService.addUsageFeatures({
// db,
// stripeId: curSub.id,
// usageFeatures: itemSet.usageFeatures,
// orgId: org.id,
// env: customer.env,
// });
// if (invoiceOnly && attachParams.finalizeInvoice) {
// logger.info(`FINALIZING INVOICE ${latestInvoice?.id}`);
// try {
// latestInvoice = await stripeCli.invoices.finalizeInvoice(
// latestInvoice?.id as string
// );
// } catch (error) {
// logger.error(`Failed to finalize invoice ${latestInvoice?.id}`, {
// error,
// });
// }
// }
return {
updatedSub,
latestInvoice: latestInvoice,
cusEntIds,
replaceables: [],
};
};

View File

@@ -15,6 +15,7 @@ import { updateSubsByInt } from "./updateSubsSameInt.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { attachToInvoiceResponse } from "@/internal/invoices/invoiceUtils.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const handleUpgradeSameInterval = async ({
req,
@@ -62,6 +63,11 @@ export const handleUpgradeSameInterval = async ({
logger.info(`3. Creating new cus product`);
const end =
stripeSubs.length > 0
? getLatestPeriodEnd({ sub: stripeSubs[0] })
: undefined;
await createFullCusProduct({
db: req.db,
attachParams: attachToInsertParams(attachParams, attachParams.products[0]),
@@ -69,10 +75,7 @@ export const handleUpgradeSameInterval = async ({
disableFreeTrial: config.disableTrial,
carryExistingUsages: config.carryUsage,
carryOverTrial: config.carryTrial,
anchorToUnix:
stripeSubs.length > 0
? stripeSubs[0].current_period_end * 1000
: undefined,
anchorToUnix: end,
logger,
});

View File

@@ -6,6 +6,8 @@ import {
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import Stripe from "stripe";
import { attachParamsToProduct } from "./convertAttachParams.js";
import { FullCusProduct } from "@autumn/shared";
import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
export const getCycleWillReset = ({
attachParams,
@@ -22,3 +24,34 @@ export const getCycleWillReset = ({
intervalB: prevInterval,
});
};
export const removeCurCusProductItems = async ({
sub,
cusProduct,
subItems,
}: {
sub?: Stripe.Subscription | null;
cusProduct?: FullCusProduct;
subItems: any[];
}) => {
if (!sub || !cusProduct) {
return subItems;
}
const newItems: any[] = structuredClone(subItems);
for (const item of sub.items.data) {
let shouldRemove = subItemInCusProduct({
cusProduct,
subItem: item,
});
if (shouldRemove) {
newItems.push({
id: item.id,
deleted: true,
});
}
}
return newItems;
};

View File

@@ -45,3 +45,62 @@ export const attachParamsToProduct = ({
free_trial: freeTrial,
};
};
export const paramsToCurSub = async ({
attachParams,
}: {
attachParams: AttachParams;
}) => {
const { stripeCli } = attachParams;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
const subIds = curCusProduct?.subscription_ids || [];
if (subIds.length === 0) {
return undefined;
}
const sub = await stripeCli.subscriptions.retrieve(subIds[0], {
expand: ["items.data.price.tiers"],
});
return sub;
};
export const paramsToCurSubSchedule = async ({
attachParams,
}: {
attachParams: AttachParams;
}) => {
const { stripeCli } = attachParams;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
const subScheduleIds = curCusProduct?.scheduled_ids || [];
if (subScheduleIds.length === 0) {
return {
schedule: null,
prices: [],
};
}
const schedule = await stripeCli.subscriptionSchedules.retrieve(
subScheduleIds[0]
);
if (schedule.status == "canceled") {
return {
schedule: null,
prices: [],
};
}
const batchPricesGet = [];
for (const item of schedule.phases[0].items) {
batchPricesGet.push(stripeCli.prices.retrieve(item.price as string));
}
const prices = await Promise.all(batchPricesGet);
return {
schedule,
prices,
};
};

View File

@@ -3,6 +3,7 @@ import { AttachParams } from "../../cusProducts/AttachParams.js";
import { CusProductService } from "../../cusProducts/CusProductService.js";
import { cancelFutureProductSchedule } from "../../change-product/scheduleUtils.js";
import { attachParamToCusProducts } from "./convertAttachParams.js";
import { cancelScheduledProduct } from "../../cancel/cancelScheduledProduct.js";
export const deleteCurrentScheduledProduct = async ({
req,
@@ -19,13 +20,13 @@ export const deleteCurrentScheduledProduct = async ({
}) => {
const stripeCli = attachParams.stripeCli;
const { curScheduledProduct } = attachParamToCusProducts({
const { curScheduledProduct, curMainProduct } = attachParamToCusProducts({
attachParams,
});
if (curScheduledProduct) {
logger.info(
`deleteCurrentScheduledProduct: cancelling scheduled - ${curScheduledProduct.product.name}`,
`deleteCurrentScheduledProduct: cancelling scheduled - ${curScheduledProduct.product.name}`
);
// 2. Delete scheduled product
@@ -35,17 +36,26 @@ export const deleteCurrentScheduledProduct = async ({
});
}
if (attachFunc == AttachFunction.Renew || curScheduledProduct) {
await cancelFutureProductSchedule({
if (curScheduledProduct || attachFunc == AttachFunction.Renew) {
await cancelScheduledProduct({
req,
db: req.db,
org,
cusProducts: attachParams.cusProducts!,
product: attachParams.products[0],
stripeCli,
logger,
env: attachParams.customer.env,
internalEntityId: attachParams.internalEntityId || undefined,
curScheduledProduct,
fullCus: attachParams.customer,
curMainProduct,
});
}
// if (attachFunc == AttachFunction.Renew || curScheduledProduct) {
// await cancelFutureProductSchedule({
// req,
// db: req.db,
// org,
// cusProducts: attachParams.cusProducts!,
// product: attachParams.products[0],
// stripeCli,
// logger,
// env: attachParams.customer.env,
// internalEntityId: attachParams.internalEntityId || undefined,
// });
// }
};

View File

@@ -147,6 +147,13 @@ const checkSameCustom = async ({
// return AttachBranch.MainIsFree;
// }
// if (
// isFreeProduct(attachParams.prices) &&
// isFreeProduct(cusProductToPrices({ cusProduct: curSameProduct }))
// ) {
// return AttachBranch.MainIsFree;
// }
if (onlyEntsChanged) {
return AttachBranch.SameCustomEnts;
}

View File

@@ -26,6 +26,8 @@ import { handleOneOffFunction } from "../attachFunctions/addProductFlow/handleOn
import { handleUpgradeSameInterval } from "../attachFunctions/upgradeSameIntFlow/handleUpgradeSameInt.js";
import { CusProductService } from "../../cusProducts/CusProductService.js";
import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js";
import { handleUpgradeFlow } from "../attachFunctions/upgradeFlow/handleUpgradeFlow.js";
import { handleScheduleFunction2 } from "../attachFunctions/scheduleFlow/handleScheduleFlow2.js";
/*
1. If from new version, free trial should just carry over
@@ -158,13 +160,6 @@ export const runAttachFunction = async ({
);
}
// config.proration = ProrationBehavior.None;
// attachParams.billingAnchor = 1781702400000;
// attachParams.billingAnchor = 1749902400000;
// config.proration = ProrationBehavior.None;
// config.carryUsage = true;
if (attachFunction == AttachFunction.OneOff) {
return await handleOneOffFunction({
req,
@@ -230,6 +225,7 @@ export const runAttachFunction = async ({
req,
res,
attachParams,
config,
});
}
@@ -243,15 +239,24 @@ export const runAttachFunction = async ({
}
if (attachFunction == AttachFunction.ScheduleProduct) {
return await handleScheduleFunction({
return await handleScheduleFunction2({
req,
res,
attachParams,
config,
});
// return await handleScheduleFunction({
// req,
// res,
// attachParams,
// });
}
if (attachFunction == AttachFunction.UpgradeSameInterval) {
return await handleUpgradeSameInterval({
if (
attachFunction == AttachFunction.UpgradeDiffInterval ||
attachFunction == AttachFunction.UpgradeSameInterval
) {
return await handleUpgradeFlow({
req,
res,
attachParams,
@@ -259,14 +264,23 @@ export const runAttachFunction = async ({
});
}
if (attachFunction == AttachFunction.UpgradeDiffInterval) {
return await handleUpgradeDiffInterval({
req,
res,
attachParams,
config,
});
}
// if (attachFunction == AttachFunction.UpgradeSameInterval) {
// return await handleUpgradeSameInterval({
// req,
// res,
// attachParams,
// config,
// });
// }
// if (attachFunction == AttachFunction.UpgradeDiffInterval) {
// return await handleUpgradeDiffInterval({
// req,
// res,
// attachParams,
// config,
// });
// }
if (attachFunction == AttachFunction.UpdatePrepaidQuantity) {
return await handleUpdateQuantityFunction({

View File

@@ -14,6 +14,7 @@ import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductU
import { attachParamsToProduct } from "../convertAttachParams.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { intervalsAreSame } from "../getAttachConfig.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const filterContUsageProrations = async ({
sub,
@@ -33,29 +34,37 @@ export const filterContUsageProrations = async ({
});
let allPrices = [...curPrices, ...newProduct.prices];
const upcomingLines = await stripeCli.invoices.listUpcomingLines({
subscription: sub.id,
// const upcomingLines = await stripeCli.invoices.listUpcomingLines({
// subscription: sub.id,
// });
const pendingItems = await stripeCli.invoiceItems.list({
pending: true,
customer: sub.customer as string,
});
const intervalSet = subToAutumnInterval(sub);
for (const item of upcomingLines.data) {
for (const item of pendingItems.data) {
// console.log("LINE ITEM:", item);
if (!item.proration) continue;
let price = findPriceInStripeItems({
prices: allPrices,
subItem: item as any,
lineItem: item,
billingType: BillingType.InArrearProrated,
});
if (!price) continue;
logger.info(
`Deleting ii: ${item.description} - ${item.amount / 100} (${intervalSet.interval}, ${intervalSet.intervalCount})`
);
await stripeCli.invoiceItems.del(
item.id
// @ts-ignore -- Stripe types are not correct
item.parent.subscription_item_details.invoice_item
// item.parent.subscription_item_details.invoice_item
);
}
};
@@ -63,14 +72,16 @@ export const filterContUsageProrations = async ({
export const createAndFilterContUseItems = async ({
attachParams,
curMainProduct,
stripeSubs,
// stripeSubs,
sub,
logger,
interval,
intervalCount,
}: {
attachParams: AttachParams;
curMainProduct: FullCusProduct;
stripeSubs: Stripe.Subscription[];
// stripeSubs: Stripe.Subscription[];
sub: Stripe.Subscription;
logger: any;
interval?: BillingInterval;
intervalCount?: number;
@@ -87,19 +98,10 @@ export const createAndFilterContUseItems = async ({
let { newItems, oldItems } = await getContUseInvoiceItems({
attachParams,
cusProduct: curMainProduct!,
stripeSubs,
sub,
logger,
});
let sub =
stripeSubs.find((sub) => {
return intervalsSame({
intervalA: { interval: interval!, intervalCount: intervalCount! },
intervalB: subToAutumnInterval(sub),
});
// subToAutumnInterval(sub) == interval
}) || stripeSubs[0];
await filterContUsageProrations({
sub,
stripeCli,
@@ -138,6 +140,7 @@ export const createAndFilterContUseItems = async ({
`Adding invoice item: ${item.description}, ${item.description}, interval: ${interval}`
);
const { start, end } = subToPeriodStartEnd({ sub });
await stripeCli.invoiceItems.create({
customer: customer.processor?.id!,
amount: Math.round(item.amount * 100),
@@ -146,7 +149,7 @@ export const createAndFilterContUseItems = async ({
subscription: sub.id,
period: {
start: Math.floor(now / 1000),
end: sub.current_period_end,
end: end,
},
});
}

View File

@@ -109,12 +109,12 @@ export const getContUseNewItems = async ({
export const getContUseInvoiceItems = async ({
cusProduct,
stripeSubs,
sub,
attachParams,
logger,
}: {
cusProduct?: FullCusProduct;
stripeSubs?: Stripe.Subscription[];
sub?: Stripe.Subscription;
attachParams: AttachParams;
logger: any;
}) => {
@@ -122,10 +122,10 @@ export const getContUseInvoiceItems = async ({
const cusEnts = cusProduct ? cusProduct.customer_entitlements : [];
const product = attachParamsToProduct({ attachParams });
const allIntervalsSame = intervalsAreSame({ attachParams });
const curItems = stripeSubs
// const allIntervalsSame = intervalsAreSame({ attachParams });
const curItems = sub
? await getCurContUseItems({
stripeSubs,
sub,
attachParams,
})
: [];
@@ -151,7 +151,7 @@ export const getContUseInvoiceItems = async ({
? getRelatedCusPrice(prevCusEnt, cusPrices)!
: undefined;
if (!allIntervalsSame || !prevCusEnt || !stripeSubs) {
if (!prevCusEnt || !sub) {
const newItem = await getContUseNewItems({
price,
ent,
@@ -176,14 +176,6 @@ export const getContUseInvoiceItems = async ({
(item) => item.price_id === prevCusPrice?.price.id
);
let sub = stripeSubs!.find((sub) => {
let subInterval = subToAutumnInterval(sub);
return intervalsSame({
intervalA: price.config,
intervalB: subInterval,
});
});
let {
oldItem,
newItems: newItems_,
@@ -206,6 +198,8 @@ export const getContUseInvoiceItems = async ({
replaceables.push(...replaceables_);
}
// console.log("Replaceables:", replaceables);
return {
oldItems,
newItems,

View File

@@ -7,13 +7,19 @@ import {
} from "@autumn/shared";
import Stripe from "stripe";
import { attachParamsToProduct } from "../convertAttachParams.js";
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import {
getExistingUsageFromCusProducts,
getRelatedCusPrice,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { getContUseDowngradeItems } from "./getContUseDowngradeItems.js";
import { shouldProrate } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { getContUseUpgradeItems } from "./getContUseUpgradeItems.js";
import { priceToInvoiceItem } from "@/internal/products/prices/priceUtils/priceToInvoiceItem.js";
import { Decimal } from "decimal.js";
import { notNullish } from "@/utils/genUtils.js";
import { cusProductsToCusPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const priceToContUseItem = async ({
price,
@@ -35,10 +41,30 @@ export const priceToContUseItem = async ({
const { cusProducts, entities, internalEntityId, now } = attachParams;
const product = attachParamsToProduct({ attachParams });
const prevEnt = prevCusEnt?.entitlement;
const prevCusPrice = getRelatedCusPrice(
prevCusEnt,
cusProductsToCusPrices({ cusProducts })
)!;
let { start, end } = subToPeriodStartEnd({ sub });
if (prevCusPrice) {
const subItem = findStripeItemForPrice({
price: prevCusPrice.price,
stripeItems: sub?.items.data || [],
});
if (subItem) {
start = (subItem as Stripe.SubscriptionItem).current_period_start;
end = (subItem as Stripe.SubscriptionItem).current_period_end;
}
}
const proration = sub
? {
start: sub.current_period_start * 1000,
end: sub.current_period_end * 1000,
start: start * 1000,
end: end * 1000,
}
: undefined;
@@ -113,7 +139,7 @@ export const priceToContUseItem = async ({
return {
oldItem: null,
newItems: [res.newUsageItem].filter((item) =>
notNullish(item),
notNullish(item)
) as PreviewLineItem[],
replaceables: res.replaceables,
};
@@ -121,7 +147,7 @@ export const priceToContUseItem = async ({
return {
oldItem: res.oldItem,
newItems: [res.newItem, res.newUsageItem].filter((item) =>
notNullish(item),
notNullish(item)
) as PreviewLineItem[],
replaceables: res.replaceables,
};

View File

@@ -125,7 +125,7 @@ export const updateStripeSub = async ({
db,
attachParams,
cusProduct: curMainProduct!,
stripeSubs,
sub: curSub,
logger,
interval: config.sameIntervals ? interval : undefined,
intervalCount: config.sameIntervals ? intervalCount : undefined,

View File

@@ -146,6 +146,7 @@ export const handleCheckout = (req: any, res: any) =>
req,
res,
attachParams,
config,
returnCheckout: true,
});

View File

@@ -74,6 +74,8 @@ export const attachParamsToPreview = async ({
attachParams,
now,
logger,
branch,
config,
});
}

View File

@@ -4,35 +4,45 @@ import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
attachParamsToProduct,
attachParamToCusProducts,
paramsToCurSub,
} from "../attachUtils/convertAttachParams.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
import { UsageModel } from "@autumn/shared";
import { AttachBranch, AttachConfig, UsageModel } from "@autumn/shared";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const getDowngradeProductPreview = async ({
attachParams,
now,
logger,
branch,
config,
}: {
attachParams: AttachParams;
now: number;
logger: any;
branch: AttachBranch;
config: AttachConfig;
}) => {
const newProduct = attachParamsToProduct({ attachParams });
const { curCusProduct } = attachParamToCusProducts({ attachParams });
const stripeSubs = await getStripeSubs({
stripeCli: attachParams.stripeCli,
subIds: curCusProduct?.subscription_ids || [],
});
const sub = await paramsToCurSub({ attachParams });
const anchorToUnix = stripeSubs[0].current_period_end * 1000;
// const stripeSubs = await getStripeSubs({
// stripeCli: attachParams.stripeCli,
// subIds: curCusProduct?.subscription_ids || [],
// });
// const anchorToUnix = stripeSubs[0].current_period_end * 1000;
const anchorToUnix = sub ? getLatestPeriodEnd({ sub }) * 1000 : undefined;
let items = await getItemsForNewProduct({
newProduct,
attachParams,
now,
logger,
branch,
config,
});
items = items.filter((item) => item.usage_model !== UsageModel.Prepaid);

View File

@@ -1,4 +1,10 @@
import { AttachBranch, AttachConfig, BillingInterval } from "@autumn/shared";
import {
AttachBranch,
AttachConfig,
BillingInterval,
FullProduct,
FreeTrial,
} from "@autumn/shared";
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
@@ -10,10 +16,75 @@ import {
getNextStartOfMonthUnix,
} from "@/internal/products/prices/billingIntervalUtils.js";
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import {
getLargestInterval,
getSmallestInterval,
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { getMergeCusProduct } from "../attachFunctions/addProductFlow/getMergeCusProduct.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import { formatUnixToDateTime, notNullish, nullish } from "@/utils/genUtils.js";
import {
getLatestPeriodEnd,
subToPeriodStartEnd,
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
const getNextCycleItems = async ({
newProduct,
attachParams,
anchorToUnix,
branch,
withPrepaid,
logger,
config,
}: {
newProduct: FullProduct;
attachParams: AttachParams;
anchorToUnix?: number;
branch: AttachBranch;
withPrepaid?: boolean;
logger: any;
config: AttachConfig;
}) => {
// 1. If one off, return null
if (branch == AttachBranch.OneOff || isFreeProduct(newProduct.prices))
return null;
// 2. If free trial
let nextCycleAt = undefined;
if (attachParams.freeTrial) {
nextCycleAt =
freeTrialToStripeTimestamp({
freeTrial: attachParams.freeTrial,
now: attachParams.now,
})! * 1000;
} else if (anchorToUnix) {
// Yearly one
const largestInterval = getLargestInterval({ prices: newProduct.prices });
if (largestInterval) {
nextCycleAt = getAlignedIntervalUnix({
alignWithUnix: anchorToUnix,
interval: largestInterval.interval,
intervalCount: largestInterval.intervalCount,
now: attachParams.now,
});
}
}
const items = await getItemsForNewProduct({
newProduct,
attachParams,
now: attachParams.now,
logger,
withPrepaid,
branch,
config,
});
return {
line_items: items,
due_at: nextCycleAt,
};
};
export const getNewProductPreview = async ({
branch,
@@ -39,87 +110,105 @@ export const getNewProductPreview = async ({
});
}
const { mergeCusProduct, mergeSubs } = await getMergeCusProduct({
const { mergeSub } = await getMergeCusProduct({
attachParams,
products: [newProduct],
config,
});
if (mergeSubs.length > 0) {
anchorToUnix = mergeSubs[0].current_period_end * 1000;
if (mergeSub) {
const { start } = subToPeriodStartEnd({ sub: mergeSub });
// const smallestInterval = getLargestInterval({ prices: newProduct.prices });
const smallestInterval = getSmallestInterval({ prices: newProduct.prices });
// console.log("New product:", newProduct.prices);
// console.log("Smallest interval", smallestInterval);
if (smallestInterval) {
anchorToUnix = addBillingIntervalUnix({
unixTimestamp: start * 1000,
interval: smallestInterval!.interval,
intervalCount: smallestInterval!.intervalCount,
});
}
}
const freeTrial = attachParams.freeTrial;
const items = await getItemsForNewProduct({
newProduct,
attachParams,
now: attachParams.now,
freeTrial: attachParams.freeTrial,
anchorToUnix,
freeTrial,
logger,
withPrepaid,
branch,
config,
});
let dueNextCycle = null;
// let dueNextCycle = null;
const dueNextCycle = await getNextCycleItems({
newProduct,
attachParams,
anchorToUnix,
branch,
withPrepaid,
logger,
config,
});
if (
(freeTrial || notNullish(anchorToUnix)) &&
branch != AttachBranch.OneOff
) {
let nextCycleItems = await getItemsForNewProduct({
newProduct,
attachParams,
now: attachParams.now,
logger,
withPrepaid,
branch,
config,
});
// Show next cycle if free trial or notNullish(anchorToUnix) or branch != one off?
// let minInterval = getLastInterval({
// prices: newProduct.prices,
// ents: newProduct.entitlements,
// });
let min = getSmallestInterval({
prices: newProduct.prices,
ents: newProduct.entitlements,
});
// if (
// (freeTrial || notNullish(anchorToUnix)) &&
// branch != AttachBranch.OneOff
// ) {
// let nextCycleItems = await getItemsForNewProduct({
// newProduct,
// attachParams,
// now: attachParams.now,
// logger,
// withPrepaid,
// branch,
// config,
// });
let getAligned = notNullish(anchorToUnix) && notNullish(min);
// // let minInterval = getLastInterval({
// // prices: newProduct.prices,
// // ents: newProduct.entitlements,
// // });
// let min = getSmallestInterval({
// prices: newProduct.prices,
// ents: newProduct.entitlements,
// });
let dueAt = freeTrial
? freeTrialToStripeTimestamp({
freeTrial,
now: attachParams.now,
})! * 1000
: getAligned
? getAlignedIntervalUnix({
alignWithUnix: anchorToUnix!,
interval: min!.interval,
intervalCount: min!.intervalCount,
now: attachParams.now,
})
: notNullish(min)
? addBillingIntervalUnix({
unixTimestamp: attachParams.now || Date.now(),
interval: min!.interval,
intervalCount: min!.intervalCount,
})
: undefined;
// let getAligned = notNullish(anchorToUnix) && notNullish(min);
dueNextCycle = !nullish(dueAt)
? {
line_items: nextCycleItems,
due_at: dueAt,
}
: undefined;
}
// let dueAt = freeTrial
// ? freeTrialToStripeTimestamp({
// freeTrial,
// now: attachParams.now,
// })! * 1000
// : getAligned
// ? getAlignedIntervalUnix({
// alignWithUnix: anchorToUnix!,
// interval: min!.interval,
// intervalCount: min!.intervalCount,
// now: attachParams.now,
// })
// : notNullish(min)
// ? addBillingIntervalUnix({
// unixTimestamp: attachParams.now || Date.now(),
// interval: min!.interval,
// intervalCount: min!.intervalCount,
// })
// : undefined;
const dueTodayAmt = items.reduce((acc, item) => {
return acc + (item.amount ?? 0);
}, 0);
// dueNextCycle = !nullish(dueAt)
// ? {
// line_items: nextCycleItems,
// due_at: dueAt,
// }
// : undefined;
// }
let options = getOptions({
prodItems: mapToProductItems({
@@ -132,31 +221,35 @@ export const getNewProductPreview = async ({
now: attachParams.now || Date.now(),
});
// Next cycle at
if (!dueNextCycle) {
if (!isFreeProduct(newProduct.prices) && branch != AttachBranch.OneOff) {
let min = getSmallestInterval({
prices: newProduct.prices,
ents: newProduct.entitlements,
});
dueNextCycle = {
line_items: items.filter((item) => {
let price = newProduct.prices.find(
(price) => price.id == item.price_id
);
return (
price?.config.interval == min!.interval &&
(price?.config.interval_count || 1) == (min!.intervalCount || 1)
);
}),
due_at: addBillingIntervalUnix({
unixTimestamp: attachParams.now || Date.now(),
interval: min!.interval,
intervalCount: min!.intervalCount,
}),
};
}
}
const dueTodayAmt = items.reduce((acc, item) => {
return acc + (item.amount ?? 0);
}, 0);
// // Next cycle at
// if (!dueNextCycle) {
// if (!isFreeProduct(newProduct.prices) && branch != AttachBranch.OneOff) {
// let min = getSmallestInterval({
// prices: newProduct.prices,
// ents: newProduct.entitlements,
// });
// dueNextCycle = {
// line_items: items.filter((item) => {
// let price = newProduct.prices.find(
// (price) => price.id == item.price_id
// );
// return (
// price?.config.interval == min!.interval &&
// (price?.config.interval_count || 1) == (min!.intervalCount || 1)
// );
// }),
// due_at: addBillingIntervalUnix({
// unixTimestamp: attachParams.now || Date.now(),
// interval: min!.interval,
// intervalCount: min!.intervalCount,
// }),
// };
// }
// }
return {
currency: attachParams.org.default_currency,
@@ -165,7 +258,7 @@ export const getNewProductPreview = async ({
total: dueTodayAmt,
},
due_next_cycle: dueNextCycle,
free_trial: freeTrial,
free_trial: attachParams.freeTrial,
options,
};
};

View File

@@ -2,6 +2,7 @@ import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
attachParamsToProduct,
attachParamToCusProducts,
paramsToCurSub,
} from "../attachUtils/convertAttachParams.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
@@ -30,19 +31,21 @@ import { Decimal } from "decimal.js";
import { intervalsAreSame } from "../attachUtils/getAttachConfig.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { formatUnixToDateTime, notNullish, nullish } from "@/utils/genUtils.js";
import {
getLatestPeriodEnd,
subToPeriodStartEnd,
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
const getNextCycleAt = ({
prices,
stripeSubs,
willCycleReset,
sub,
now,
freeTrial,
branch,
curCusProduct,
}: {
prices: Price[];
stripeSubs: Stripe.Subscription[];
willCycleReset: boolean;
sub: Stripe.Subscription;
now?: number;
freeTrial?: FreeTrial | null;
branch: AttachBranch;
@@ -51,50 +54,33 @@ const getNextCycleAt = ({
now = now || Date.now();
if (branch == AttachBranch.NewVersion && curCusProduct?.free_trial) {
return {
next_cycle_at: curCusProduct.trial_ends_at,
};
return curCusProduct.trial_ends_at;
}
if (freeTrial) {
return {
next_cycle_at:
freeTrialToStripeTimestamp({
freeTrial,
now,
})! * 1000,
};
return (
freeTrialToStripeTimestamp({
freeTrial,
now,
})! * 1000
);
}
const firstInterval = getLargestInterval({ prices });
if (nullish(firstInterval)) {
return {
next_cycle_at: now,
};
}
if (willCycleReset) {
return {
next_cycle_at: addBillingIntervalUnix({
unixTimestamp: now,
interval: firstInterval!.interval,
intervalCount: firstInterval!.intervalCount,
}),
};
return now;
}
const nextCycleAt = getAlignedIntervalUnix({
alignWithUnix: stripeSubs[0].current_period_end * 1000,
alignWithUnix: getLatestPeriodEnd({ sub }) * 1000,
interval: firstInterval!.interval,
intervalCount: firstInterval!.intervalCount,
alwaysReturn: true,
now,
});
return {
next_cycle_at: nextCycleAt,
};
return nextCycleAt;
};
export const getUpgradeProductPreview = async ({
@@ -114,22 +100,15 @@ export const getUpgradeProductPreview = async ({
}) => {
const { logtail: logger } = req;
const { stripeCli } = attachParams;
const { curMainProduct, curSameProduct } = attachParamToCusProducts({
attachParams,
});
const curCusProduct = curSameProduct || curMainProduct!;
const stripeSubs = await getStripeSubs({
stripeCli,
subIds: curCusProduct?.subscription_ids || [],
expand: ["items.data.price.tiers"],
});
const sub = await paramsToCurSub({ attachParams });
const curPreviewItems = await getItemsForCurProduct({
stripeSubs,
sub: sub!,
attachParams,
branch,
config,
@@ -139,11 +118,26 @@ export const getUpgradeProductPreview = async ({
// Get prorated amounts for new product
const newProduct = attachParamsToProduct({ attachParams });
const intervalsSame = intervalsAreSame({ attachParams });
const anchorToUnix =
intervalsSame && stripeSubs.length > 0
? stripeSubs[0].current_period_end * 1000
: undefined;
// const anchorToUnix = sub ? getLatestPeriodEnd({ sub }) * 1000 : undefined;
let anchorToUnix = undefined;
try {
if (sub) {
const { start, end } = subToPeriodStartEnd({ sub });
const largestInterval = getLargestInterval({ prices: newProduct.prices });
anchorToUnix = addBillingIntervalUnix({
unixTimestamp: start * 1000,
interval: largestInterval!.interval,
intervalCount: largestInterval!.intervalCount,
});
}
} catch (error: any) {
logger.error(
`Error getting anchorToUnix for upgrade preview: ${error.message}`,
{
error,
}
);
}
let freeTrial = attachParams.freeTrial;
if (config?.carryTrial && curCusProduct?.free_trial) {
@@ -156,21 +150,18 @@ export const getUpgradeProductPreview = async ({
now,
anchorToUnix,
freeTrial,
stripeSubs,
sub: sub!,
logger,
withPrepaid,
branch,
config,
});
const largestInterval = getLargestInterval({ prices: newProduct.prices });
let dueNextCycle = undefined;
if (!isFreeProduct(newProduct.prices)) {
const nextCycleAt = getNextCycleAt({
prices: newProduct.prices,
stripeSubs,
willCycleReset: !intervalsSame,
sub: sub!,
now,
freeTrial: attachParams.freeTrial,
branch,
@@ -180,10 +171,7 @@ export const getUpgradeProductPreview = async ({
let nextCycleItems = await getItemsForNewProduct({
newProduct,
attachParams,
interval: attachParams.freeTrial ? undefined : largestInterval?.interval,
intervalCount: attachParams.freeTrial
? undefined
: largestInterval?.intervalCount,
// sub: sub!,
logger,
withPrepaid,
branch,
@@ -192,7 +180,7 @@ export const getUpgradeProductPreview = async ({
dueNextCycle = {
line_items: nextCycleItems,
due_at: nextCycleAt.next_cycle_at,
due_at: nextCycleAt,
};
}

View File

@@ -30,9 +30,6 @@ export const handleAttachPreview = (req: any, res: any) =>
logger,
});
console.log("Branch:", attachPreview.branch);
console.log("Func:", attachPreview.func);
res.status(200).json(attachPreview);
return;

View File

@@ -0,0 +1,90 @@
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { AttachConfig, intervalsSame } from "@autumn/shared";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import Stripe from "stripe";
import { ItemSet } from "@/utils/models/ItemSet.js";
import { attachParamsToCurCusProduct } from "../attachUtils/convertAttachParams.js";
// 1. Create proration items for fixed prices
const createFixedPriceProrations = async ({
req,
attachParams,
config,
currentSubs,
itemSets,
}: {
req: ExtendedRequest;
attachParams: AttachParams;
currentSubs: Stripe.Subscription[];
config: AttachConfig;
itemSets: ItemSet[];
}) => {
const { stripeCli } = attachParams;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
for (const sub of currentSubs) {
const itemSet = itemSets.find((itemSet) =>
intervalsSame({
intervalA: itemSet,
intervalB: subToAutumnInterval(sub),
})
);
// const { newItems, shouldCancel } = mergeWithCurItems({
// sub,
// itemSet,
// curCusProduct,
// });
// subToNewItems.push({
// sub,
// newItems,
// shouldCancel,
// });
// if (newItems.length == 0) continue;
// if (shouldCancel) {
// const preview = await stripeCli.invoices.createPreview({
// subscription: sub.id,
// subscription_details: {
// cancel_now: true,
// },
// });
// for (const lineItem of preview.lines.data) {
// await stripeCli.invoiceItems.create({
// customer: attachParams.customer.processor.id,
// amount: lineItem.amount,
// currency: lineItem.currency,
// description: lineItem.description || "",
// });
// }
// } else {
// const [originalPreview, previewInvoice] = await Promise.all([
// stripeCli.invoices.createPreview({
// subscription: sub.id,
// }),
// stripeCli.invoices.createPreview({
// subscription: sub.id,
// subscription_details: {
// items: newItems,
// },
// }),
// ]);
// for (const lineItem of previewInvoice.lines.data) {
// const inCurItems = originalPreview.lines.data.find(
// (i) => i.id == lineItem.id
// );
// if (!inCurItems) {
// // console.log(lineItem.description, lineItem.amount);
// // prorationItems.push();
// await stripeCli.invoiceItems.create({
// customer: attachParams.customer.processor.id,
// amount: lineItem.amount,
// currency: lineItem.currency,
// description: lineItem.description || "",
// });
// }
// }
// }
}
};

View File

@@ -0,0 +1,302 @@
// import Stripe from "stripe";
// import { DrizzleCli } from "@/db/initDrizzle.js";
// import { createStripeSub } from "@/external/stripe/stripeSubUtils/createStripeSub.js";
// import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
// import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
// import {
// AttachConfig,
// FullCusProduct,
// intervalsSame,
// ProrationBehavior,
// } from "@autumn/shared";
// import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
// import { ExtendedRequest } from "@/utils/models/Request.js";
// export const handleUnifiedAttach = async ({
// req,
// attachParams,
// config,
// }: {
// req: ExtendedRequest;
// attachParams: AttachParams;
// config: AttachConfig;
// }) => {
// // 1. Get upcoming item sets, and current subscriptions
// const itemSets = await getStripeSubItems({
// attachParams,
// carryExistingUsages: config.carryUsage,
// });
// const currentSubs = await getCurrentSubs({
// db: req.db,
// attachParams,
// });
// // 2. Create invoice items for prorations
// };
// Expire old cus product
// Create new full cus product
// Insert new invoices
// export const updateSubsDiffInt = async ({
// req,
// stripeCli,
// curCusProduct,
// attachParams,
// stripeSubs,
// config,
// }: {
// req: ExtendedRequest;
// stripeCli: Stripe;
// curCusProduct: FullCusProduct;
// attachParams: AttachParams;
// stripeSubs: Stripe.Subscription[];
// config: AttachConfig;
// }) => {
// const { db, logger } = req;
// const itemSets = await getStripeSubItems({
// attachParams,
// carryExistingUsages: config.carryUsage,
// });
// // let trialEnd = config.disableTrial
// // ? undefined
// // : freeTrialToStripeTimestamp({
// // freeTrial: attachParams.freeTrial,
// // now: attachParams.now,
// // });
// // 1. Create prorations for fixed price items
// const subToNewItems: any = [];
// for (const sub of stripeSubs) {
// const itemSet = itemSets.find((itemSet) =>
// intervalsSame({
// intervalA: itemSet,
// intervalB: subToAutumnInterval(sub),
// })
// );
// const { newItems, shouldCancel } = mergeWithCurItems({
// sub,
// itemSet,
// curCusProduct,
// });
// subToNewItems.push({
// sub,
// newItems,
// shouldCancel,
// });
// if (newItems.length == 0) continue;
// if (shouldCancel) {
// const preview = await stripeCli.invoices.createPreview({
// subscription: sub.id,
// subscription_details: {
// cancel_now: true,
// },
// });
// for (const lineItem of preview.lines.data) {
// await stripeCli.invoiceItems.create({
// customer: attachParams.customer.processor.id,
// amount: lineItem.amount,
// currency: lineItem.currency,
// description: lineItem.description || "",
// });
// }
// } else {
// const [originalPreview, previewInvoice] = await Promise.all([
// stripeCli.invoices.createPreview({
// subscription: sub.id,
// }),
// stripeCli.invoices.createPreview({
// subscription: sub.id,
// subscription_details: {
// items: newItems,
// },
// }),
// ]);
// for (const lineItem of previewInvoice.lines.data) {
// const inCurItems = originalPreview.lines.data.find(
// (i) => i.id == lineItem.id
// );
// if (!inCurItems) {
// // console.log(lineItem.description, lineItem.amount);
// // prorationItems.push();
// await stripeCli.invoiceItems.create({
// customer: attachParams.customer.processor.id,
// amount: lineItem.amount,
// currency: lineItem.currency,
// description: lineItem.description || "",
// });
// }
// }
// }
// }
// for (const subToNewItem of subToNewItems) {
// console.log(`Sub to new item`);
// console.log(`Sub ID: ${subToNewItem.sub.id}`);
// console.log(`New items: ${JSON.stringify(subToNewItem.newItems)}`);
// console.log(`Should cancel: ${subToNewItem.shouldCancel}`);
// console.log(`--------------------------------`);
// }
// throw new Error("Stop");
// // 2. Create prorations for single use items
// let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({
// db,
// attachParams,
// cusProduct: curCusProduct,
// stripeSubs,
// logger,
// });
// // Create any new subs
// const createItemSets = itemSets.filter((itemSet) => {
// return !stripeSubs.some((sub) =>
// intervalsSame({
// intervalA: itemSet,
// intervalB: subToAutumnInterval(sub),
// })
// );
// });
// const updatedSubs = [];
// const invoices = [];
// // 1. Create new subscriptions
// for (const itemSet of createItemSets) {
// const newSub = await createStripeSub({
// db,
// attachParams,
// itemSet,
// });
// updatedSubs.push(newSub);
// invoices.push(newSub.latest_invoice as Stripe.Invoice);
// }
// // 2. Update or cancel old subs
// for (const newItemSet of subToNewItems) {
// if (newItemSet.shouldCancel) {
// await stripeCli.subscriptions.cancel(newItemSet.sub.id, {
// prorate: false,
// cancellation_details: {
// comment: "autumn_upgrade",
// },
// });
// } else {
// const intervalConfig = subToAutumnInterval(newItemSet.sub);
// const { updatedSub, latestInvoice } = await updateStripeSub({
// req,
// attachParams,
// config: {
// ...config,
// proration: ProrationBehavior.None,
// },
// stripeSubs: [newItemSet.sub],
// itemSet: {
// items: newItemSet.newItems,
// interval: intervalConfig.interval,
// intervalCount: intervalConfig.intervalCount,
// } as any,
// intervalConfig: subToAutumnInterval(newItemSet.sub),
// });
// updatedSubs.push(updatedSub!);
// if (latestInvoice) {
// invoices.push(latestInvoice);
// }
// }
// }
// return {
// newSubs: stripeSubs,
// invoices,
// // invoice: latestInvoice,
// // newInvoiceIds,
// };
// };
// 2. Update / cancel old subscriptions
// // 3. Update current subscription
// logger.info("1.2: Updating current subscription");
// const { updatedSub, latestInvoice } = await updateStripeSub({
// req,
// attachParams,
// config,
// trialEnd,
// itemSet: firstItemSet,
// stripeSubs: [firstSub],
// });
// await resetUsageBalances({
// db,
// cusEntIds,
// cusProduct: curCusProduct,
// });
// let newSubs = [updatedSub!];
// const newInvoiceIds = latestInvoice ? [latestInvoice.id] : [];
// // 4. Update current sub schedules if exist...
// logger.info("1.3 Updating current sub schedules");
// await updateCurSchedules({
// db,
// stripeCli,
// curCusProduct,
// attachParams,
// itemSets,
// logger,
// });
// // 5. Cancel other subscriptions
// for (const sub of stripeSubs.slice(1)) {
// logger.info(`1.4: canceling additional sub: ${sub.id}`);
// // Filter out
// await stripeCli.subscriptions.cancel(sub.id, {
// prorate: true,
// cancellation_details: {
// comment: "autumn_upgrade",
// },
// });
// }
// // 6. Create subs for other intervals
// for (const itemSet of itemSets.slice(1)) {
// const newSub = await createStripeSub({
// db,
// stripeCli,
// customer: attachParams.customer,
// org: attachParams.org,
// itemSet,
// invoiceOnly: attachParams.invoiceOnly || false,
// freeTrial: attachParams.freeTrial,
// // anchorToUnix: updatedSub!.current_period_end! * 1000,
// now: attachParams.now,
// });
// newSubs.push(newSub);
// const latestInvoice = newSub.latest_invoice as Stripe.Invoice;
// newInvoiceIds.push(latestInvoice.id);
// }
// const firstSub = stripeSubs?.[0];
// const firstItemSet = itemSets?.[0];
// await addSubItemsToRemove({
// sub: firstSub,
// cusProduct: curCusProduct,
// itemSet: firstItemSet,
// });
// throw new Error("Stop");

View File

@@ -0,0 +1,42 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { CusProductStatus, FullCusProduct, FullCustomer } from "@autumn/shared";
import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
export const cancelEndOfCycle = async ({
req,
cusProduct,
fullCus,
}: {
req: ExtendedRequest;
cusProduct: FullCusProduct;
fullCus: FullCustomer;
}) => {
const { db, org, env, logger } = req;
const stripeCli = createStripeCli({ org, env });
const sub = await cusProductToSub({ cusProduct, stripeCli });
if (sub) {
const latestPeriodEnd = getLatestPeriodEnd({ sub });
await stripeCli.subscriptions.update(sub.id, {
cancel_at: latestPeriodEnd,
});
await CusProductService.update({
db,
cusProductId: cusProduct.id,
updates: { canceled_at: Date.now() },
});
} else {
await CusProductService.update({
db,
cusProductId: cusProduct.id,
updates: {
status: CusProductStatus.Expired,
ended_at: Date.now(),
},
});
}
};

View File

@@ -0,0 +1,61 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { CusProductStatus, FullCusProduct, FullCustomer } from "@autumn/shared";
import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { activateDefaultProduct } from "../cusProducts/cusProductUtils.js";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
export const cancelImmediately = async ({
req,
cusProduct,
fullCus,
prorate,
}: {
req: ExtendedRequest;
cusProduct: FullCusProduct;
fullCus: FullCustomer;
prorate: boolean;
}) => {
const { db, org, env, logger } = req;
const stripeCli = createStripeCli({ org, env });
const { curScheduledProduct } = getExistingCusProducts({
product: cusProduct.product,
cusProducts: fullCus.customer_products,
internalEntityId: cusProduct.internal_entity_id,
});
const sub = await cusProductToSub({ cusProduct, stripeCli });
if (sub) {
await stripeCli.subscriptions.cancel(sub.id, { prorate: prorate });
}
const isMain = !cusProduct.product.is_add_on;
if (isMain) {
// So it doesn't duplicate
if (curScheduledProduct) {
await CusProductService.delete({
db,
cusProductId: curScheduledProduct.id,
});
}
await activateDefaultProduct({
req,
productGroup: cusProduct.product.group,
fullCus,
});
}
await CusProductService.update({
db,
cusProductId: cusProduct.id,
updates: {
status: CusProductStatus.Expired,
ended_at: Date.now(),
},
});
};

View File

@@ -6,10 +6,11 @@ import { Router } from "express";
import { expireCusProduct } from "../handlers/handleCusProductExpired.js";
import { RELEVANT_STATUSES } from "../cusProducts/CusProductService.js";
import { nullish } from "@/utils/genUtils.js";
import { handleCancelProduct } from "./handleCancelProduct.js";
const expireRouter: Router = Router();
const cancelRouter: Router = Router();
expireRouter.post("", async (req, res) =>
cancelRouter.post("", async (req, res) =>
routeHandler({
req,
res,
@@ -57,7 +58,15 @@ expireRouter.post("", async (req, res) =>
});
}
await expireCusProduct({
// await expireCusProduct({
// req,
// cusProduct,
// fullCus,
// expireImmediately,
// prorate,
// });
await handleCancelProduct({
req,
cusProduct,
fullCus,
@@ -74,4 +83,4 @@ expireRouter.post("", async (req, res) =>
})
);
export default expireRouter;
export default cancelRouter;

View File

@@ -0,0 +1,57 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { FullCusProduct, FullCustomer, CusProductStatus } from "@autumn/shared";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { cusProductToSchedule } from "../cusProducts/cusProductUtils/convertCusProduct.js";
export const cancelScheduledProduct = async ({
req,
curScheduledProduct,
fullCus,
curMainProduct,
}: {
req: ExtendedRequest;
curScheduledProduct?: FullCusProduct;
fullCus: FullCustomer;
curMainProduct?: FullCusProduct;
}) => {
const { org, env, db, logger } = req;
const stripeCli = createStripeCli({ org, env });
// 1. Delete subscription schedule if exists
if (curScheduledProduct) {
const { schedule, prices } = await cusProductToSchedule({
cusProduct: curScheduledProduct,
stripeCli,
});
if (schedule) {
await stripeCli.subscriptionSchedules.cancel(schedule.id);
}
logger.info(`Deleting scheduled prod (${curScheduledProduct.product.id})`);
await CusProductService.delete({
db,
cusProductId: curScheduledProduct.id,
});
}
// 2. Uncancel current main product
const subId = curMainProduct?.subscription_ids?.[0];
if (subId) {
await stripeCli.subscriptions.update(subId, { cancel_at: null });
}
if (curMainProduct) {
logger.info(`Updating main prod (${curMainProduct!.product.id}) to active`);
logger.info(`Cus product ID: ${curMainProduct!.id}`);
await CusProductService.update({
db,
cusProductId: curMainProduct!.id,
updates: {
status: CusProductStatus.Active,
canceled_at: null,
},
});
}
};

View File

@@ -0,0 +1,128 @@
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
CusProductStatus,
ErrCode,
FullCusProduct,
FullCustomer,
} from "@autumn/shared";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
import {
cusProductToPrices,
cusProductToProduct,
cusProductToSchedule,
} from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
import Stripe from "stripe";
import RecaseError from "@/utils/errorUtils.js";
import { StatusCodes } from "http-status-codes";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { cancelEndOfCycle } from "./cancelEndOfCycle.js";
import { cancelImmediately } from "./cancelImmediately.js";
import { cancelScheduledProduct } from "./cancelScheduledProduct.js";
export const handleCancelProduct = async ({
req,
cusProduct, // cus product to expire
fullCus,
expireImmediately = true,
prorate,
}: {
req: ExtendedRequest;
cusProduct: FullCusProduct;
fullCus: FullCustomer;
expireImmediately: boolean;
prorate: boolean;
}) => {
const { db, org, env, logger } = req;
logger.info("--------------------------------");
logger.info(
`🔔 Expiring cutomer product (${
expireImmediately ? "immediately" : "end of cycle"
})`
);
logger.info(
`Customer: ${fullCus.id || fullCus.internal_id} (${env}), Org: ${org.id}`
);
logger.info(
`Product: ${cusProduct.product.name}, Status: ${cusProduct.status}`
);
const { curMainProduct, curSameProduct, curScheduledProduct } =
getExistingCusProducts({
product: cusProductToProduct({ cusProduct }),
cusProducts: fullCus.customer_products,
internalEntityId: cusProduct.internal_entity_id,
});
const stripeCli = createStripeCli({ org, env });
// 1. Build attach params
if (cusProduct.status == CusProductStatus.Scheduled) {
await cancelScheduledProduct({
req,
curScheduledProduct,
fullCus,
curMainProduct,
});
return;
}
// 2. If there's a scheduled product, throw error?
const isMain = !cusProduct.product.is_add_on;
if (isMain) {
if (cusProduct.canceled_at && !expireImmediately) {
throw new RecaseError({
message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
code: ErrCode.InvalidRequest,
statusCode: StatusCodes.BAD_REQUEST,
});
}
if (
curScheduledProduct &&
!isFreeProduct(cusProductToPrices({ cusProduct: curScheduledProduct }))
) {
throw new RecaseError({
message: `Please delete scheduled product ${curScheduledProduct.product.name} first`,
code: ErrCode.InvalidRequest,
statusCode: StatusCodes.BAD_REQUEST,
});
}
}
// 2. If expire at cycle end, just cancel subscriptions
if (!expireImmediately) {
await cancelEndOfCycle({
req,
cusProduct,
fullCus,
});
return;
}
// Expire product immediately
await cancelImmediately({
req,
cusProduct,
fullCus,
prorate,
});
};
// if (isOneOff(cusProduct.customer_prices.map((p) => p.price))) {
// await CusProductService.update({
// db,
// cusProductId: cusProduct.id,
// updates: { status: CusProductStatus.Expired },
// });
// } else {
// await CusProductService.update({
// db,
// cusProductId: cusProduct.id,
// updates: { canceled_at: Date.now() },
// });
// }
// return;

View File

@@ -19,52 +19,46 @@ export const cancelCurSubs = async ({
curSubs: Stripe.Subscription[];
stripeCli: Stripe;
}) => {
let latestPeriodEnd = curSubs[0].current_period_end;
let intervalToOtherSubs: Record<
string,
{
otherSubItems: Stripe.SubscriptionItem[];
otherSub: Stripe.Subscription;
}
> = {};
for (const sub of curSubs) {
let latestEndDate = new Date(latestPeriodEnd * 1000);
let curEndDate = new Date(sub.current_period_end * 1000);
const { otherSubItems } = await getSubItemsForCusProduct({
stripeSub: sub,
cusProduct: curCusProduct,
});
// let interval = sub.items.data[0].price.recurring!.interval;
let subInterval = subToAutumnInterval(sub);
let intervalKey = toIntervalKey(subInterval);
intervalToOtherSubs[intervalKey] = {
otherSubItems,
otherSub: sub,
};
if (notNullish(sub.schedule)) {
await stripeCli.subscriptionSchedules.release(sub.schedule as string);
}
if (differenceInDays(latestEndDate, curEndDate) > 10) {
await stripeCli.subscriptions.update(sub.id, {
cancel_at: latestPeriodEnd,
cancellation_details: {
comment: "autumn_downgrade",
},
});
} else {
await stripeCli.subscriptions.update(sub.id, {
cancel_at_period_end: true,
cancellation_details: {
comment: "autumn_downgrade",
},
});
}
}
return intervalToOtherSubs;
// let latestPeriodEnd = curSubs[0].current_period_end;
// let intervalToOtherSubs: Record<
// string,
// {
// otherSubItems: Stripe.SubscriptionItem[];
// otherSub: Stripe.Subscription;
// }
// > = {};
// for (const sub of curSubs) {
// let latestEndDate = new Date(latestPeriodEnd * 1000);
// let curEndDate = new Date(sub.current_period_end * 1000);
// const { otherSubItems } = await getSubItemsForCusProduct({
// stripeSub: sub,
// cusProduct: curCusProduct,
// });
// // let interval = sub.items.data[0].price.recurring!.interval;
// let subInterval = subToAutumnInterval(sub);
// let intervalKey = toIntervalKey(subInterval);
// intervalToOtherSubs[intervalKey] = {
// otherSubItems,
// otherSub: sub,
// };
// if (notNullish(sub.schedule)) {
// await stripeCli.subscriptionSchedules.release(sub.schedule as string);
// }
// if (differenceInDays(latestEndDate, curEndDate) > 10) {
// await stripeCli.subscriptions.update(sub.id, {
// cancel_at: latestPeriodEnd,
// cancellation_details: {
// comment: "autumn_downgrade",
// },
// });
// } else {
// await stripeCli.subscriptions.update(sub.id, {
// cancel_at_period_end: true,
// cancellation_details: {
// comment: "autumn_downgrade",
// },
// });
// }
// }
// return intervalToOtherSubs;
};

View File

@@ -11,11 +11,11 @@ import {
Product,
} from "@autumn/shared";
import Stripe from "stripe";
import { fullCusProductToProduct } from "../cusProducts/cusProductUtils.js";
import {
fullCusProductToProduct,
isActiveStatus,
} from "../cusProducts/cusProductUtils.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
ACTIVE_STATUSES,
CusProductService,
} from "../cusProducts/CusProductService.js";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
@@ -144,7 +144,7 @@ export const cancelFutureProductSchedule = async ({
// 1. Remove cur scheduled product items from schedule
const activeCusProducts = cusProducts.filter((cusProduct) =>
isActiveStatus(cusProduct?.status)
ACTIVE_STATUSES.includes(cusProduct?.status)
);
const filteredScheduleItems = getFilteredScheduleItems({

View File

@@ -42,13 +42,8 @@ import { initStripeCusAndProducts } from "../handlers/handleCreateCustomer.js";
import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handleAddProduct.js";
import { newCusToAttachParams } from "../attach/attachUtils/attachParams/convertToParams.js";
export const isActiveStatus = (status: CusProductStatus) => {
return (
status === CusProductStatus.Active || status === CusProductStatus.PastDue
);
};
// 1. Cancel cusProductSubscriptions
// CAN DELETE
export const cancelCusProductSubscriptions = async ({
cusProduct,
org,
@@ -79,7 +74,7 @@ export const cancelCusProductSubscriptions = async ({
subIds: cusProduct.subscription_ids,
});
latestSubEnd = stripeSubs?.[0]?.current_period_end;
latestSubEnd = stripeSubs?.[0]?.items.data[0].current_period_end;
}
const cancelStripeSub = async (subId: string) => {
@@ -308,7 +303,7 @@ export const processFullCusProduct = ({
}: {
cusProduct: FullCusProduct;
org: Organization;
subs?: (Stripe.Subscription | Subscription)[];
subs?: Subscription[];
entities?: Entity[];
apiVersion: number;
}) => {

View File

@@ -142,6 +142,60 @@ export const cusProductsToSchedules = ({
});
};
export const cusProductToSchedule = async ({
cusProduct,
stripeCli,
}: {
cusProduct: FullCusProduct;
stripeCli: Stripe;
}) => {
const subScheduleIds = cusProduct?.scheduled_ids || [];
if (subScheduleIds.length === 0) {
return {
schedule: null,
prices: [],
};
}
const schedule = await stripeCli.subscriptionSchedules.retrieve(
subScheduleIds[0]
);
if (schedule.status == "canceled") {
return {
schedule: null,
prices: [],
};
}
const batchPricesGet = [];
for (const item of schedule.phases[0].items) {
batchPricesGet.push(stripeCli.prices.retrieve(item.price as string));
}
const prices = await Promise.all(batchPricesGet);
return {
schedule,
prices,
};
};
export const cusProductToSub = async ({
cusProduct,
stripeCli,
}: {
cusProduct?: FullCusProduct;
stripeCli: Stripe;
}) => {
const subId = cusProduct?.subscription_ids?.[0];
if (!subId) {
return undefined;
}
const sub = await stripeCli.subscriptions.retrieve(subId);
return sub;
};
export const cusProductsToStripeSubs = ({
cusProducts,
stripeCli,

View File

@@ -99,8 +99,6 @@ export const expireCusProduct = async ({
`Product: ${cusProduct.product.name}, Status: ${cusProduct.status}`
);
// If current product is scheduled
if (cusProduct.status == CusProductStatus.Scheduled) {
await removeScheduledProduct({
req,

View File

@@ -1,68 +0,0 @@
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import {
AppEnv,
AttachScenario,
FullCusProduct,
FullProduct,
Organization,
} from "@autumn/shared";
import { Customer } from "@autumn/shared";
export const getDowngradePreview = async ({
customer,
org,
env,
product,
curMainProduct,
curScheduledProduct,
}: {
customer: Customer;
org: Organization;
env: AppEnv;
product: FullProduct;
curMainProduct: FullCusProduct;
curScheduledProduct: FullCusProduct;
}) => {
let stripeCli = createStripeCli({ org, env });
// 1. Get latest period end:
const curSubscriptions = await getStripeSubs({
stripeCli,
subIds: curMainProduct.subscription_ids!,
});
curSubscriptions.sort((a, b) => b.current_period_end - a.current_period_end);
const latestPeriodEnd = curSubscriptions[0].current_period_end;
let endDate = formatUnixToDate(latestPeriodEnd * 1000);
let newProductFree = isFreeProduct(product.prices);
let message = `By clicking confirm, your current subscription to ${
curMainProduct.product.name
} will end on ${endDate}${
!newProductFree
? ` and a new subscription to ${product.name} will begin.`
: "."
}`;
return {
title: isFreeProduct(product.prices)
? `Cancel subscription to ${curMainProduct.product.name}`
: `Downgrade to ${product.name}`,
message,
scenario: isFreeProduct(product.prices)
? AttachScenario.Cancel
: AttachScenario.Downgrade,
product_id: product.id,
product_name: product.name,
recurring: !isOneOff(product.prices),
next_cycle_at: latestPeriodEnd * 1000,
current_product_name: curMainProduct.product.name,
error_on_attach: false,
};
};

View File

@@ -27,10 +27,10 @@ import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProduct
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
export const getCurContUseItems = async ({
stripeSubs,
sub,
attachParams,
}: {
stripeSubs: Stripe.Subscription[];
sub: Stripe.Subscription;
attachParams: AttachParams;
}) => {
const { features } = attachParams;
@@ -44,66 +44,64 @@ export const getCurContUseItems = async ({
let items: PreviewLineItem[] = [];
let now = attachParams.now || Date.now();
for (const sub of stripeSubs) {
for (const item of sub.items.data) {
const price = findPriceInStripeItems({
prices: curPrices,
subItem: item,
billingType: BillingType.InArrearProrated,
for (const item of sub.items.data) {
const price = findPriceInStripeItems({
prices: curPrices,
subItem: item,
billingType: BillingType.InArrearProrated,
});
if (!price) continue;
const periodEnd = item.current_period_end * 1000;
const totalAmountCents = getSubItemAmount({ subItem: item });
const totalAmount = new Decimal(totalAmountCents).div(100).toNumber();
const ent = getPriceEntitlement(price, curEnts);
if (now < periodEnd) {
const finalProration = getProration({
now,
interval: price.config.interval!,
intervalCount: price.config.interval_count || 1,
anchorToUnix: periodEnd,
})!;
const proratedAmount = -calculateProrationAmount({
periodEnd: finalProration?.end,
periodStart: finalProration?.start,
now,
amount: totalAmount,
});
if (!price) continue;
const existingUsage = getExistingUsageFromCusProducts({
entitlement: ent!,
cusProducts: [curCusProduct],
entities: attachParams.entities,
carryExistingUsages: true,
internalEntityId: attachParams.internalEntityId,
});
const totalAmountCents = getSubItemAmount({ subItem: item });
const totalAmount = new Decimal(totalAmountCents).div(100).toNumber();
const periodEnd = sub.current_period_end * 1000;
const ent = getPriceEntitlement(price, curEnts);
const feature = priceToFeature({ price, features });
if (now < periodEnd) {
const finalProration = getProration({
now,
interval: price.config.interval!,
intervalCount: price.config.interval_count || 1,
anchorToUnix: sub.current_period_end * 1000,
})!;
let description = getFeatureInvoiceDescription({
feature: feature!,
usage: existingUsage,
billingUnits: (price.config as UsagePriceConfig).billing_units,
prodName: curMainProduct?.product.name,
});
const proratedAmount = -calculateProrationAmount({
periodEnd: finalProration?.end,
periodStart: finalProration?.start,
now,
amount: totalAmount,
});
description = `Unused ${description} (from ${formatUnixToDate(now)})`;
const existingUsage = getExistingUsageFromCusProducts({
entitlement: ent!,
cusProducts: [curCusProduct],
entities: attachParams.entities,
carryExistingUsages: true,
internalEntityId: attachParams.internalEntityId,
});
const feature = priceToFeature({ price, features });
let description = getFeatureInvoiceDescription({
feature: feature!,
usage: existingUsage,
billingUnits: (price.config as UsagePriceConfig).billing_units,
prodName: curMainProduct?.product.name,
});
description = `Unused ${description} (from ${formatUnixToDate(now)})`;
items.push({
price: formatAmount({
org: attachParams.org,
amount: proratedAmount,
}),
description,
items.push({
price: formatAmount({
org: attachParams.org,
amount: proratedAmount,
usage_model: priceToUsageModel(price),
price_id: price.id!,
});
}
}),
description,
amount: proratedAmount,
usage_model: priceToUsageModel(price),
price_id: price.id!,
});
}
}

View File

@@ -30,14 +30,16 @@ import { getContUseInvoiceItems } from "@/internal/customers/attach/attachUtils/
import { isTrialing } from "@/internal/customers/cusProducts/cusProductUtils.js";
export const getItemsForCurProduct = async ({
stripeSubs,
// stripeSubs,
sub,
attachParams,
branch,
config,
now,
logger,
}: {
stripeSubs: Stripe.Subscription[];
// stripeSubs: Stripe.Subscription[];
sub?: Stripe.Subscription;
attachParams: AttachParams;
branch: AttachBranch;
config: AttachConfig;
@@ -53,77 +55,78 @@ export const getItemsForCurProduct = async ({
const curPrices = cusProductToPrices({ cusProduct: curCusProduct });
let items: PreviewLineItem[] = [];
const subItems = sub?.items.data || [];
let onTrial = isTrialing(curCusProduct);
for (const sub of stripeSubs) {
for (const item of sub.items.data) {
const price = findPriceInStripeItems({
prices: curPrices,
subItem: item,
for (const item of subItems) {
const price = findPriceInStripeItems({
prices: curPrices,
subItem: item,
});
if (!price) continue;
const billingType = getBillingType(price.config);
if (
billingType == BillingType.UsageInArrear ||
billingType == BillingType.InArrearProrated
)
continue;
const totalAmountCents = getSubItemAmount({ subItem: item });
let totalAmount = new Decimal(totalAmountCents).div(100).toNumber();
if (onTrial) {
totalAmount = 0;
}
// const periodEnd = sub.items.data[0].current_period_end * 1000;
const periodEnd = item.current_period_end * 1000;
const ents = cusProductToEnts({ cusProduct: curCusProduct });
const ent = getPriceEntitlement(price, ents);
if (now < periodEnd) {
const finalProration = getProration({
now,
interval: price.config.interval!,
intervalCount: price.config.interval_count || 1,
anchorToUnix: periodEnd,
})!;
const proratedAmount = -calculateProrationAmount({
periodEnd: finalProration?.end,
periodStart: finalProration?.start,
now,
amount: totalAmount,
});
if (!price) continue;
const billingType = getBillingType(price.config);
let description = priceToInvoiceDescription({
price,
org: attachParams.org,
cusProduct: curCusProduct,
quantity: item.quantity,
logger,
});
if (
billingType == BillingType.UsageInArrear ||
billingType == BillingType.InArrearProrated
)
continue;
description = `Unused ${description} (from ${formatUnixToDate(now)})`;
const totalAmountCents = getSubItemAmount({ subItem: item });
let totalAmount = new Decimal(totalAmountCents).div(100).toNumber();
if (onTrial) {
totalAmount = 0;
}
const periodEnd = sub.current_period_end * 1000;
const ents = cusProductToEnts({ cusProduct: curCusProduct });
const ent = getPriceEntitlement(price, ents);
if (now < periodEnd) {
const finalProration = getProration({
now,
interval: price.config.interval!,
intervalCount: price.config.interval_count || 1,
anchorToUnix: sub.current_period_end * 1000,
})!;
const proratedAmount = -calculateProrationAmount({
periodEnd: finalProration?.end,
periodStart: finalProration?.start,
now,
amount: totalAmount,
});
let description = priceToInvoiceDescription({
price,
items.push({
price: formatAmount({
org: attachParams.org,
cusProduct: curCusProduct,
quantity: item.quantity,
logger,
});
description = `Unused ${description} (from ${formatUnixToDate(now)})`;
items.push({
price: formatAmount({
org: attachParams.org,
amount: proratedAmount,
}),
description,
amount: proratedAmount,
usage_model: priceToUsageModel(price),
price_id: price.id!,
feature_id: ent?.feature.id,
});
}
}),
description,
amount: proratedAmount,
usage_model: priceToUsageModel(price),
price_id: price.id!,
feature_id: ent?.feature.id,
});
}
}
// }
let { oldItems, newItems } = await getContUseInvoiceItems({
stripeSubs,
sub,
attachParams,
logger,
cusProduct: curCusProduct,

View File

@@ -33,8 +33,13 @@ import { calculateProrationAmount } from "../prorationUtils.js";
import { getPricecnPrice } from "../../products/pricecn/pricecnUtils.js";
import { toProductItem } from "../../products/product-items/mapToItem.js";
import { formatAmount } from "@/utils/formatUtils.js";
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
import {
formatUnixToDate,
formatUnixToDateTime,
notNullish,
} from "@/utils/genUtils.js";
import {
addBillingIntervalUnix,
getAlignedIntervalUnix,
subtractBillingIntervalUnix,
} from "../../products/prices/billingIntervalUtils.js";
@@ -51,7 +56,6 @@ import {
import { sortPricesByType } from "@/internal/products/prices/priceUtils/sortPriceUtils.js";
import { getMergeCusProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/getMergeCusProduct.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import { intervalsDifferent } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
export const getDefaultPriceStr = ({
org,
@@ -117,6 +121,11 @@ export const getProration = ({
intervalCount,
});
// console.log(`Anchor to unix: ${formatUnixToDateTime(anchorToUnix)}`);
// console.log(`Start: ${formatUnixToDateTime(start)}`);
// console.log(`End: ${formatUnixToDateTime(end)}`);
// console.log(`--------------------------------`);
return {
start,
end: end!,
@@ -128,11 +137,9 @@ export const getItemsForNewProduct = async ({
attachParams,
now,
proration,
interval,
intervalCount,
anchorToUnix,
freeTrial,
stripeSubs,
sub,
logger,
withPrepaid = false,
branch,
@@ -145,11 +152,10 @@ export const getItemsForNewProduct = async ({
start: number;
end: number;
};
interval?: BillingInterval;
intervalCount?: number;
anchorToUnix?: number;
freeTrial?: FreeTrial | null;
stripeSubs?: Stripe.Subscription[];
sub?: Stripe.Subscription;
logger: any;
withPrepaid?: boolean;
branch: AttachBranch;
@@ -158,6 +164,8 @@ export const getItemsForNewProduct = async ({
const { org, features } = attachParams;
now = now || Date.now();
// console.log("Anchoring to", formatUnixToDateTime(anchorToUnix));
const items: PreviewLineItem[] = [];
sortPricesByType(newProduct.prices);
@@ -166,21 +174,6 @@ export const getItemsForNewProduct = async ({
const ent = getPriceEntitlement(price, newProduct.entitlements);
const billingType = getBillingType(price.config);
if (
interval &&
intervalsDifferent({
intervalA: {
interval: interval,
intervalCount: intervalCount,
},
intervalB: {
interval: price.config.interval,
intervalCount: price.config.interval_count,
},
})
)
continue;
const finalProration = getProration({
proration,
anchorToUnix,
@@ -279,7 +272,7 @@ export const getItemsForNewProduct = async ({
let { newItems } = await getContUseInvoiceItems({
cusProduct,
stripeSubs,
sub,
attachParams,
logger,
});

View File

@@ -137,6 +137,19 @@ export const getAlignedIntervalUnix = ({
const maxIterations = 10000;
let iterations = 0;
const printLogs = false;
if (printLogs) {
console.log(
"Natural billing date:",
formatUnixToDateTime(naturalBillingDate)
);
console.log(
"Next cycle anchor unix:",
formatUnixToDateTime(nextCycleAnchorUnix)
);
}
while (true) {
const subtractedUnix = subtractBillingIntervalUnix({
unixTimestamp: nextCycleAnchorUnix,
@@ -144,7 +157,9 @@ export const getAlignedIntervalUnix = ({
intervalCount,
});
// console.log("Subtracted unix:", formatUnixToDateTime(subtractedUnix));
if (printLogs) {
console.log("Subtracted unix:", formatUnixToDateTime(subtractedUnix));
}
if (subtractedUnix <= now) {
break;
@@ -160,8 +175,14 @@ export const getAlignedIntervalUnix = ({
let billingCycleAnchorUnix: number | undefined = nextCycleAnchorUnix;
// console.log("Next cycle anchor:", formatUnixToDateTime(nextCycleAnchorUnix));
// console.log("--------------------------------");
if (printLogs) {
console.log(
"Next cycle anchor:",
formatUnixToDateTime(nextCycleAnchorUnix)
);
console.log("Now:", formatUnixToDateTime(now));
console.log("--------------------------------");
}
let anchorAndNaturalDiff = differenceInSeconds(
naturalBillingDate,

View File

@@ -141,7 +141,7 @@ export const getSmallestInterval = ({
return compareBillingIntervals({ configA: a, configB: b });
});
const smallestInterval = allIntervals?.[0];
const smallestInterval = allIntervals?.[allIntervals.length - 1];
return {
interval: smallestInterval.interval,

View File

@@ -4,6 +4,7 @@ import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
import { and, eq, inArray } from "drizzle-orm";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export class SubService {
static async createSub({ db, sub }: { db: DrizzleCli; sub: Subscription }) {
@@ -47,8 +48,8 @@ export class SubService {
stripeId ? eq(subscriptions.stripe_id, stripeId) : undefined,
scheduleId
? eq(subscriptions.stripe_schedule_id, scheduleId)
: undefined,
),
: undefined
)
);
if (data.length == 0) {
@@ -97,11 +98,12 @@ export class SubService {
db: DrizzleCli;
stripeSub: Stripe.Subscription;
}) {
const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
let results = await db
.update(subscriptions)
.set({
current_period_start: stripeSub.current_period_start,
current_period_end: stripeSub.current_period_end,
current_period_start: start,
current_period_end: end,
})
.where(eq(subscriptions.stripe_id, stripeSub.id))
.returning();

View File

@@ -21,6 +21,7 @@ import {
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
export const getUpgradeProrationInvoiceItem = ({
prevPrice,
@@ -33,6 +34,7 @@ export const getUpgradeProrationInvoiceItem = ({
onIncrease,
product,
stripeSub,
subItem,
}: {
prevPrice: number;
newPrice: number;
@@ -44,6 +46,7 @@ export const getUpgradeProrationInvoiceItem = ({
onIncrease: OnIncrease;
product: Product;
stripeSub: Stripe.Subscription;
subItem: Stripe.SubscriptionItem;
}) => {
const billingUnits = (price.config as UsagePriceConfig).billing_units;
let invoiceAmount = new Decimal(newPrice).minus(prevPrice).toNumber();
@@ -56,14 +59,14 @@ export const getUpgradeProrationInvoiceItem = ({
if (shouldProrate(onIncrease)) {
invoiceAmount = calculateProrationAmount({
periodStart: stripeSub.current_period_start * 1000,
periodEnd: stripeSub.current_period_end * 1000,
periodStart: subItem.current_period_start * 1000,
periodEnd: subItem.current_period_end * 1000,
now,
amount: invoiceAmount,
});
let start = formatUnixToDate(now);
let end = formatUnixToDate(stripeSub.current_period_end * 1000);
let end = formatUnixToDate(subItem.current_period_end * 1000);
invoiceDescription = `${invoiceDescription} (from ${start} to ${end})`;
}
@@ -76,7 +79,7 @@ export const getUpgradeProrationInvoiceItem = ({
stripeSubId: stripeSub.id,
stripeCustomerId: stripeSub.customer as string,
periodStart: Math.floor(now / 1000),
periodEnd: Math.floor(stripeSub.current_period_end * 1000),
periodEnd: Math.floor(subItem.current_period_end * 1000),
});
return invoiceItem;
@@ -87,6 +90,7 @@ export const createUpgradeProrationInvoice = async ({
cusPrice,
stripeCli,
sub,
subItem,
newPrice,
prevPrice,
newRoundedUsage,
@@ -100,6 +104,7 @@ export const createUpgradeProrationInvoice = async ({
cusPrice: FullCustomerPrice;
stripeCli: Stripe;
sub: Stripe.Subscription;
subItem: Stripe.SubscriptionItem;
newPrice: number;
prevPrice: number;
newRoundedUsage: number;
@@ -127,6 +132,7 @@ export const createUpgradeProrationInvoice = async ({
onIncrease,
product,
stripeSub: sub,
subItem,
});
let invoiceAmount =
@@ -137,7 +143,7 @@ export const createUpgradeProrationInvoice = async ({
if (invoiceAmount == 0) return;
logger.info(
`🚀 Creating invoice item: ${invoiceDescription} - ${invoiceAmount.toFixed(2)}`,
`🚀 Creating invoice item: ${invoiceDescription} - ${invoiceAmount.toFixed(2)}`
);
await stripeCli.invoiceItems.create(invoiceItem);

View File

@@ -1,6 +1,5 @@
import {
FullCusEntWithFullCusProduct,
FullCusEntWithProduct,
FullCustomerPrice,
InsertReplaceable,
OnDecrease,
@@ -36,6 +35,7 @@ export const createDowngradeProrationInvoice = async ({
cusPrice,
stripeCli,
sub,
subItem,
newPrice,
prevPrice,
newRoundedUsage,
@@ -48,6 +48,7 @@ export const createDowngradeProrationInvoice = async ({
cusPrice: FullCustomerPrice;
stripeCli: Stripe;
sub: Stripe.Subscription;
subItem: Stripe.SubscriptionItem;
newPrice: number;
prevPrice: number;
newRoundedUsage: number;
@@ -72,15 +73,15 @@ export const createDowngradeProrationInvoice = async ({
});
invoiceAmount = calculateProrationAmount({
periodStart: sub.current_period_start * 1000,
periodEnd: sub.current_period_end * 1000,
periodStart: subItem.current_period_start * 1000,
periodEnd: subItem.current_period_end * 1000,
now,
amount: invoiceAmount,
allowNegative: true,
});
let start = formatUnixToDate(now);
let end = formatUnixToDate(sub.current_period_end * 1000);
let end = formatUnixToDate(subItem.current_period_end * 1000);
invoiceDescription = `${invoiceDescription} (from ${start} to ${end})`;
if (invoiceAmount == 0) return;
@@ -98,7 +99,7 @@ export const createDowngradeProrationInvoice = async ({
stripeSubId: sub.id,
stripeCustomerId: sub.customer as string,
periodStart: Math.floor(now / 1000),
periodEnd: Math.floor(sub.current_period_end * 1000),
periodEnd: Math.floor(subItem.current_period_end * 1000),
});
await stripeCli.invoiceItems.create(invoiceItem);
@@ -189,6 +190,7 @@ export const handleProratedDowngrade = async ({
cusPrice,
stripeCli,
sub,
subItem,
newPrice,
prevPrice,
newRoundedUsage: roundUsage({

View File

@@ -156,6 +156,7 @@ export const handleProratedUpgrade = async ({
cusPrice,
stripeCli,
sub,
subItem,
newPrice,
prevPrice,
newRoundedUsage,

View File

@@ -11,12 +11,13 @@ import { createProducts } from "tests/utils/productUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addHours, addMonths } from "date-fns";
import { expect } from "chai";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "customInterval1";
@@ -90,7 +91,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interva
});
it("should attach pro product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
@@ -110,7 +111,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interva
waitForSeconds: 15,
});
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,

View File

@@ -11,13 +11,14 @@ import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addHours, addMonths } from "date-fns";
import { expect } from "chai";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "customInterval2";
@@ -74,7 +75,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear p
});
it("should attach pro product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,

View File

@@ -25,7 +25,7 @@ import {
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addDays, addHours, addMonths } from "date-fns";
import { expect } from "chai";
@@ -35,6 +35,7 @@ import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUt
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { getProration } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "customInterval3";
@@ -105,7 +106,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on add on m
});
it("should attach pro product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,

View File

@@ -11,7 +11,8 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { addMonths } from "date-fns";
import { expect } from "chai";
import {
@@ -19,6 +20,7 @@ import {
expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "customInterval4";
@@ -87,7 +89,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrades for custom inter
});
it("should attach premium product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,

View File

@@ -11,10 +11,11 @@ import { createProducts } from "tests/utils/productUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { expect } from "chai";
import { Customer } from "autumn-js";
import { timeout } from "@/utils/genUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "customInterval5";
@@ -92,7 +93,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features wit
});
it("should attach pro product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,

View File

@@ -1,165 +0,0 @@
import chalk from "chalk";
import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { APIVersion, AppEnv, FullCustomer, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { expect } from "chai";
import { Customer } from "autumn-js";
import { timeout } from "@/utils/genUtils.js";
const testCase = "customInterval6";
// Update balances!
// const includedUsage = 500;
// const monthlyWords = constructFeatureItem({
// featureId: TestFeature.Words,
// includedUsage,
// });
// const biMonthlyWords = constructFeatureItem({
// featureId: TestFeature.Words,
// intervalCount: 2,
// includedUsage,
// });
// export let pro = constructProduct({
// items: [monthlyWords, biMonthlyWords],
// intervalCount: 2,
// type: "pro",
// });
// const getBreakdown = ({
// customer,
// intervalCount,
// }: {
// customer: Customer;
// intervalCount: number;
// }) => {
// const wordsFeature = customer.features[TestFeature.Words];
// // @ts-ignore
// return wordsFeature.breakdown?.find(
// (b: any) => b.interval_count == intervalCount
// );
// };
// describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features with custom intervals`)}`, () => {
// let customerId = testCase;
// let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
// let testClockId: string;
// let db: DrizzleCli, org: Organization, env: AppEnv;
// let stripeCli: Stripe;
// before(async function () {
// await setupBefore(this);
// const { autumnJs } = this;
// db = this.db;
// org = this.org;
// env = this.env;
// stripeCli = this.stripeCli;
// const { testClockId: testClockId1 } = await initCustomer({
// autumn: autumnJs,
// customerId,
// db,
// org,
// env,
// attachPm: "success",
// });
// addPrefixToProducts({
// products: [pro],
// prefix: testCase,
// });
// await createProducts({
// autumn,
// products: [pro],
// db,
// orgId: org.id,
// env,
// });
// testClockId = testClockId1!;
// });
// it("should attach pro product", async function () {
// await runAttachTest({
// autumn,
// customerId,
// product: pro,
// stripeCli,
// db,
// org,
// env,
// });
// const customer = await autumn.customers.get(customerId);
// const wordsFeature = customer.features[TestFeature.Words];
// // @ts-ignore
// expect(wordsFeature.interval_count).to.equal(null);
// expect(wordsFeature.breakdown?.length).to.equal(2);
// expect(
// wordsFeature.breakdown?.some(
// (b: any) => b.interval_count == 1 && b.interval == "month"
// )
// ).to.equal(true);
// expect(
// wordsFeature.breakdown?.some(
// (b: any) => b.interval_count == 2 && b.interval == "month"
// )
// ).to.equal(true);
// });
// const trackVal = 300;
// it("should have correct breakdown after usage", async function () {
// await autumn.track({
// customer_id: customerId,
// feature_id: TestFeature.Words,
// value: trackVal,
// });
// await timeout(3000);
// const customer = await autumn.customers.get(customerId);
// // Should deduct
// const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 });
// const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 });
// expect(monthlyBreakdown?.balance).to.equal(includedUsage - trackVal);
// expect(biMonthlyBreakdown?.balance).to.equal(includedUsage);
// await autumn.track({
// customer_id: customerId,
// feature_id: TestFeature.Words,
// value: trackVal,
// });
// await timeout(3000);
// const customer2 = await autumn.customers.get(customerId);
// const monthlyBreakdown2 = getBreakdown({
// customer: customer2,
// intervalCount: 1,
// });
// const biMonthlyBreakdown2 = getBreakdown({
// customer: customer2,
// intervalCount: 2,
// });
// expect(monthlyBreakdown2?.balance).to.equal(0);
// expect(biMonthlyBreakdown2?.balance).to.equal(includedUsage - 100);
// });
// });

View File

@@ -10,9 +10,10 @@ import { createProducts } from "tests/utils/productUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const userItem = constructArrearProratedItem({
featureId: TestFeature.Users,
@@ -96,7 +97,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`)
];
it("should attach pro product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
@@ -140,7 +141,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`)
// @ts-ignore
expect(customer.features[TestFeature.Users].usage_limit).to.equal(
userItem.usage_limit,
userItem.usage_limit
);
});
});

View File

@@ -13,9 +13,10 @@ import {
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { expect } from "chai";
import { timeout } from "@/utils/genUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const messageItem = constructArrearItem({
featureId: TestFeature.Messages,
@@ -88,7 +89,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`
});
it("should attach pro product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
@@ -125,7 +126,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`
expect(check.usage_limit!).to.equal(messageItem.usage_limit!);
// @ts-ignore
expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
messageItem.usage_limit!,
messageItem.usage_limit!
);
});
@@ -150,11 +151,11 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`
// @ts-ignore
expect(check.usage_limit!).to.equal(
messageItem.usage_limit! + addOnMessages.included_usage,
messageItem.usage_limit! + addOnMessages.included_usage
);
// @ts-ignore
expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
messageItem.usage_limit! + addOnMessages.included_usage,
messageItem.usage_limit! + addOnMessages.included_usage
);
});
@@ -178,11 +179,11 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`
expect(check.allowed).to.equal(false);
// @ts-ignore
expect(check.usage_limit!).to.equal(
messageItem.usage_limit! + addOnMessages.included_usage,
messageItem.usage_limit! + addOnMessages.included_usage
);
// @ts-ignore
expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
messageItem.usage_limit! + addOnMessages.included_usage,
messageItem.usage_limit! + addOnMessages.included_usage
);
});
});

View File

@@ -19,10 +19,11 @@ import {
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { expect } from "chai";
import { timeout } from "@/utils/genUtils.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const messageItem = constructPrepaidItem({
featureId: TestFeature.Messages,
@@ -98,7 +99,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)}
expectAutumnError({
errCode: ErrCode.InvalidOptions,
func: async () => {
return await runAttachTest({
return await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
@@ -131,7 +132,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)}
expectAutumnError({
errCode: ErrCode.InvalidOptions,
func: async () => {
return await runAttachTest({
return await attachAndExpectCorrect({
autumn,
customerId,
product: pro,

View File

@@ -20,10 +20,11 @@ import {
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { expect } from "chai";
import { timeout } from "@/utils/genUtils.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const messageItem = constructArrearProratedItem({
featureId: TestFeature.Users,
@@ -84,7 +85,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use i
});
it("should attach pro product with quantity exceeding usage limit and get an error", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,

View File

@@ -10,11 +10,11 @@ import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features, products } from "tests/global.js";
import { compareMainProduct } from "tests/utils/compare.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { APIVersion } from "@autumn/shared";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
const freeProd = constructProduct({
type: "free",

View File

@@ -72,7 +72,7 @@ describe(`${chalk.yellowBright("basic10: Multi attach, all one off")}`, () => {
const metered2Amount = metered2Tiers[0].amount;
let numBillingUnits = new Decimal(options[0].quantity).div(
oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units,
oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units
);
const expectedTotal = new Decimal(metered2Amount)

View File

@@ -1,19 +1,14 @@
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { assert, expect } from "chai";
import chalk from "chalk";
import { expect } from "chai";
import { setupBefore } from "tests/before.js";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features, products } from "tests/global.js";
import { products } from "tests/global.js";
import { compareMainProduct } from "tests/utils/compare.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { timeout } from "tests/utils/genUtils.js";
const oneTimeQuantity = 2;
const oneTimePurchaseCount = 2;
const oneTimeOverrideQuantity = 4;
const monthlyQuantity = 2;
// UNCOMMENT FROM HERE
const testCase = "basic2";
describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
@@ -57,8 +52,6 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
expect(res.invoices.length).to.be.greaterThan(0);
});
// return;
it("should have correct result when calling /check", async function () {
const proEntitlements = products.pro.entitlements;

View File

@@ -1,11 +1,8 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features, products } from "tests/global.js";
import { products } from "tests/global.js";
import chalk from "chalk";
import {
checkFeatureHasCorrectBalance,
compareMainProduct,
} from "tests/utils/compare.js";
import { compareMainProduct } from "tests/utils/compare.js";
import { expect } from "chai";
import { CusProductStatus } from "@autumn/shared";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
@@ -34,7 +31,7 @@ describe(`${chalk.yellowBright(
});
it("should attach pro product", async function () {
const res: any = await AutumnCli.attach({
await AutumnCli.attach({
customerId: customerId,
productId: products.pro.id,
});
@@ -56,6 +53,8 @@ describe(`${chalk.yellowBright(
await timeout(5000);
});
return;
it("should have pro product active, and canceled_at != null, and free scheduled", async function () {
const cusRes: any = await AutumnCli.getCustomer(customerId);
compareMainProduct({

View File

@@ -8,12 +8,13 @@ import { APIVersion, AppEnv, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts, runAttachTest } from "../utils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { expectDowngradeCorrect } from "tests/utils/expectUtils/expectScheduleUtils.js";
import { expectNextCycleCorrect } from "tests/utils/expectUtils/expectScheduleUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "downgrade1";
@@ -72,7 +73,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> p
});
it("should attach premium product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,

View File

@@ -7,7 +7,8 @@ import { APIVersion, AppEnv, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts, runAttachTest } from "../utils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
@@ -16,6 +17,7 @@ import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "downgrade2";
@@ -80,7 +82,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> f
});
it("should attach premium product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,

View File

@@ -7,13 +7,14 @@ import { APIVersion, AppEnv, Customer, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts, runAttachTest } from "../utils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructFeatureItem } from "@/internal/products/product-items/productItemUtils.js";
import { expectDowngradeCorrect } from "tests/utils/expectUtils/expectScheduleUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "downgrade3";
@@ -86,7 +87,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: premium -> pro -
});
it("should attach premium product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
@@ -153,14 +154,5 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: premium -> pro -
customer,
product: premium,
});
// await runAttachTest({
// autumn,
// customerId,
// product: premium,
// stripeCli,
// db,
// org,
// env,
// });
});
});

View File

@@ -13,7 +13,7 @@ import {
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { addPrefixToProducts, runAttachTest } from "../utils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
@@ -24,7 +24,7 @@ import {
import { createProducts } from "tests/utils/productUtils.js";
import { advanceMonths } from "tests/utils/stripeUtils.js";
import { timeout } from "@/utils/genUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "downgrade4";
let proQuarter = constructProduct({
@@ -91,7 +91,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: pro-quarter -> p
});
it("should attach pro quarterly product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: proQuarter,

View File

@@ -7,7 +7,7 @@ import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts, runAttachTest } from "../utils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
@@ -17,6 +17,7 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addWeeks } from "date-fns";
import { defaultApiVersion } from "tests/constants.js";
import { runMigrationTest } from "./runMigrationTest.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let messagesItem = constructFeatureItem({
featureId: TestFeature.Messages,
@@ -81,7 +82,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`
});
it("should attach free product", async function () {
await runAttachTest({
await attachAndExpectCorrect({
autumn,
customerId,
product: free,

Some files were not shown because too many files have changed in this diff Show More