fixing combined interval stuff
This commit is contained in:
14
bun.lock
14
bun.lock
@@ -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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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' \
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
14
server/src/external/stripe/stripeInvoiceUtils.ts
vendored
14
server/src/external/stripe/stripeInvoiceUtils.ts
vendored
@@ -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 = {
|
||||
|
||||
9
server/src/external/stripe/stripeSubUtils.ts
vendored
9
server/src/external/stripe/stripeSubUtils.ts
vendored
@@ -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 = ({
|
||||
|
||||
49
server/src/external/stripe/stripeSubUtils/convertSubUtils.ts
vendored
Normal file
49
server/src/external/stripe/stripeSubUtils/convertSubUtils.ts
vendored
Normal 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 }),
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
// };
|
||||
// }
|
||||
// };
|
||||
|
||||
@@ -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;
|
||||
// };
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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)");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -62,7 +62,7 @@ export const updateSubsDiffInt = async ({
|
||||
db,
|
||||
attachParams,
|
||||
cusProduct: curCusProduct,
|
||||
stripeSubs,
|
||||
sub: firstSub,
|
||||
logger,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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: [],
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -146,6 +146,7 @@ export const handleCheckout = (req: any, res: any) =>
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
config,
|
||||
returnCheckout: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ export const attachParamsToPreview = async ({
|
||||
attachParams,
|
||||
now,
|
||||
logger,
|
||||
branch,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || "",
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
42
server/src/internal/customers/cancel/cancelEndOfCycle.ts
Normal file
42
server/src/internal/customers/cancel/cancelEndOfCycle.ts
Normal 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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
61
server/src/internal/customers/cancel/cancelImmediately.ts
Normal file
61
server/src/internal/customers/cancel/cancelImmediately.ts
Normal 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(),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
128
server/src/internal/customers/cancel/handleCancelProduct.ts
Normal file
128
server/src/internal/customers/cancel/handleCancelProduct.ts
Normal 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;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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!,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -156,6 +156,7 @@ export const handleProratedUpgrade = async ({
|
||||
cusPrice,
|
||||
stripeCli,
|
||||
sub,
|
||||
subItem,
|
||||
newPrice,
|
||||
prevPrice,
|
||||
newRoundedUsage,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user