fix: ts types

This commit is contained in:
John Yeo
2025-08-17 13:43:25 -07:00
parent 3e52f39d56
commit 6d5008f487
14 changed files with 63 additions and 277 deletions

View File

@@ -1,4 +1,6 @@
MOCHA_PARALLEL=true npx mocha 'tests/alex/00_setup.ts' && npx mocha --parallel --timeout 10000000 \
# npx mocha 'tests/alex/00_setup.ts' --timeout 10000000
MOCHA_PARALLEL=true npx mocha --parallel --timeout 10000000 \
'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \
'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \
--ignore 'tests/alex/00_setup.ts'

View File

@@ -62,7 +62,7 @@ export const findPriceInStripeItems = ({
}: {
prices: Price[];
subItem?: Stripe.SubscriptionItem;
lineItem?: Stripe.InvoiceItem;
lineItem?: Stripe.InvoiceItem | Stripe.InvoiceLineItem;
billingType?: BillingType;
}) => {
return prices.find((p: Price) => {

View File

@@ -1,180 +0,0 @@
import Stripe from "stripe";
import {
BillingInterval,
AttachConfig,
ProrationBehavior,
} from "@autumn/shared";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { createProrationInvoice } from "./updateStripeSub/createProrationinvoice.js";
import {
createUsageInvoiceItems,
resetUsageBalances,
} from "@/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.js";
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import { createAndFilterContUseItems } from "@/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.js";
export const updateStripeSubscription = async ({
db,
attachParams,
config,
trialEnd,
stripeSubs,
itemSet,
logger,
interval,
}: {
db: DrizzleCli;
attachParams: AttachParams;
config: AttachConfig;
stripeSubs: Stripe.Subscription[];
trialEnd?: number;
itemSet: ItemSet;
shouldPreview?: boolean;
logger: any;
interval?: BillingInterval;
}) => {
const { curMainProduct } = attachParamToCusProducts({ attachParams });
const { stripeCli, customer, org, paymentMethod } = attachParams;
const { invoiceOnly, proration } = config;
const curSub = stripeSubs[0];
let { items, prices } = itemSet;
let subItems = items.filter(
(i: any, index: number) =>
i.deleted || prices[index].config!.interval !== BillingInterval.OneOff
);
let subInvoiceItems = items.filter((i: any, index: number) => {
if (index < prices.length) {
return prices[index].config!.interval === BillingInterval.OneOff;
}
return false;
});
// 1. Update subscription
let sub: Stripe.Subscription | null = null;
let stripeProration =
proration == ProrationBehavior.None ? "none" : "create_prorations";
try {
sub = await stripeCli.subscriptions.update(curSub.id, {
items: subItems,
proration_behavior: stripeProration,
trial_end: trialEnd,
default_payment_method: paymentMethod?.id,
add_invoice_items: subInvoiceItems,
...((invoiceOnly && {
collection_method: "send_invoice",
days_until_due: 30,
}) as any),
payment_behavior: "error_if_incomplete",
expand: ["latest_invoice"],
});
} catch (error: any) {
throw error;
}
const latestInvoice = sub.latest_invoice as Stripe.Invoice;
if (proration == ProrationBehavior.None) {
return {
preview: null,
sub: {
...sub,
latest_invoice: latestInvoice.id,
},
invoice: null,
};
}
// 3. Create invoice items for remaining usages
let invoice: Stripe.Invoice | null = null;
let { cusEntIds } = await createUsageInvoiceItems({
db,
attachParams,
cusProduct: curMainProduct!,
stripeSubs,
logger,
});
await createAndFilterContUseItems({
attachParams,
curMainProduct: curMainProduct!,
stripeSubs,
interval,
logger,
});
if (proration === ProrationBehavior.Immediately) {
if (latestInvoice.id != curSub.latest_invoice) {
sub.latest_invoice = latestInvoice.id;
return {
preview: null,
sub,
invoice: latestInvoice,
};
}
invoice = await createProrationInvoice({
attachParams,
invoiceOnly,
curSub,
updatedSub: sub,
logger,
});
}
await resetUsageBalances({
db,
cusEntIds,
cusProduct: curMainProduct!,
});
// Upsert sub
await SubService.addUsageFeatures({
db,
stripeId: curSub.id,
usageFeatures: itemSet.usageFeatures,
orgId: org.id,
env: customer.env,
});
if (invoice) {
sub.latest_invoice = invoice.id;
} else {
sub.latest_invoice = null;
}
return {
preview: null,
sub,
invoice,
};
};
// if (shouldPreview) {
// let preview = await stripeCli.invoices.createPreview({
// subscription_details: {
// items: subItems,
// proration_behavior: getStripeProrationBehavior({
// org,
// prorationBehavior: proration,
// }) as any,
// trial_end: trialEnd as any,
// },
// subscription: curSub.id,
// invoice_items: subInvoiceItems,
// customer: customer.processor.id,
// });
// return {
// preview,
// sub: null,
// invoice: null,
// };
// }

View File

@@ -1,37 +0,0 @@
import Stripe from "stripe";
import { BillingType, Feature, FeatureUsageType } from "@autumn/shared";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { FullCusProduct } from "@autumn/shared";
import { findPriceInStripeItems } from "../stripeSubItemUtils.js";
import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
// For prepaid single use prices...
export const removePrepaidSingleUseItems = async ({
stripeCli,
curCusProduct,
features,
invoice,
}: {
stripeCli: Stripe;
curCusProduct: FullCusProduct;
features: Feature[];
invoice: Stripe.Invoice;
}) => {
const curPrices = cusProductToPrices({ cusProduct: curCusProduct });
for (const item of invoice.lines.data) {
let price = findPriceInStripeItems({
prices: curPrices,
subItem: item,
billingType: BillingType.UsageInAdvance,
});
if (!price) continue;
let feature = priceToFeature({ price, features })!;
if (feature.config.usage_type == FeatureUsageType.Single) {
console.log("Deleting item:", item.id, "Feature:", feature.id);
await stripeCli.invoiceItems.del(item.id);
}
}
};

View File

@@ -95,7 +95,7 @@ export const createAndFilterContUseItems = async ({
// return { newItems: [], oldItems: [], replaceables: [] };
// }
let { newItems, oldItems } = await getContUseInvoiceItems({
let { newItems, oldItems, replaceables } = await getContUseInvoiceItems({
attachParams,
cusProduct: curMainProduct!,
sub,
@@ -154,5 +154,5 @@ export const createAndFilterContUseItems = async ({
});
}
return { newItems, oldItems };
return { newItems, oldItems, replaceables };
};

View File

@@ -32,6 +32,7 @@ export const constructFeature = ({
type,
config,
display,
archived: false,
};
return newFeature;
@@ -58,6 +59,7 @@ export const constructBooleanFeature = ({
name: name || keyToTitle(featureId),
type: FeatureType.Boolean,
config: null,
archived: false,
};
return newFeature;
@@ -99,6 +101,7 @@ export const constructMeteredFeature = ({
},
usage_type: usageType,
},
archived: false,
};
return newFeature;
@@ -139,6 +142,7 @@ export const constructCreditSystem = ({
name: name || keyToTitle(featureId),
type: FeatureType.CreditSystem,
config,
archived: false,
};
return newFeature;

View File

@@ -39,7 +39,7 @@ export const processInvoice = ({
items: withItems
? (invoice.items || []).map((i) => {
let feature = features?.find(
(f) => f.internal_id === i.internal_feature_id,
(f) => f.internal_id === i.internal_feature_id
);
return InvoiceItemResponseSchema.parse({
@@ -84,7 +84,7 @@ export class InvoiceService {
eq(invoices.internal_customer_id, internalCustomerId),
internalEntityId
? eq(invoices.internal_entity_id, internalEntityId)
: undefined,
: undefined
),
orderBy: [desc(invoices.created_at)],
limit,
@@ -146,7 +146,7 @@ export class InvoiceService {
internal_customer_id: internalCustomerId,
product_ids: uniqueProductIds,
created_at: stripeInvoice.created * 1000,
stripe_id: stripeInvoice.id,
stripe_id: stripeInvoice.id!,
hosted_invoice_url: stripeInvoice.hosted_invoice_url || null,
status: status || (stripeInvoice.status as InvoiceStatus | null),
internal_product_ids: uniqueInternalProductIds,

View File

@@ -31,7 +31,7 @@ export const insertInvoiceFromAttach = async ({
// Create or update
let invoice = await InvoiceService.getByStripeId({
db,
stripeId: stripeInvoice.id,
stripeId: stripeInvoice.id!,
});
let autumnInvoiceItems = await getInvoiceItems({
@@ -43,7 +43,7 @@ export const insertInvoiceFromAttach = async ({
if (invoice) {
await InvoiceService.updateByStripeId({
db,
stripeId: stripeInvoice.id,
stripeId: stripeInvoice.id!,
updates: {
product_ids: attachParams.products.map((p) => p.id),
internal_product_ids: attachParams.products.map((p) => p.internal_id),
@@ -99,7 +99,7 @@ export const getInvoiceItems = async ({
for (const line of stripeInvoice.lines.data) {
let price = findPriceInStripeItems({
prices,
subItem: line,
lineItem: line,
});
if (!price) {

View File

@@ -30,13 +30,13 @@ export const createAndFinalizeInvoice = async ({
for (const item of invoiceItems) {
await stripeCli.invoiceItems.create({
...item,
invoice: invoice.id,
invoice: invoice.id!,
customer: stripeCusId,
});
}
}
let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id, {
let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id!, {
auto_advance: false,
});
@@ -47,7 +47,7 @@ export const createAndFinalizeInvoice = async ({
invoice: paidInvoice,
} = await payForInvoice({
stripeCli,
invoiceId: finalInvoice.id,
invoiceId: finalInvoice.id!,
paymentMethod,
logger,
errorOnFail: errorOnPaymentFail,

View File

@@ -303,7 +303,7 @@ export class ProductService {
parseFreeTrials({ product: data });
if (!data) {
if (allowNotFound) return null;
if (allowNotFound) return null as unknown as FullProduct;
throw new RecaseError({
message: `Product ${idOrInternalId} not found`,
code: ErrCode.ProductNotFound,

View File

@@ -20,12 +20,13 @@ import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUt
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { prices as priceTable } from "@autumn/shared";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const addProductFromSubs = async ({
req,
autumnCus,
autumnProduct,
stripeSubs,
sub,
prices,
entitlements,
force = false,
@@ -35,7 +36,7 @@ export const addProductFromSubs = async ({
req: ExtendedRequest;
autumnCus: FullCustomer;
autumnProduct: FullProduct;
stripeSubs: Stripe.Subscription[];
sub: Stripe.Subscription;
prices?: Price[];
entitlements?: EntitlementWithFeature[];
force?: boolean;
@@ -71,9 +72,7 @@ export const addProductFromSubs = async ({
}
// Handle if trialing
let trialEndsAt = stripeSubs?.[0]?.trial_end
? stripeSubs[0].trial_end * 1000
: null;
let trialEndsAt = sub?.trial_end ? sub.trial_end * 1000 : null;
// throw new Error("test");
@@ -86,6 +85,10 @@ export const addProductFromSubs = async ({
});
}
const { start, end } = subToPeriodStartEnd({
sub,
});
let newCusProduct = await createFullCusProduct({
db,
attachParams: {
@@ -108,18 +111,16 @@ export const addProductFromSubs = async ({
},
logger,
trialEndsAt: trialEndsAt || undefined,
subscriptionIds: stripeSubs.map((s) => s.id),
anchorToUnix: anchorToUnix || stripeSubs?.[0]?.current_period_end * 1000,
subscriptionIds: sub ? [sub.id] : [],
anchorToUnix: anchorToUnix || end,
subscriptionStatus: stripeSubs?.[0]?.status
? (stripeToAutumnSubStatus(stripeSubs[0].status) as CusProductStatus)
subscriptionStatus: sub?.status
? (stripeToAutumnSubStatus(sub?.status) as CusProductStatus)
: undefined,
canceledAt: stripeSubs?.[0]?.canceled_at
? stripeSubs[0].canceled_at * 1000
: null,
canceledAt: sub?.canceled_at ? sub.canceled_at * 1000 : null,
createdAt: stripeSubs?.[0]?.created ? stripeSubs[0].created * 1000 : null,
createdAt: sub?.created ? sub.created * 1000 : null,
sendWebhook: false,
});
@@ -132,31 +133,29 @@ export const addProductFromSubs = async ({
.filter((p) => isUsagePrice({ price: p }))
.map((p) => (p.config as UsagePriceConfig).internal_feature_id);
for (const sub of stripeSubs) {
let subFromDb = await SubService.getInStripeIds({
let subFromDb = await SubService.getInStripeIds({
db,
ids: [sub.id],
});
let subInterval = subToAutumnInterval(sub);
if (subFromDb.length === 0) {
await SubService.createSub({
db,
ids: [sub.id],
sub: constructSub({
stripeId: sub.id,
usageFeatures:
subInterval.interval == BillingInterval.Month ? usageFeatures : [],
orgId: org.id,
env,
currentPeriodStart: start,
currentPeriodEnd: end,
}),
});
let subInterval = subToAutumnInterval(sub);
if (subFromDb.length === 0) {
await SubService.createSub({
db,
sub: constructSub({
stripeId: sub.id,
usageFeatures:
subInterval.interval == BillingInterval.Month ? usageFeatures : [],
orgId: org.id,
env,
currentPeriodStart: sub.current_period_start,
currentPeriodEnd: sub.current_period_end,
}),
});
logger.info(`Created sub ${sub.id} in DB`);
} else {
logger.info(`Sub ${sub.id} already exists in DB`);
}
logger.info(`Created sub ${sub.id} in DB`);
} else {
logger.info(`Sub ${sub.id} already exists in DB`);
}
autumnCus.customer_products = [

View File

@@ -8,8 +8,6 @@ const DEFAULT_ENV = AppEnv.Sandbox;
describe("Initialize org for tests", () => {
it("should initialize org", async function () {
this.timeout(20000);
this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
this.env = DEFAULT_ENV;
await setupOrg({

View File

@@ -65,14 +65,14 @@ describe(chalk.yellowBright("Top ups"), () => {
const cusRes = await AutumnCli.getCustomer(customerId);
// Get product
const product = cusRes.add_ons.find(
(p: any) => p.id === alexProducts.topUpMessages.id,
(p: any) => p.id === alexProducts.topUpMessages.id
);
expect(product).to.exist;
expect(product.status).to.equal(CusProductStatus.Active);
// Check quantity is correct
let cusEnt = cusRes.entitlements.find(
(e: any) => e.feature_id === alexFeatures.topUpMessage.id,
(e: any) => e.feature_id === alexFeatures.topUpMessage.id
);
expect(cusEnt).to.exist;
@@ -86,7 +86,7 @@ describe(chalk.yellowBright("Top ups"), () => {
const { allowed, balanceObj }: any = await AutumnCli.entitled(
customerId,
alexFeatures.topUpMessage.id,
true,
true
);
leftoverBalance = balanceObj.balance;
@@ -102,18 +102,18 @@ describe(chalk.yellowBright("Top ups"), () => {
const cusRes = await AutumnCli.getCustomer(customerId);
// Get product
const product = cusRes.add_ons.find(
(p: any) => p.id === alexProducts.topUpMessages.id,
(p: any) => p.id === alexProducts.topUpMessages.id
);
expect(product).to.exist;
expect(product.status).to.equal(CusProductStatus.Active);
// Check quantity is correct
let cusEnt = cusRes.entitlements.find(
(e: any) => e.feature_id === alexFeatures.topUpMessage.id,
(e: any) => e.feature_id === alexFeatures.topUpMessage.id
);
expect(cusEnt).to.exist;
expect(cusEnt.balance).to.equal(
leftoverBalance + overrideQuantity * billingUnits,
leftoverBalance + overrideQuantity * billingUnits
);
expect(cusEnt.interval).to.equal(prodEnt.interval);
});
@@ -166,7 +166,7 @@ describe(chalk.yellowBright("Testing o1 message top up"), () => {
const cusRes = await AutumnCli.getCustomer(customerId);
// Get product
const product = cusRes.add_ons.find(
(p: any) => p.id === alexProducts.o1TopUps.id,
(p: any) => p.id === alexProducts.o1TopUps.id
);
expect(product).to.exist;

View File

@@ -184,7 +184,7 @@ export const runEventsAndCheckBalances = async ({
);
}
let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 1000;
let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 2500;
await timeout(timeoutMilli);
await Promise.all(batchUpdate);