chore: added hover texts, fixed some ui bugs

This commit is contained in:
John Yeo
2025-11-30 14:58:59 +00:00
142 changed files with 2401 additions and 1615 deletions

View File

@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "autumn",

View File

@@ -1,11 +1,15 @@
import { initializeDatabaseFunctions } from "@server/db/initializeDatabaseFunctions";
import { loadLocalEnv } from "@server/utils/envUtils";
import inquirer from "inquirer";
loadLocalEnv();
import { loadLocalEnv } from "@server/utils/envUtils";
import inquirer from "inquirer";
export const migrateFunctions = async () => {
// Dynamic import to ensure env is loaded first
const { initializeDatabaseFunctions } = await import(
"@server/db/initializeDatabaseFunctions"
);
const databaseUrl = process.env.DATABASE_URL;
console.log("databaseUrl", databaseUrl);
if (databaseUrl?.includes("us-west-3")) {
const { confirm } = await inquirer.prompt([
{

View File

@@ -11,4 +11,5 @@ BUN_PARALLEL_COMPACT \
'server/tests/attach/addOn' \
'server/tests/attach/checkout' \
'server/tests/attach/misc' \
'server/tests/billing/invoice-action-required' \
--max=6 \

View File

@@ -20,8 +20,8 @@ BUN_PARALLEL_COMPACT \
'server/tests/attach/newVersion' \
'server/tests/attach/upgradeOld' \
'server/tests/attach/updateEnts' \
'server/tests/advanced/check' \
'server/tests/attach/prepaid' \
'server/tests/attach/response' \
'server/tests/interval/upgrade' \
'server/tests/interval/multiSub' \
--max=6

View File

@@ -3,19 +3,19 @@
# Source shared configuration
source "$(dirname "$0")/config.sh"
BUN_PARALLEL_COMPACT 'server/tests/advanced/rollovers'
# BUN_PARALLEL_COMPACT \
# 'server/tests/advanced/coupons' \
# 'server/tests/advanced/misc' \
# 'server/tests/attach/updateQuantity' \
# 'server/tests/attach/multiProduct' \
# 'server/tests/advanced/multiFeature' \
# 'server/tests/advanced/referrals' \
# 'server/tests/advanced/rollovers' \
# 'server/tests/advanced/customInterval' \
# 'server/tests/advanced/usageLimit' \
# --max=6
BUN_PARALLEL_COMPACT \
'server/tests/advanced/coupons' \
'server/tests/advanced/misc' \
'server/tests/attach/updateQuantity' \
'server/tests/attach/multiProduct' \
'server/tests/advanced/multiFeature' \
'server/tests/advanced/referrals' \
'server/tests/advanced/rollovers' \
'server/tests/advanced/customInterval' \
'server/tests/advanced/usageLimit' \
--max=6
# BUN_PARALLEL_COMPACT \

View File

@@ -4,13 +4,16 @@ import { UTCDate } from "@date-fns/utc";
import { CronJob } from "cron";
import { format } from "date-fns";
import { initDrizzle } from "../db/initDrizzle.js";
import { logger } from "../external/logtail/logtailUtils.js";
import { CusEntService } from "../internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { notNullish } from "../utils/genUtils.js";
import {
clearCusEntsFromCache,
resetCustomerEntitlement,
} from "./cronUtils.js";
import { runInvoiceCron } from "./invoiceCron/runInvoiceCron.js";
import { runProductCron } from "./productCron/runProductCron.js";
import type { CronContext } from "./utils/CronContext.js";
const { db, client } = initDrizzle();
@@ -66,7 +69,17 @@ const main = async () => {
console.log(`Cron disabled!`);
return;
}
await Promise.all([cronTask(), runProductCron()]);
const ctx: CronContext = {
db,
logger,
};
await Promise.all([
cronTask(),
runProductCron(),
runInvoiceCron({ ctx }),
// TODO: Add runUsageCron({ ctx })
]);
};
new CronJob(

View File

@@ -0,0 +1,65 @@
import { type Metadata, MetadataType, metadata } from "@autumn/shared";
import { and, eq, lt } from "drizzle-orm";
import { createStripeCli } from "../../external/connect/createStripeCli";
import type { AttachParams } from "../../internal/customers/cusProducts/AttachParams";
import type { CronContext } from "../utils/CronContext";
export const handleVoidInvoiceCron = async ({
ctx,
metadata,
}: {
ctx: CronContext;
metadata: Metadata;
}) => {
const { logger } = ctx;
const data = metadata.data as AttachParams;
const { org, customer } = data;
const stripeCli = createStripeCli({ org, env: customer.env });
if (!metadata.stripe_invoice_id) {
return;
}
const invoice = await stripeCli.invoices.retrieve(metadata.stripe_invoice_id);
if (invoice.status === "open") {
try {
await stripeCli.invoices.voidInvoice(metadata.stripe_invoice_id);
logger.info(
`voided invoice ${metadata.stripe_invoice_id} for customer ${customer.id} (org: ${org.slug})`,
);
} catch (error) {
logger.error(`Error voiding invoice: ${error}`);
}
}
};
export const runInvoiceCron = async ({ ctx }: { ctx: CronContext }) => {
console.log("Running invoice cron");
const { db } = ctx;
// 1. Fetch from metadata invoices
const invoices = await db
.select()
.from(metadata)
.where(
and(
eq(metadata.type, MetadataType.InvoiceActionRequired),
lt(metadata.expires_at, Date.now()),
),
);
const batchSize = 50;
for (let i = 0; i < invoices.length; i += batchSize) {
const batch = invoices.slice(i, i + batchSize);
const promises = [];
for (const metadata of batch) {
promises.push(handleVoidInvoiceCron({ ctx, metadata }));
}
await Promise.all(promises);
console.log(`Handled ${i + batch.length}/${invoices.length} invoices`);
console.log("----------------------------------\n");
}
console.log("FINISHED INVOICE CRON");
};

View File

@@ -0,0 +1,7 @@
import type { DrizzleCli } from "../../db/initDrizzle";
import type { Logger } from "../../external/logtail/logtailUtils";
export interface CronContext {
db: DrizzleCli;
logger: Logger;
}

View File

@@ -1,8 +1,6 @@
import dotenv from "dotenv";
import "dotenv/config";
dotenv.config();
import { initLogger } from "@/errors/logger.js";
import { initLogger } from "@server/errors/logger.js";
const pinoLogger = initLogger();

View File

@@ -3,6 +3,7 @@ import {
type FullCustomer,
type Organization,
} from "@autumn/shared";
import * as Sentry from "@sentry/bun";
import chalk from "chalk";
import { Stripe } from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
@@ -14,6 +15,7 @@ import { deleteCachedApiCustomer } from "../../internal/customers/cusUtils/apiCu
import { setCachedApiInvoices } from "../../internal/customers/cusUtils/apiCusCacheUtils/setCachedApiInvoices.js";
import { setCachedApiSubs } from "../../internal/customers/cusUtils/apiCusCacheUtils/setCachedApiSubs.js";
import type { Logger } from "../logtail/logtailUtils.js";
import { getSentryTags } from "../sentry/sentryUtils.js";
import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js";
import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js";
import { handleInvoiceCreated } from "./webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.js";
@@ -148,19 +150,9 @@ const handleStripeWebhookRefresh = async ({
export const handleStripeWebhookEvent = async ({
ctx,
event,
// db,
// org,
// env,
// logger,
// req,
}: {
ctx: AutumnContext;
event: Stripe.Event;
// db: DrizzleCli;
// org: Organization;
// env: AppEnv;
// logger: Logger;
// req: ExtendedRequest;
}) => {
const { db, logger, org, env } = ctx;
logStripeWebhook({ logger, org, event });
@@ -181,35 +173,29 @@ export const handleStripeWebhookEvent = async ({
case "customer.subscription.updated": {
const subscription = event.data.object;
await handleSubscriptionUpdated({
req: ctx as unknown as ExtendedRequest,
db,
org,
ctx,
subscription,
previousAttributes: event.data.previous_attributes,
env,
logger,
});
break;
}
case "customer.subscription.deleted":
await handleSubDeleted({
req: ctx as unknown as ExtendedRequest,
ctx,
stripeCli,
data: event.data.object,
logger,
});
break;
case "checkout.session.completed": {
const checkoutSession = event.data.object;
await handleCheckoutSessionCompleted({
req: ctx as unknown as ExtendedRequest,
ctx,
db,
data: checkoutSession,
org,
env,
logger,
});
break;
}
@@ -217,12 +203,9 @@ export const handleStripeWebhookEvent = async ({
case "invoice.paid": {
const invoice = event.data.object;
await handleInvoicePaid({
db,
org,
ctx,
invoiceData: invoice,
env,
event,
req: ctx as unknown as ExtendedRequest,
});
break;
}
@@ -292,6 +275,13 @@ export const handleStripeWebhookEvent = async ({
break;
}
} catch (error) {
Sentry.captureException(error, {
tags: getSentryTags({
ctx,
method: event.type,
}),
});
if (error instanceof Stripe.errors.StripeError) {
if (error.message.includes("No such customer")) {
logger.warn(`stripe customer missing: ${error.message}`);

View File

@@ -11,6 +11,7 @@ import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import RecaseError from "@/utils/errorUtils.js";
import type { TestContext } from "../../../tests/utils/testInitUtils/createTestContext";
export const getStripeCus = async ({
stripeCli,
@@ -313,6 +314,38 @@ export const attachFailedPaymentMethod = async ({
});
};
export const attachAuthenticatePaymentMethod = async ({
ctx,
customerId,
}: {
ctx: TestContext;
customerId: string;
}) => {
const { org, env, db } = ctx;
const stripeCli = createStripeCli({ org, env });
const autumnCustomer = await CusService.get({
db,
idOrInternalId: customerId,
orgId: org.id,
env: env,
});
const stripeCustomer = await stripeCli.customers.retrieve(
autumnCustomer!.processor?.id,
);
// Delete existing payment method
const paymentMethods = await stripeCli.paymentMethods.list({
customer: stripeCustomer.id,
});
for (const pm of paymentMethods.data) {
await stripeCli.paymentMethods.detach(pm.id);
}
await stripeCli.paymentMethods.attach("pm_card_authenticationRequired", {
customer: stripeCustomer.id,
});
};
export const deleteAllStripeCustomers = async ({
org,
env,

View File

@@ -114,7 +114,11 @@ export const payForInvoice = async ({
}
if (errorOnFail) {
throw error;
throw new RecaseError({
message: error?.message,
code: ErrCode.PayInvoiceFailed,
data: invoice,
});
} else {
return {
paid: false,
@@ -122,7 +126,7 @@ export const payForInvoice = async ({
message: `Failed to pay invoice: ${error?.message || error}`,
code: ErrCode.PayInvoiceFailed,
}),
invoice: null,
invoice: invoice,
};
}
}

View File

@@ -1,7 +1,10 @@
import { ErrCode } from "@autumn/shared";
import { InternalError, MetadataType } from "@autumn/shared";
import { addMinutes } from "date-fns";
import type Stripe from "stripe";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import RecaseError from "@/utils/errorUtils.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { attachParamsToMetadata } from "../../../../internal/billing/attach/utils/attachParamsToMetadata.js";
import type { Logger } from "../../../logtail/logtailUtils.js";
import { payForInvoice } from "../../stripeInvoiceUtils.js";
export const undoSubUpdate = async ({
@@ -55,17 +58,19 @@ export const undoSubUpdate = async ({
};
export const createProrationInvoice = async ({
ctx,
attachParams,
invoiceOnly,
curSub,
updatedSub,
logger,
}: {
ctx: AutumnContext;
attachParams: AttachParams;
invoiceOnly: boolean;
curSub: Stripe.Subscription;
updatedSub: Stripe.Subscription;
logger: any;
logger: Logger;
}) => {
const { stripeCli, customer, paymentMethod } = attachParams;
@@ -77,49 +82,78 @@ export const createProrationInvoice = async ({
if (items.data.length === 0) {
logger.info(`No items to prorate, skipping invoice creation`);
return null;
return {
invoice: null,
url: null,
};
}
// const shouldMemo = attachParams.org.config.invoice_memos && invoiceOnly;
// const invoiceMemo = shouldMemo
// ? await buildInvoiceMemoFromEntitlements({
// org: attachParams.org,
// entitlements: attachParams.entitlements,
// features: attachParams.features,
// })
// : undefined;
const invoice = await stripeCli.invoices.create({
customer: customer.processor.id,
subscription: curSub.id,
// subscription: curSub.id,
auto_advance: false,
// ...(shouldMemo ? { description: invoiceMemo } : {}),
pending_invoice_items_behavior: "include",
});
if (invoiceOnly) return invoice;
if (invoiceOnly)
return {
invoice,
url: null,
};
await stripeCli.invoices.finalizeInvoice(invoice.id!, {
auto_advance: false,
});
try {
const { invoice: subInvoice } = await payForInvoice({
const {
paid,
error,
invoice: subInvoice,
} = await payForInvoice({
stripeCli,
paymentMethod: paymentMethod || null,
invoiceId: invoice.id!,
logger,
voidIfFailed: true,
voidIfFailed: false,
errorOnFail: false,
});
return subInvoice;
} catch (error: any) {
if (!paid) {
await undoSubUpdate({ stripeCli, curSub, updatedSub });
throw new RecaseError({
code: ErrCode.UpdateSubscriptionFailed,
message: `Failed to update subscription. ${error.message}`,
if (subInvoice && subInvoice.status === "open") {
logger.info(
`[update subscription] invoice action required: ${subInvoice.id}`,
);
const metadata = await attachParamsToMetadata({
db: ctx.db,
attachParams,
type: MetadataType.InvoiceActionRequired,
stripeInvoiceId: subInvoice.id,
expiresAt: addMinutes(Date.now(), 10).getTime(),
});
await stripeCli.invoices.update(subInvoice.id, {
metadata: {
autumn_metadata_id: metadata.id,
},
});
return {
invoice: subInvoice,
url: subInvoice?.hosted_invoice_url,
};
} else {
throw new InternalError({
message: `[update subscription] Failed to pay invoice: ${error?.message}`,
code: "update_subscription_failed",
statusCode: 500,
data: `Stripe error: ${error.message}`,
data: error,
});
}
}
return {
invoice: subInvoice,
url: null,
};
};

View File

@@ -8,6 +8,7 @@ import {
import type { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import { CusService } from "@/internal/customers/CusService.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
@@ -17,7 +18,6 @@ import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtil
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js";
import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js";
import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js";
@@ -25,43 +25,42 @@ import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSe
import { handleSetupCheckout } from "./handleCheckoutCompleted/handleSetupCheckout.js";
export const handleCheckoutSessionCompleted = async ({
req,
ctx,
db,
org,
data,
env,
logger,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
db: DrizzleCli;
org: Organization;
data: Stripe.Checkout.Session;
env: AppEnv;
logger: any;
}) => {
const { logger } = ctx;
const metadata = await getMetadataFromCheckoutSession(data, db);
if (!metadata) {
console.log("checkout.completed: metadata not found, skipping");
logger.info("checkout.completed: metadata not found, skipping");
return;
}
// Get options
const stripeCli = createStripeCli({ org, env });
const attachParams: AttachParams = metadata.data;
const attachParams: AttachParams = metadata.data as AttachParams;
const checkoutSession = await stripeCli.checkout.sessions.retrieve(data.id, {
expand: ["line_items", "subscription"],
});
attachParams.req = req;
attachParams.req = ctx as AutumnContext;
attachParams.stripeCli = stripeCli;
if (attachParams.org.id !== org.id) {
console.log("checkout.completed: org doesn't match, skipping");
logger.info("checkout.completed: org doesn't match, skipping");
return;
}
if (attachParams.customer.env !== env) {
console.log("checkout.completed: environments don't match, skipping");
logger.info("checkout.completed: environments don't match, skipping");
return;
}
@@ -70,14 +69,14 @@ export const handleCheckoutSessionCompleted = async ({
attachParams,
});
console.log(
logger.info(
"Handling checkout.completed: autumn metadata:",
checkoutSession.metadata?.autumn_metadata_id,
);
if (attachParams.setupPayment) {
await handleSetupCheckout({
req,
ctx,
attachParams,
});
return;
@@ -96,7 +95,7 @@ export const handleCheckoutSessionCompleted = async ({
});
if (activeCusProducts && activeCusProducts.length > 0) {
console.log("✅ checkout.completed: subscription already exists");
logger.info("✅ checkout.completed: subscription already exists");
return true;
}
}
@@ -121,14 +120,14 @@ export const handleCheckoutSessionCompleted = async ({
? getEarliestPeriodEnd({ sub: checkoutSub! }) * 1000
: undefined;
if (attachParams.productsList) {
console.log("Inserting products list");
logger.info("Inserting products list");
for (const productOptions of attachParams.productsList) {
const product = attachParams.products.find(
(p) => p.id === productOptions.product_id,
);
if (!product) {
logger.error(
ctx.logger.error(
`checkout.completed: product not found for productOptions: ${JSON.stringify(
productOptions,
)}`,
@@ -146,8 +145,8 @@ export const handleCheckoutSessionCompleted = async ({
subscriptionIds: checkoutSub ? [checkoutSub.id] : undefined,
anchorToUnix,
scenario: AttachScenario.New,
logger,
productOptions,
logger: ctx.logger,
});
}
} else {
@@ -159,12 +158,12 @@ export const handleCheckoutSessionCompleted = async ({
subscriptionIds: checkoutSub ? [checkoutSub.id] : undefined,
anchorToUnix,
scenario: AttachScenario.New,
logger,
logger: ctx.logger,
});
}
}
console.log("✅ checkout.completed: successfully created cus product");
logger.info("✅ checkout.completed: successfully created cus product");
const batchInsertInvoice: any = [];
for (const invoiceId of invoiceIds) {
@@ -179,11 +178,10 @@ export const handleCheckoutSessionCompleted = async ({
}
await Promise.all(batchInsertInvoice);
console.log("✅ checkout.completed: successfully inserted invoices");
logger.info("✅ checkout.completed: successfully inserted invoices");
for (const product of attachParams.products) {
console.log("Adding task to queue for trigger checkout reward");
console.log("Adding task to queue for trigger checkout reward");
logger.info("Adding task to queue for trigger checkout reward");
await addTaskToQueue({
jobName: JobName.TriggerCheckoutReward,
payload: {
@@ -222,6 +220,4 @@ export const handleCheckoutSessionCompleted = async ({
update: updates,
});
}
return;
};

View File

@@ -5,17 +5,17 @@ import { handleOneOffFunction } from "@/internal/customers/attach/attachFunction
import { getDefaultAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { getCusPaymentMethod } from "../../stripeCusUtils.js";
export const handleSetupCheckout = async ({
req,
ctx,
attachParams,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachParams: AttachParams;
}) => {
const logger = req.logger;
const { logger } = ctx;
const { org, customer } = attachParams;
@@ -31,16 +31,15 @@ export const handleSetupCheckout = async ({
if (isOneOff(attachParams.prices)) {
await handleOneOffFunction({
req,
ctx,
attachParams,
config: getDefaultAttachConfig(),
res: undefined,
});
return;
}
// 1. Check attach prices...
await handleAddProduct({
req,
ctx,
attachParams: {
...attachParams,
stripeCli: createStripeCli({ org, env: customer.env }),

View File

@@ -8,13 +8,13 @@ import type {
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { handleInvoiceCheckoutPaid } from "@/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import { nullish } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import {
getFullStripeInvoice,
getInvoiceDiscounts,
@@ -23,6 +23,7 @@ import {
} from "../stripeInvoiceUtils.js";
import { lineItemInCusProduct } from "../stripeSubUtils/stripeSubItemUtils.js";
import { getStripeSubs } from "../stripeSubUtils.js";
import { handleInvoicePaidMetadata } from "./handleInvoicePaid/handleInvoicePaidMetadata.js";
import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js";
const handleOneOffInvoicePaid = async ({
@@ -137,21 +138,15 @@ const convertToChargeAutomatically = async ({
};
export const handleInvoicePaid = async ({
db,
req,
org,
ctx,
invoiceData,
env,
event,
}: {
db: DrizzleCli;
req: any;
org: Organization;
ctx: AutumnContext;
invoiceData: Stripe.Invoice;
env: AppEnv;
event: Stripe.Event;
}) => {
const logger = req.logger;
const { logger, org, env, db } = ctx;
const stripeCli = createStripeCli({ org, env });
const invoice = await getFullStripeInvoice({
stripeCli,
@@ -160,12 +155,8 @@ export const handleInvoicePaid = async ({
});
if (invoice.metadata?.autumn_metadata_id) {
await handleInvoiceCheckoutPaid({
req,
org,
env,
db,
stripeCli,
await handleInvoicePaidMetadata({
ctx,
invoice,
});
}

View File

@@ -0,0 +1,74 @@
import { AttachBranch, type Metadata, ProrationBehavior } from "@autumn/shared";
import type Stripe from "stripe";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
import { resetUsageBalances } from "../../../../internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems";
import { handleUpgradeFlow } from "../../../../internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow";
import { attachParamToCusProducts } from "../../../../internal/customers/attach/attachUtils/convertAttachParams";
import { getDefaultAttachConfig } from "../../../../internal/customers/attach/attachUtils/getAttachConfig";
import type { AttachParams } from "../../../../internal/customers/cusProducts/AttachParams";
import { deleteCachedApiCustomer } from "../../../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer";
import { MetadataService } from "../../../../internal/metadata/MetadataService";
import { createStripeCli } from "../../../connect/createStripeCli";
import { getCusPaymentMethod } from "../../stripeCusUtils";
export const handleInvoiceActionRequiredCompleted = async ({
ctx,
invoice,
metadata,
}: {
ctx: AutumnContext;
invoice: Stripe.Invoice;
metadata: Metadata;
}) => {
const { logger, org, env } = ctx;
logger.info(`invoice.paid, handling action required`);
const stripeCli = createStripeCli({ org, env });
const paymentMethod = await getCusPaymentMethod({
stripeCli,
stripeId: invoice.customer as string,
});
const attachParams = {
...(metadata.data as AttachParams),
stripeCli,
req: ctx,
paymentMethod,
} as AttachParams;
const attachConfig = {
...getDefaultAttachConfig(),
proration: ProrationBehavior.None,
};
ctx.logger.info(`handling upgrade flow for invoice ${invoice.id}`);
const { curMainProduct } = attachParamToCusProducts({ attachParams });
await handleUpgradeFlow({
ctx,
attachParams,
config: attachConfig,
branch: AttachBranch.Upgrade,
});
if (attachParams.cusEntIds && curMainProduct) {
await resetUsageBalances({
db: ctx.db,
cusEntIds: attachParams.cusEntIds,
cusProduct: curMainProduct,
});
}
await MetadataService.delete({
db: ctx.db,
id: metadata.id,
});
await deleteCachedApiCustomer({
customerId: attachParams.customer.id || "",
orgId: attachParams.org.id,
env: attachParams.customer.env,
});
};

View File

@@ -0,0 +1,56 @@
import type { Metadata } from "@autumn/shared";
import { AttachScenario } from "@autumn/shared";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { deleteCachedApiCustomer } from "../../../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
export const handleInvoiceCheckoutPaid = async ({
ctx,
metadata,
}: {
ctx: AutumnContext;
metadata: Metadata;
}) => {
const { logger, org, env, db } = ctx;
const { subId, anchorToUnix, config, ...rest } =
metadata.data as AttachParams;
const attachParams = rest;
if (!attachParams) return;
const reqMatch =
attachParams.org.id === org.id && attachParams.customer.env === env;
if (!reqMatch) return;
const batchInsert = [];
for (const product of attachParams.products) {
batchInsert.push(
createFullCusProduct({
db,
attachParams: attachToInsertParams(attachParams, product),
subscriptionIds: subId ? [subId] : undefined,
anchorToUnix,
carryExistingUsages: config?.carryUsage,
scenario: AttachScenario.New,
logger: logger,
}),
);
}
await Promise.all(batchInsert);
logger.info(
`✅ invoice.paid, successfully inserted cus products: ${attachParams.products.map((p) => p.id).join(", ")}`,
);
await deleteCachedApiCustomer({
customerId: attachParams.customer.id || "",
orgId: org.id,
env,
});
};

View File

@@ -0,0 +1,45 @@
import { MetadataType } from "@autumn/shared";
import type Stripe from "stripe";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
import { MetadataService } from "../../../../internal/metadata/MetadataService";
import { handleInvoiceActionRequiredCompleted } from "./handleInvoiceActionRequiredCompleted";
import { handleInvoiceCheckoutPaid } from "./handleInvoiceCheckoutPaid";
export const handleInvoicePaidMetadata = async ({
ctx,
invoice,
}: {
ctx: AutumnContext;
invoice: Stripe.Invoice;
}) => {
const metadataId = invoice.metadata?.autumn_metadata_id;
if (!metadataId) return;
const metadata = await MetadataService.get({
db: ctx.db,
id: metadataId,
});
if (!metadata) return;
if (metadata.type === MetadataType.InvoiceActionRequired) {
await handleInvoiceActionRequiredCompleted({
ctx,
invoice,
metadata,
});
return;
}
await handleInvoiceCheckoutPaid({
ctx,
metadata,
});
await MetadataService.delete({
db: ctx.db,
id: metadata.id,
});
};

View File

@@ -38,11 +38,13 @@ const handleInvoiceCheckoutVoided = async ({
id: metadataId,
});
if (!metadata) return;
const {
anchorToUnix: _anchorToUnix,
config: _config,
...rest
} = metadata?.data || {};
} = metadata.data as AttachParams;
const attachParams = rest as AttachParams;

View File

@@ -1,6 +1,6 @@
import type Stripe from "stripe";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import {
getFullStripeSub,
subIsPrematurelyCanceled,
@@ -8,17 +8,15 @@ import {
import { handleCusProductDeleted } from "./handleSubDeleted/handleCusProductDeleted.js";
export const handleSubDeleted = async ({
req,
ctx,
stripeCli,
data,
logger,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
stripeCli: Stripe;
data: Stripe.Subscription;
logger: any;
}) => {
const { db, org, env } = req;
const { db, org, env, logger } = ctx;
const activeCusProducts = await CusProductService.getByStripeSubId({
db,
@@ -65,12 +63,11 @@ export const handleSubDeleted = async ({
// const batchUpdate = [];
for (const cusProduct of activeCusProducts) {
await handleCusProductDeleted({
req,
ctx,
db,
stripeCli,
cusProduct,
subscription,
logger,
prematurelyCanceled,
});
}

View File

@@ -17,28 +17,26 @@ import {
activateDefaultProduct,
activateFutureProduct,
} from "@/internal/customers/cusProducts/cusProductUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { getCusPaymentMethod } from "../../stripeCusUtils.js";
import { webhookToAttachParams } from "../../webhookUtils/webhookUtils.js";
export const handleCusProductDeleted = async ({
req,
ctx,
db,
stripeCli,
cusProduct,
subscription,
logger,
prematurelyCanceled,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
db: DrizzleCli;
stripeCli: Stripe;
cusProduct: FullCusProduct;
subscription: Stripe.Subscription;
logger: any;
prematurelyCanceled: boolean;
}) => {
const { org, env } = req;
const { org, env, logger } = ctx;
const { scheduled_ids } = cusProduct;
const fullCus = await CusService.getFull({
db,
@@ -73,7 +71,7 @@ export const handleCusProductDeleted = async ({
await createUsageInvoice({
db,
attachParams: webhookToAttachParams({
req,
ctx,
stripeCli,
paymentMethod,
cusProduct,
@@ -114,20 +112,19 @@ export const handleCusProductDeleted = async ({
});
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: cusProduct.internal_customer_id,
org,
env,
customerId: null,
scenario: AttachScenario.Expired,
cusProduct,
logger,
});
if (cusProduct.product.is_add_on) return;
const activatedFuture = await activateFutureProduct({
req,
ctx,
cusProduct,
});
@@ -148,7 +145,7 @@ export const handleCusProductDeleted = async ({
});
await activateDefaultProduct({
req,
ctx,
productGroup: cusProduct.product.group,
fullCus,
curCusProduct: curMainProduct || undefined,

View File

@@ -1,39 +1,32 @@
import {
type AppEnv,
type CollectionMethod,
CusProductStatus,
type Organization,
InternalError,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { handleSchedulePhaseCompleted } from "./handleSubUpdated/handleSchedulePhaseCompleted.js";
import { handleSubCanceled } from "./handleSubUpdated/handleSubCanceled.js";
import { handleSubPastDue } from "./handleSubUpdated/handleSubPastDue.js";
import { handleSubRenewed } from "./handleSubUpdated/handleSubRenewed.js";
export const handleSubscriptionUpdated = async ({
req,
db,
org,
ctx,
subscription,
previousAttributes,
env,
logger,
}: {
req: ExtendedRequest;
db: DrizzleCli;
org: Organization;
env: AppEnv;
subscription: any;
ctx: AutumnContext;
subscription: Stripe.Subscription;
// biome-ignore lint/suspicious/noExplicitAny: Don't know the type of previousAttributes
previousAttributes: any;
logger: any;
}) => {
const { db, org, env, logger } = ctx;
// handle scheduled updated
await handleSchedulePhaseCompleted({
req,
ctx,
subObject: subscription,
prevAttributes: previousAttributes,
});
@@ -93,7 +86,7 @@ export const handleSubscriptionUpdated = async ({
}
await handleSubCanceled({
req,
ctx,
previousAttributes,
sub: fullSub,
updatedCusProducts,
@@ -101,7 +94,7 @@ export const handleSubscriptionUpdated = async ({
});
await handleSubPastDue({
req,
ctx,
previousAttributes,
sub: fullSub,
updatedCusProducts,
@@ -109,7 +102,7 @@ export const handleSubscriptionUpdated = async ({
});
await handleSubRenewed({
req,
ctx,
prevAttributes: previousAttributes,
sub: fullSub,
updatedCusProducts,
@@ -130,6 +123,15 @@ export const handleSubscriptionUpdated = async ({
// Cancel subscription immediately
if (subscription.status === "past_due" && org.config.cancel_on_past_due) {
if (
!subscription.latest_invoice ||
typeof subscription.latest_invoice !== "string"
) {
throw new InternalError({
message: "subscription.latest_invoice is not a string",
});
}
const stripeCli = createStripeCli({
org,
env,

View File

@@ -10,20 +10,22 @@ import { CusProductService } from "@/internal/customers/cusProducts/CusProductSe
import { activateFutureProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { deleteCachedApiCustomer } from "../../../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
export const handleSchedulePhaseCompleted = async ({
req,
ctx,
subObject,
prevAttributes,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
subObject: Stripe.Subscription;
// biome-ignore lint/suspicious/noExplicitAny: Don't know the type of prevAttributes
prevAttributes: any;
}) => {
const { db, org, env, logger } = req;
const { db, org, env, logger } = ctx;
const phasePossiblyChanged =
notNullish(prevAttributes?.items) && notNullish(subObject.schedule);
@@ -58,25 +60,24 @@ export const handleSchedulePhaseCompleted = async ({
`Expiring cus product: ${cusProduct.product.name} (entity ID: ${cusProduct.entity_id})`,
);
await CusProductService.update({
db: req.db,
db,
cusProductId: cusProduct.id,
updates: { status: CusProductStatus.Expired },
});
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: cusProduct.internal_customer_id,
org,
env,
customerId: null,
scenario: AttachScenario.Expired,
cusProduct: cusProduct,
logger,
});
// ACTIVATING FUTURE PRODUCT
const futureCusProduct = await activateFutureProduct({
req,
ctx,
cusProduct,
});
@@ -90,7 +91,7 @@ export const handleSchedulePhaseCompleted = async ({
!isOneOff(fullFutureProduct.prices)
) {
await CusProductService.update({
db: req.db,
db,
cusProductId: futureCusProduct.id,
updates: {
subscription_ids: [subObject.id],
@@ -124,13 +125,14 @@ export const handleSchedulePhaseCompleted = async ({
// Last phase, cancel schedule
await stripeCli.subscriptionSchedules.release(schedule.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
db,
stripeScheduledId: schedule.id,
updates: {
scheduled_ids: [],
},
});
} catch (error: any) {
} catch (error: unknown) {
if (error instanceof Error) {
if (process.env.NODE_ENV === "development") {
logger.warn(
`schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`,
@@ -142,4 +144,5 @@ export const handleSchedulePhaseCompleted = async ({
}
}
}
}
};

View File

@@ -13,7 +13,7 @@ import { CusService } from "@/internal/customers/CusService.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { formatUnixToDateTime, nullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import {
getLatestPeriodEnd,
subToPeriodStartEnd,
@@ -86,13 +86,14 @@ const updateCusProductCanceled = async ({
};
export const handleSubCanceled = async ({
req,
ctx,
previousAttributes,
org,
sub,
updatedCusProducts,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
// biome-ignore lint/suspicious/noExplicitAny: Don't know the type of previousAttributes
previousAttributes: any;
sub: Stripe.Subscription;
org: Organization;
@@ -111,7 +112,7 @@ export const handleSubCanceled = async ({
const canceledFromPortal = canceled && !isAutumnDowngrade;
const { db, env, logger } = req;
const { db, env, logger } = ctx;
if (!canceledFromPortal || updatedCusProducts.length === 0) return;
@@ -171,7 +172,7 @@ export const handleSubCanceled = async ({
}
const insertParams = productToInsertParams({
req,
ctx,
fullCus,
newProduct: product,
entities,
@@ -194,12 +195,11 @@ export const handleSubCanceled = async ({
for (const cusProd of updatedCusProducts) {
try {
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: cusProd.internal_customer_id,
org,
env,
customerId: null,
logger,
scenario: AttachScenario.Cancel,
cusProduct: cusProd,
scheduledCusProduct: scheduledCusProducts.find(

View File

@@ -5,13 +5,14 @@ import {
} from "@autumn/shared";
import type Stripe from "stripe";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { deleteCachedApiCustomer } from "../../../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
export const isSubPastDue = ({
previousAttributes,
sub,
}: {
// biome-ignore lint/suspicious/noExplicitAny: Don't know the type of previousAttributes
previousAttributes: any;
sub: Stripe.Subscription;
}) => {
@@ -24,13 +25,14 @@ export const isSubPastDue = ({
};
export const handleSubPastDue = async ({
req,
ctx,
previousAttributes,
org,
sub,
updatedCusProducts,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
// biome-ignore lint/suspicious/noExplicitAny: Don't know the type of previousAttributes
previousAttributes: any;
sub: Stripe.Subscription;
org: Organization;
@@ -41,7 +43,7 @@ export const handleSubPastDue = async ({
sub,
});
const { env, logger } = req;
const { env, logger } = ctx;
if (!pastDue || updatedCusProducts.length === 0) return;
@@ -54,12 +56,11 @@ export const handleSubPastDue = async ({
for (const cusProd of updatedCusProducts) {
try {
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: cusProd.internal_customer_id,
org,
env,
customerId: null,
logger,
scenario: AttachScenario.PastDue,
cusProduct: cusProd,
});

View File

@@ -6,7 +6,7 @@ import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheU
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
const isSubRenewed = ({
previousAttributes,
@@ -32,17 +32,18 @@ const isSubRenewed = ({
};
export const handleSubRenewed = async ({
req,
ctx,
prevAttributes,
sub,
updatedCusProducts,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
// biome-ignore lint/suspicious/noExplicitAny: Don't know the type of prevAttributes
prevAttributes: any;
sub: Stripe.Subscription;
updatedCusProducts: FullCusProduct[];
}) => {
const { db, org, env, logger } = req;
const { db, org, env, logger } = ctx;
const { renewed } = isSubRenewed({
previousAttributes: prevAttributes,
@@ -97,12 +98,11 @@ export const handleSubRenewed = async ({
try {
for (const cusProd of updatedCusProducts) {
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: cusProd.internal_customer_id,
org,
env,
customerId: null,
logger,
scenario: AttachScenario.Renew,
cusProduct: cusProd,
deletedCusProduct: deletedCusProducts.find(

View File

@@ -8,17 +8,17 @@ import {
} from "@autumn/shared";
import type Stripe from "stripe";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv";
export const webhookToAttachParams = ({
req,
ctx,
stripeCli,
paymentMethod,
cusProduct,
fullCus,
entities,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
stripeCli: Stripe;
paymentMethod?: Stripe.PaymentMethod | null;
cusProduct: FullCusProduct;
@@ -26,16 +26,17 @@ export const webhookToAttachParams = ({
entities?: Entity[];
}): AttachParams => {
const fullProduct = cusProductToProduct({ cusProduct });
const { org, features } = ctx;
const params: AttachParams = {
stripeCli,
paymentMethod,
customer: fullCus,
org: req.org,
org,
products: [fullProduct],
prices: cusProductToPrices({ cusProduct }),
entitlements: cusProductToEnts({ cusProduct }),
features: req.features,
features,
freeTrial: cusProduct.free_trial || null,
optionsList: cusProduct.options,
cusProducts: [cusProduct],

View File

@@ -134,7 +134,7 @@ export const submitInvoiceToVercel = async ({
// Calculate total amount from invoice (includes subscription + usage charges)
const totalAmount = invoice.amount_due / 100;
let memo;
let memo: string | undefined;
if (org.config.invoice_memos) {
try {
@@ -293,13 +293,14 @@ export const getVercelAttachBody = ({
vercel_resource_id: resourceId || integrationConfigurationId,
},
req: {
db,
org,
env: env as AppEnv,
logger: c.get("ctx").logger,
features,
},
req: c.get("ctx"),
// req: {
// db,
// org,
// env: env as AppEnv,
// logger: c.get("ctx").logger,
// features,
// },
apiVersion: ApiVersion.V1_2,
};

View File

@@ -137,20 +137,6 @@ const ZOD_RULES = [
statusCode: 400,
format: (err: ZodError) => formatZodError(err),
},
{
name: "Zod error on /attach",
match: (err: Error, c: Context<HonoEnv>) =>
err instanceof ZodError && c.req.url.includes("/attach"),
statusCode: 400,
format: (err: ZodError) => formatZodError(err),
},
{
name: "Zod error on /checkout (email validation)",
match: (err: Error, c: Context<HonoEnv>) =>
err instanceof ZodError && c.req.url.includes("/checkout"),
statusCode: 400,
format: (err: ZodError) => formatZodError(err),
},
] as const;
const createErrorResponse = ({

View File

@@ -9,17 +9,20 @@ import {
} from "@autumn/shared";
import { generateId } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../honoUtils/HonoEnv";
export const parseReqForAction = (
req: ExtendedRequest,
): Partial<ExtendedRequest> => {
export const parseCtxForAction = ({
ctx,
}: {
ctx: AutumnContext;
}): Partial<AutumnContext> => {
return {
id: req.id,
authType: req.authType,
originalUrl: req.originalUrl,
method: req.method,
body: req.body,
timestamp: Date.now(),
id: ctx.id,
authType: ctx.authType,
// originalUrl: ctx.originalUrl,
// method: ctx.method,
// body: ctx.body,
// timestamp: Date.now(),
} as Partial<ExtendedRequest>;
};

View File

@@ -17,12 +17,11 @@ import {
} from "@autumn/shared";
import { sendSvixEvent } from "@/external/svix/svixHelpers.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { parseReqForAction } from "@/internal/analytics/actionUtils.js";
import { parseCtxForAction } from "@/internal/analytics/actionUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { getApiCustomerBase } from "../../customers/cusUtils/apiCusUtils/getApiCustomerBase";
import { getPlanResponse } from "../../products/productUtils/productResponseUtils/getPlanResponse";
@@ -36,7 +35,7 @@ interface ActionDetails {
}
export const addProductsUpdatedWebhookTask = async ({
req,
ctx,
org,
env,
customerId,
@@ -45,9 +44,8 @@ export const addProductsUpdatedWebhookTask = async ({
scheduledCusProduct,
deletedCusProduct,
scenario,
logger,
}: {
req?: ExtendedRequest;
ctx?: AutumnContext;
org: Organization;
env: AppEnv;
customerId: string | null;
@@ -56,7 +54,6 @@ export const addProductsUpdatedWebhookTask = async ({
scheduledCusProduct?: FullCusProduct;
deletedCusProduct?: FullCusProduct;
scenario: string;
logger: any;
}) => {
// Build action
@@ -64,12 +61,10 @@ export const addProductsUpdatedWebhookTask = async ({
await addTaskToQueue({
jobName: JobName.HandleProductsUpdated,
payload: {
req: req ? parseReqForAction(req) : undefined,
reqCtx: ctx ? parseCtxForAction({ ctx }) : undefined,
internalCustomerId,
orgId: org.id,
env,
// org,
// env,
customerId,
cusProduct,
scheduledCusProduct,
@@ -78,16 +73,9 @@ export const addProductsUpdatedWebhookTask = async ({
},
});
} catch (error) {
logger.error("Failed to add products updated webhook task to queue", {
error,
org_slug: org.slug,
org_id: org.id,
env,
internalCustomerId,
productId: cusProduct.product.id,
cusProductId: cusProduct.id,
// productId: product.id,
});
ctx?.logger.error(
`Failed to add products updated webhook task to queue: ${error}`,
);
}
};
@@ -97,7 +85,7 @@ export const handleProductsUpdated = async ({
}: {
ctx: AutumnContext;
data: {
req: Partial<ExtendedRequest>;
reqCtx?: Partial<AutumnContext>;
actionDetails: ActionDetails;
internalCustomerId: string;
// org: Organization;

View File

@@ -17,7 +17,6 @@ import { getProductResponse } from "@/internal/products/productUtils/productResp
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { getAttachScenario } from "./attachToCheckPreview/getAttachScenario.js";
@@ -136,24 +135,21 @@ export const attachToCheckPreview = async ({
};
export const getProductCheckPreview = async ({
req,
ctx,
customer,
product,
logger,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
customer: FullCustomer;
product: FullProduct;
logger: any;
}) => {
const { org, features, db } = req;
const { org, features, db } = ctx;
// Build attach params
const attachParams = await checkToAttachParams({
req,
ctx,
customer,
product,
logger,
});
const attachBody: AttachBodyV0 = {
@@ -163,7 +159,7 @@ export const getProductCheckPreview = async ({
};
const preview = await attachParamsToPreview({
ctx: req as AutumnContext,
ctx,
attachParams,
attachBody,
});

View File

@@ -8,7 +8,6 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "../../../../utils/models/Request.js";
import { getProductCheckPreview } from "./getProductCheckPreview.js";
export const handleProductCheck = async ({
@@ -29,7 +28,7 @@ export const handleProductCheck = async ({
entity_data,
} = body;
const { org, env, logger, db } = ctx;
const { org, env, db } = ctx;
// 1. Get customer and org
const [customer, product] = await Promise.all([
@@ -69,10 +68,9 @@ export const handleProductCheck = async ({
const preview = with_preview
? await getProductCheckPreview({
req: ctx as ExtendedRequest,
ctx,
customer,
product,
logger,
})
: undefined;

View File

@@ -8,7 +8,7 @@ import {
RewardTriggerEvent,
} from "@autumn/shared";
import { z } from "zod/v4";
import { parseReqForAction } from "@/internal/analytics/actionUtils.js";
import { parseCtxForAction } from "@/internal/analytics/actionUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
@@ -149,7 +149,7 @@ export const handleRedeemReferral = createRoute({
const rewardCat = getRewardCat(reward);
if (rewardCat === RewardCategory.FreeProduct) {
await triggerFreeProduct({
req: parseReqForAction(ctx as ExtendedRequest) as ExtendedRequest,
req: parseCtxForAction({ ctx }) as ExtendedRequest,
db,
referralCode,
redeemer: customer,

View File

@@ -1,12 +1,18 @@
import { type AttachResponseV1, AttachResponseV1Schema } from "@autumn/shared";
import { AttachBodyV0Schema } from "../../../../../shared/api/billing/attach/prevVersions/attachBodyV0";
import { AffectedResource } from "../../../../../shared/api/versionUtils/versionUtils";
import {
AffectedResource,
applyResponseVersionChanges,
} from "../../../../../shared/api/versionUtils/versionUtils";
import { createRoute } from "../../../honoMiddlewares/routeHandler";
import { checkStripeConnections } from "../../customers/attach/attachRouter";
import { getAttachParams } from "../../customers/attach/attachUtils/attachParams/getAttachParams";
import { getAttachBranch } from "../../customers/attach/attachUtils/getAttachBranch";
import { getAttachConfig } from "../../customers/attach/attachUtils/getAttachConfig";
import { runAttachFunction } from "../../customers/attach/attachUtils/getAttachFunction";
import { handleAttachErrors } from "../../customers/attach/attachUtils/handleAttachErrors";
import { insertCustomItems } from "../../customers/attach/attachUtils/insertCustomItems";
import { attachToInvoiceResponse } from "../../invoices/invoiceUtils";
export const handleAttachV2 = createRoute({
body: AttachBodyV0Schema,
@@ -77,17 +83,41 @@ export const handleAttachV2 = createRoute({
});
} catch (_error) {}
// const response = await runAttachFunction({
// req,
// res,
// attachParams,
// branch,
// attachBody,
// config,
// });
return c.json({
message: "Hello, world!",
const response = await runAttachFunction({
ctx,
attachParams,
branch,
attachBody,
config,
});
const { products, customer } = attachParams;
const responseV1 = AttachResponseV1Schema.parse({
success: true,
product_ids: products.map((p) => p.id),
customer_id: customer.id || customer.internal_id,
...response,
invoice: response.invoice
? attachToInvoiceResponse({ invoice: response.invoice })
: undefined,
});
return c.json(
applyResponseVersionChanges<AttachResponseV1>({
input: responseV1,
targetVersion: ctx.apiVersion,
resource: AffectedResource.Attach,
}),
);
},
});
// success: true,
// message: `Successfully purchased ${productNames} and attached to ${customerName}`,
// invoice: invoiceOnly
// ? attachToInvoiceResponse({ invoice: stripeInvoice })
// : undefined,
// code: SuccessCode.OneOffProductAttached,
// scenario: AttachScenario.New,

View File

@@ -0,0 +1,43 @@
import type { MetadataInsert, MetadataType } from "@autumn/shared";
import { addDays } from "date-fns";
import type { DrizzleCli } from "../../../../db/initDrizzle";
import { generateId } from "../../../../utils/genUtils";
import type { AttachParams } from "../../../customers/cusProducts/AttachParams";
import { MetadataService } from "../../../metadata/MetadataService";
export const attachParamsToMetadata = async ({
db,
attachParams,
type,
stripeInvoiceId,
expiresAt,
}: {
db: DrizzleCli;
attachParams: AttachParams;
type: MetadataType;
stripeInvoiceId?: string;
expiresAt?: number;
}) => {
const {
req: _req,
checkoutSessionParams: _checkoutSessionParams,
stripeCli: _stripeCli,
paymentMethod: _paymentMethod,
...rest
} = attachParams;
const attachClone = structuredClone(rest);
const metadata: MetadataInsert = {
id: generateId("meta"),
created_at: Date.now(),
expires_at: expiresAt ?? addDays(Date.now(), 10).getTime(),
data: attachClone,
type,
stripe_invoice_id: stripeInvoiceId,
};
await MetadataService.insert({ db, data: metadata });
return metadata;
};

View File

@@ -0,0 +1,5 @@
import type { Customer } from "@autumn/shared";
export const getCustomerDisplay = ({ customer }: { customer: Customer }) => {
return customer.name || customer.email || customer.id || customer.internal_id;
};

View File

@@ -1,5 +1,6 @@
import { Hono } from "hono";
import type { HonoEnv } from "../../honoUtils/HonoEnv.js";
import { handleAttachV2 } from "./attach/handleAttachV2.js";
import { handleCheckoutV2 } from "./checkout/handleCheckoutV2.js";
import { handleSetupPayment } from "./handlers/handleSetupPayment.js";
@@ -7,3 +8,4 @@ export const billingRouter = new Hono<HonoEnv>();
billingRouter.post("/setup_payment", ...handleSetupPayment);
billingRouter.post("/checkout", ...handleCheckoutV2);
billingRouter.post("/attach", ...handleAttachV2);

View File

@@ -58,23 +58,21 @@ export const handleCheckoutV2 = createRoute({
if (config.invoiceCheckout) {
const result = await handleCreateInvoiceCheckout({
req: ctx as ExtendedRequest,
ctx,
attachParams,
attachBody: body,
branch,
config,
});
checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url;
checkoutUrl = result?.checkout_url;
} else {
const checkout = await handleCreateCheckout({
req: ctx as ExtendedRequest,
const result = await handleCreateCheckout({
ctx,
attachParams,
config,
returnCheckout: true,
});
checkoutUrl = checkout?.url;
checkoutUrl = result?.checkout_url;
}
}

View File

@@ -10,10 +10,11 @@ import {
type FullCusProduct,
type FullCustomer,
type Organization,
RecaseError,
} from "@autumn/shared";
import { and, eq, ilike, or, sql, type Table } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
import { withSpan } from "../analytics/tracer/spanUtils.js";
import { RELEVANT_STATUSES } from "./cusProducts/CusProductService.js";
import { getFullCusQuery } from "./getFullCusQuery.js";

View File

@@ -532,7 +532,7 @@ export const createFullCusProduct = async ({
try {
if (sendWebhook && !attachParams.fromMigration) {
await addProductsUpdatedWebhookTask({
req: attachParams.req,
ctx: attachParams.req,
internalCustomerId: customer.internal_id,
org,
env: customer.env,
@@ -540,7 +540,6 @@ export const createFullCusProduct = async ({
cusProduct: isDowngrade ? curCusProduct! : fullCusProduct,
scheduledCusProduct: isDowngrade ? fullCusProduct : undefined,
scenario,
logger,
});
}
} catch (_error) {

View File

@@ -15,6 +15,7 @@ import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/han
import { getEntRelatedPrice } from "@/internal/products/entitlements/entitlementUtils.js";
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
import { nullish } from "@/utils/genUtils.js";
import type { Logger } from "../../../external/logtail/logtailUtils.js";
import type { InsertCusProductParams } from "../cusProducts/AttachParams.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService.js";
@@ -83,7 +84,7 @@ export const updateOneTimeCusProduct = async ({
}: {
db: DrizzleCli;
attachParams: InsertCusProductParams;
logger: any;
logger: Logger;
}) => {
// 1. Sort cus products by created_at
attachParams.cusProducts?.sort((a, b) => b.created_at - a.created_at);
@@ -93,9 +94,18 @@ export const updateOneTimeCusProduct = async ({
(cp) =>
cp.product.internal_id === attachParams.product.internal_id &&
cp.status === CusProductStatus.Active,
)!;
);
const existingCusEnts = existingCusProduct.customer_entitlements;
if (!existingCusProduct) {
// logger.warn("No existing cus product found", {
// data: {
// attachParams,
// },
// });
return;
}
const existingCusEnts = existingCusProduct?.customer_entitlements || [];
// 3. Update existing entitlements
for (const entitlement of attachParams.entitlements) {
@@ -169,7 +179,7 @@ export const updateOneTimeCusProduct = async ({
// Send webhook
const { customer, org } = attachParams;
await addProductsUpdatedWebhookTask({
req: attachParams.req,
ctx: attachParams.req,
internalCustomerId: customer.internal_id,
org,
env: customer.env,
@@ -177,6 +187,5 @@ export const updateOneTimeCusProduct = async ({
cusProduct: existingCusProduct,
scheduledCusProduct: undefined,
scenario: AttachScenario.New,
logger,
});
};

View File

@@ -1,38 +1,36 @@
import {
ApiVersion,
type AttachConfig,
AttachFunctionResponseSchema,
MetadataType,
RecaseError,
SuccessCode,
} from "@autumn/shared";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js";
import { orgToCurrency } from "@/internal/orgs/orgUtils.js";
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { pricesContainRecurring } from "@/internal/products/prices/priceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import {
type AttachParams,
AttachResultSchema,
} from "../cusProducts/AttachParams.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { attachParamsToMetadata } from "../../billing/attach/utils/attachParamsToMetadata.js";
import type { AttachParams } from "../cusProducts/AttachParams.js";
export const handleCreateCheckout = async ({
req,
res,
ctx,
attachParams,
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be used in the future
config,
returnCheckout = false,
}: {
req: any;
res?: any;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
returnCheckout?: boolean;
}) => {
const { db, logger } = req;
const { db, logger } = ctx;
const { customer, org, freeTrial, successUrl, rewards } = attachParams;
@@ -60,9 +58,10 @@ export const handleCreateCheckout = async ({
const isRecurring = pricesContainRecurring(attachParams.prices);
// Insert metadata
const metaId = await createCheckoutMetadata({
const metadata = await attachParamsToMetadata({
db,
attachParams,
type: MetadataType.CheckoutSessionCompleted,
});
let billingCycleAnchorUnixSeconds = org.config.anchor_start_of_month
@@ -100,11 +99,13 @@ export const handleCreateCheckout = async ({
}
: undefined;
const checkoutParams = attachParams.checkoutSessionParams || {};
const checkoutParams = attachParams.checkoutSessionParams as
| Partial<Stripe.Checkout.SessionCreateParams>
| undefined;
const allowPromotionCodes =
notNullish(checkoutParams.discounts) || notNullish(rewards)
notNullish(checkoutParams?.discounts) || notNullish(rewards)
? undefined
: checkoutParams.allow_promotion_codes || true;
: checkoutParams?.allow_promotion_codes || true;
let rewardData = {};
if (rewards) {
@@ -114,13 +115,13 @@ export const handleCreateCheckout = async ({
}
// Prepare checkout session parameters
let checkout: Stripe.Checkout.Session;
let checkout: Stripe.Checkout.Session | undefined;
const paymentMethodSet =
notNullish(checkoutParams.payment_method_types) ||
notNullish(checkoutParams.payment_method_configuration);
notNullish(checkoutParams?.payment_method_types) ||
notNullish(checkoutParams?.payment_method_configuration);
let sessionParams = {
let sessionParams: Stripe.Checkout.SessionCreateParams = {
customer: customer.processor.id,
line_items: items,
subscription_data: subscriptionData,
@@ -136,8 +137,8 @@ export const handleCreateCheckout = async ({
...(attachParams.checkoutSessionParams || {}),
metadata: {
...(attachParams.metadata ? attachParams.metadata : {}),
...(attachParams.checkoutSessionParams?.metadata || {}),
autumn_metadata_id: metaId,
...(checkoutParams?.metadata || {}),
autumn_metadata_id: metadata.id,
},
payment_method_collection:
freeTrial &&
@@ -145,7 +146,7 @@ export const handleCreateCheckout = async ({
freeTrial.card_required === false
? "if_required"
: undefined,
} satisfies Stripe.Checkout.SessionCreateParams;
};
if (attachParams.setupPayment) {
sessionParams = {
@@ -153,10 +154,10 @@ export const handleCreateCheckout = async ({
mode: "setup",
success_url: successUrl || toSuccessUrl({ org, env: customer.env }),
currency: org.default_currency || "usd",
...(checkoutParams as any),
...checkoutParams,
metadata: {
...(attachParams.checkoutSessionParams?.metadata || {}),
autumn_metadata_id: metaId,
...(checkoutParams?.metadata || {}),
autumn_metadata_id: metadata.id,
},
};
}
@@ -166,8 +167,8 @@ export const handleCreateCheckout = async ({
logger.info(
`✅ Successfully created checkout for customer ${customer.id || customer.internal_id}`,
);
} catch (error: any) {
const msg = error.message;
} catch (error) {
const msg = error instanceof Error ? error.message : undefined;
if (msg?.includes("No valid payment method types") && !paymentMethodSet) {
checkout = await stripeCli.checkout.sessions.create({
...sessionParams,
@@ -182,25 +183,35 @@ export const handleCreateCheckout = async ({
}
}
if (returnCheckout || !res) {
return checkout;
}
if (req.apiVersion.gte(ApiVersion.V1_1)) {
res.status(200).json(
AttachResultSchema.parse({
checkout_url: checkout.url,
const customerId = customer.id || customer.internal_id;
const productNames = attachParams.products.map((p) => p.name).join(", ");
return AttachFunctionResponseSchema.parse({
checkout_url: checkout?.url,
message: `Successfully created checkout for customer ${customerId}, product(s) ${productNames}`,
code: SuccessCode.CheckoutCreated,
message: `Successfully created checkout for customer ${
customer.id || customer.internal_id
}, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
product_ids: attachParams.products.map((p) => p.id),
customer_id: customer.id || customer.internal_id,
}),
);
} else {
res.status(200).json({
checkout_url: checkout.url,
checkoutSession: checkout,
});
}
// if (returnCheckout || !res) {
// return checkout;
// }
// if (req.apiVersion.gte(ApiVersion.V1_1)) {
// res.status(200).json(
// AttachResultSchema.parse({
// checkout_url: checkout.url,
// code: SuccessCode.CheckoutCreated,
// message: `Successfully created checkout for customer ${
// customer.id || customer.internal_id
// }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
// product_ids: attachParams.products.map((p) => p.id),
// customer_id: customer.id || customer.internal_id,
// }),
// );
// } else {
// res.status(200).json({
// checkout_url: checkout.url,
// });
// }
};

View File

@@ -1,114 +1,75 @@
import {
type AttachBodyV0,
type AttachBranch,
type AttachConfig,
type AttachFunctionResponse,
AttachFunctionResponseSchema,
MetadataType,
SuccessCode,
} from "@autumn/shared";
import type Stripe from "stripe";
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { attachParamsToMetadata } from "../../billing/attach/utils/attachParamsToMetadata.js";
import { handleOneOffFunction } from "../attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
import { handlePaidProduct } from "../attach/attachFunctions/addProductFlow/handlePaidProduct.js";
import { handleMultiAttachFlow } from "../attach/attachFunctions/multiAttach/handleMultiAttachFlow.js";
import {
type AttachParams,
AttachResultSchema,
} from "../cusProducts/AttachParams.js";
import type { AttachParams } from "../cusProducts/AttachParams.js";
export const handleCreateInvoiceCheckout = async ({
req,
res,
ctx,
attachParams,
attachBody,
config,
branch,
}: {
req: any;
res?: any;
ctx: AutumnContext;
attachParams: AttachParams;
attachBody: AttachBodyV0;
config: AttachConfig;
branch: AttachBranch;
}) => {
}): Promise<AttachFunctionResponse> => {
// if one off
const { stripeCli } = attachParams;
let invoiceResult;
let invoiceResult: AttachFunctionResponse;
if (attachParams.productsList) {
invoiceResult = await handleMultiAttachFlow({
req,
res,
attachParams,
attachBody,
branch,
config,
});
} else if (isOneOff(attachParams.prices)) {
if (isOneOff(attachParams.prices)) {
invoiceResult = await handleOneOffFunction({
req,
res,
ctx,
attachParams,
config,
});
} else {
invoiceResult = await handlePaidProduct({
req,
res,
ctx,
attachParams,
config,
});
}
const { invoices, anchorToUnix, subs }: any = invoiceResult;
const { invoice, stripeSub, anchorToUnix } = invoiceResult;
const metadataId = await createCheckoutMetadata({
db: req.db,
const metadata = await attachParamsToMetadata({
db: ctx.db,
attachParams: {
...attachParams,
anchorToUnix,
subIds: subs.map((s: Stripe.Subscription) => s.id),
subId: stripeSub?.id,
config,
} as any,
},
type: MetadataType.InvoiceCheckout,
});
for (const invoice of invoices) {
if (invoice) {
await stripeCli.invoices.update(invoice.id, {
metadata: {
autumn_metadata_id: metadataId,
autumn_metadata_id: metadata.id,
},
});
}
if (res) {
if (!config.finalizeInvoice) {
res.status(200).json(
AttachResultSchema.parse({
invoice: invoices[0],
const customerId =
attachParams.customer.id || attachParams.customer.internal_id;
const productNames = attachParams.products.map((p) => p.name).join(", ");
return AttachFunctionResponseSchema.parse({
checkout_url: config.finalizeInvoice
? invoice?.hosted_invoice_url
: undefined,
message: `Successfully created invoice checkout for customer ${customerId}, product(s) ${productNames}`,
code: SuccessCode.CheckoutCreated,
message: `Successfully created invoice for customer ${
attachParams.customer.id || attachParams.customer.internal_id
}, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
product_ids: attachParams.products.map((p) => p.id),
customer_id:
attachParams.customer.id || attachParams.customer.internal_id,
}),
);
return;
}
res.status(200).json(
AttachResultSchema.parse({
checkout_url: invoices[0].hosted_invoice_url,
code: SuccessCode.CheckoutCreated,
message: `Successfully created invoice checkout for customer ${
attachParams.customer.id || attachParams.customer.internal_id
}, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
product_ids: attachParams.products.map((p) => p.id),
customer_id:
attachParams.customer.id || attachParams.customer.internal_id,
}),
);
}
return { invoices };
invoice: config.finalizeInvoice ? undefined : invoice, // if finalizeInvoice, checkout_url is used
});
};

View File

@@ -1,37 +1,32 @@
import {
ApiVersion,
AttachBranch,
type AttachConfig,
AttachFunctionResponseSchema,
SuccessCode,
} from "@autumn/shared";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { createFullCusProduct } from "../../../add-product/createFullCusProduct.js";
import {
type AttachParams,
AttachResultSchema,
} from "../../../cusProducts/AttachParams.js";
import type { AttachParams } from "../../../cusProducts/AttachParams.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { getDefaultAttachConfig } from "../../attachUtils/getAttachConfig.js";
import { getMergeCusProduct } from "./getMergeCusProduct.js";
import { handlePaidProduct } from "./handlePaidProduct.js";
export const handleAddProduct = async ({
req,
res,
ctx,
attachParams,
config,
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be used in the future
branch,
}: {
req: ExtendedRequest;
res?: any;
ctx: AutumnContext;
attachParams: AttachParams;
config?: AttachConfig;
branch?: AttachBranch;
}) => {
const { logger } = req;
const { logger, db } = ctx;
const { customer, products, prices } = attachParams;
const defaultConfig: AttachConfig = getDefaultAttachConfig();
@@ -39,14 +34,11 @@ export const handleAddProduct = async ({
// 1. If paid product
if (prices.length > 0) {
await handlePaidProduct({
req,
res,
return await handlePaidProduct({
ctx,
attachParams,
config: config || defaultConfig,
});
return;
}
logger.info("Inserting free product in handleAddProduct");
@@ -61,7 +53,7 @@ export const handleAddProduct = async ({
for (const product of products) {
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
let anchorToUnix;
let anchorToUnix: number | undefined;
if (curCusProduct && config?.branch === AttachBranch.NewVersion) {
anchorToUnix = curCusProduct.created_at;
@@ -76,7 +68,7 @@ export const handleAddProduct = async ({
batchInsert.push(
createFullCusProduct({
db: req.db,
db,
attachParams: attachToInsertParams(attachParams, product),
billLaterOnly: true,
carryExistingUsages: config?.carryUsage || false,
@@ -89,25 +81,30 @@ export const handleAddProduct = async ({
logger.info("Successfully created full cus product");
if (res) {
const productNames = products.map((p) => p.name).join(", ");
const customerName = customer.name || customer.email || customer.id;
if (req.apiVersion.gte(ApiVersion.V1_1)) {
res.status(200).json(
AttachResultSchema.parse({
success: true,
return AttachFunctionResponseSchema.parse({
message: `Successfully attached ${products.map((p) => p.name).join(", ")} to ${customer.name}`,
code: SuccessCode.FreeProductAttached,
message: `Successfully attached ${productNames} to ${customerName}`,
product_ids: products.map((p) => p.id),
customer_id: customer.id || customer.internal_id,
}),
);
} else {
res.status(200).json({
success: true,
});
}
}
// if (res) {
// const productNames = products.map((p) => p.name).join(", ");
// const customerName = customer.name || customer.email || customer.id;
// if (req.apiVersion.gte(ApiVersion.V1_1)) {
// res.status(200).json(
// AttachResultSchema.parse({
// success: true,
// code: SuccessCode.FreeProductAttached,
// message: `Successfully attached ${productNames} to ${customerName}`,
// product_ids: products.map((p) => p.id),
// customer_id: customer.id || customer.internal_id,
// }),
// );
// } else {
// res.status(200).json({
// success: true,
// });
// }
// }
};
export const handleFreeProduct = async ({
@@ -142,7 +139,7 @@ export const handleFreeProduct = async ({
for (const product of products) {
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
let anchorToUnix;
let anchorToUnix: number | undefined;
if (curCusProduct && config?.branch === AttachBranch.NewVersion) {
anchorToUnix = curCusProduct.created_at;

View File

@@ -1,6 +1,7 @@
import {
type AttachConfig,
AttachScenario,
type AttachFunctionResponse,
AttachFunctionResponseSchema,
SuccessCode,
type UsagePriceConfig,
} from "@autumn/shared";
@@ -8,10 +9,7 @@ import { Decimal } from "decimal.js";
import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
import {
type AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { newPriceToInvoiceDescription } from "@/internal/invoices/invoiceFormatUtils.js";
import { buildInvoiceMemoFromEntitlements } from "@/internal/invoices/invoiceMemoUtils.js";
import {
@@ -24,19 +22,20 @@ import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/pric
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { getPriceOptions } from "@/internal/products/prices/priceUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
import { getCustomerDisplay } from "../../../../billing/attach/utils/getCustomerDisplay";
export const handleOneOffFunction = async ({
req,
ctx,
attachParams,
config,
res,
}: {
req: any;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
res: any;
}) => {
const logger = req.logger;
}): Promise<AttachFunctionResponse> => {
const { logger } = ctx;
logger.info("Scenario 4A: One-off prices");
const {
@@ -101,7 +100,7 @@ export const handleOneOffFunction = async ({
price_data: {
unit_amount: new Decimal(amount).mul(100).round().toNumber(),
currency: orgToCurrency({ org }),
product: price.config?.stripe_product_id || product?.processor?.id!,
product: price.config?.stripe_product_id || product?.processor?.id,
},
};
}
@@ -149,7 +148,7 @@ export const handleOneOffFunction = async ({
...invoiceItem,
customer: customer.processor.id!,
invoice: stripeInvoice.id,
} as any);
});
}
if (config.invoiceCheckout) {
@@ -160,13 +159,15 @@ export const handleOneOffFunction = async ({
}
await insertInvoiceFromAttach({
db: req.db,
db: ctx.db,
attachParams,
invoiceId: stripeInvoice.id,
logger,
});
return { invoices: [stripeInvoice], subs: [], anchorToUnix: undefined };
return AttachFunctionResponseSchema.parse({
invoice: stripeInvoice,
});
}
// Create invoice items
@@ -186,8 +187,7 @@ export const handleOneOffFunction = async ({
if (!paid) {
if (org.config.checkout_on_failed_payment) {
return await handleCreateCheckout({
req,
res,
ctx,
attachParams,
config,
});
@@ -201,7 +201,7 @@ export const handleOneOffFunction = async ({
for (const product of products) {
batchInsert.push(
createFullCusProduct({
db: req.db,
db: ctx.db,
attachParams: attachToInsertParams(attachParams, product),
logger,
}),
@@ -211,27 +211,38 @@ export const handleOneOffFunction = async ({
logger.info("5. Creating invoice from stripe");
await insertInvoiceFromAttach({
db: req.db,
db: ctx.db,
attachParams,
invoiceId: stripeInvoice.id,
logger,
});
if (res) {
const customerName = getCustomerDisplay({ customer });
const productNames = products.map((p) => p.name).join(", ");
const customerName = customer.name || customer.email || customer.id;
res.status(200).json(
AttachResultSchema.parse({
success: true,
message: `Successfully purchased ${productNames} and attached to ${customerName}`,
return AttachFunctionResponseSchema.parse({
// success: true,
message: `Successfully purchased product(s) ${productNames} and attached to customer ${customerName}`,
invoice: invoiceOnly
? attachToInvoiceResponse({ invoice: stripeInvoice })
: undefined,
code: SuccessCode.OneOffProductAttached,
product_ids: products.map((p) => p.id),
customer_id: customer.id || customer.internal_id,
scenario: AttachScenario.New,
}),
);
}
// product_ids: products.map((p) => p.id),
// customer_id: customer.id || customer.internal_id,
// scenario: AttachScenario.New,
});
// if (res) {
// res.status(200).json(
// AttachResultSchema.parse({
// success: true,
// message: `Successfully purchased ${productNames} and attached to ${customerName}`,
// invoice: invoiceOnly
// ? attachToInvoiceResponse({ invoice: stripeInvoice })
// : undefined,
// code: SuccessCode.OneOffProductAttached,
// product_ids: products.map((p) => p.id),
// customer_id: customer.id || customer.internal_id,
// scenario: AttachScenario.New,
// }),
// );
// }
};

View File

@@ -1,6 +1,7 @@
import {
ApiVersion,
type AttachConfig,
type AttachFunctionResponse,
AttachFunctionResponseSchema,
AttachScenario,
ErrCode,
isTrialing,
@@ -12,20 +13,15 @@ import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSu
import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
import {
type AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
attachToInvoiceResponse,
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { type AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { addIntervalToAnchor } from "@/internal/products/prices/billingIntervalUtils2.js";
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import { getCustomerDisplay } from "../../../../billing/attach/utils/getCustomerDisplay.js";
import {
getCustomerSchedule,
getCustomerSub,
@@ -37,17 +33,15 @@ import { updateStripeSub2 } from "../upgradeFlow/updateStripeSub2.js";
import { createStripeSub2 } from "./createStripeSub2.js";
export const handlePaidProduct = async ({
req,
res,
ctx,
attachParams,
config,
}: {
req: ExtendedRequest;
res: any;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
}) => {
const logger = req.logger;
}): Promise<AttachFunctionResponse> => {
const { logger, db } = ctx;
const {
org,
@@ -77,7 +71,7 @@ export const handlePaidProduct = async ({
let sub: Stripe.Subscription | null = null;
let schedule: Stripe.SubscriptionSchedule | null | undefined = null;
let invoice: Stripe.Invoice | undefined;
let trialEndsAt;
let trialEndsAt: number | null | undefined;
// 1. If merge sub
@@ -93,14 +87,14 @@ export const handlePaidProduct = async ({
attachParams.freeTrial = null;
// 1. If merged sub is canceled, also add to current schedule
const newItemSet = await paramsToSubItems({
req,
ctx,
sub: mergeSub,
attachParams,
config,
});
const { updatedSub, latestInvoice } = await updateStripeSub2({
req,
ctx,
attachParams,
curSub: mergeSub,
itemSet: newItemSet,
@@ -112,7 +106,7 @@ export const handlePaidProduct = async ({
if (latestInvoice) {
invoice = await insertInvoiceFromAttach({
db: req.db,
db,
stripeInvoice: latestInvoice,
attachParams,
logger,
@@ -121,7 +115,7 @@ export const handlePaidProduct = async ({
if (subIsCanceled({ sub: mergeSub })) {
logger.info("ADD PRODUCT FLOW, CREATING NEW SCHEDULE");
schedule = await subToNewSchedule({
req,
ctx,
sub: mergeSub,
attachParams,
config,
@@ -138,8 +132,7 @@ export const handlePaidProduct = async ({
logger.info(`ADD PRODUCT FLOW, SCHEDULE ID: ${schedule?.id}`);
if (schedule) {
await handleUpgradeFlowSchedule({
req,
logger,
ctx,
attachParams,
config,
schedule,
@@ -150,7 +143,7 @@ export const handlePaidProduct = async ({
}
}
} else {
let billingCycleAnchorUnix;
let billingCycleAnchorUnix: number | undefined;
const smallestInterval = getSmallestInterval({
prices: attachParams.prices,
});
@@ -180,7 +173,7 @@ export const handlePaidProduct = async ({
// console.log("Item set: ", itemSet);
try {
sub = await createStripeSub2({
db: req.db,
db: ctx.db,
stripeCli,
attachParams,
itemSet,
@@ -191,21 +184,20 @@ export const handlePaidProduct = async ({
if (sub?.latest_invoice) {
invoice = await insertInvoiceFromAttach({
db: req.db,
db: ctx.db,
stripeInvoice: sub.latest_invoice as Stripe.Invoice,
attachParams,
logger,
});
}
} catch (error: any) {
} catch (error) {
if (
error instanceof RecaseError &&
!invoiceOnly &&
error.code === ErrCode.CreateStripeSubscriptionFailed
) {
return await handleCreateCheckout({
req,
res,
ctx,
attachParams,
config,
});
@@ -220,12 +212,18 @@ export const handlePaidProduct = async ({
const anchorToUnix = getEarliestPeriodEnd({ sub }) * 1000;
if (config.invoiceCheckout) {
return {
invoices: subscriptions.map((s) => s.latest_invoice as Stripe.Invoice),
subs: subscriptions,
return AttachFunctionResponseSchema.parse({
invoice: subscriptions?.[0]?.latest_invoice as Stripe.Invoice,
stripeSub: subscriptions?.[0],
anchorToUnix,
config,
};
});
// return {
// invoices: subscriptions.map((s) => s.latest_invoice as Stripe.Invoice),
// subs: subscriptions,
// anchorToUnix,
// config,
// };
}
// Add product and entitlements to customer
@@ -234,7 +232,7 @@ export const handlePaidProduct = async ({
for (const product of products) {
batchInsert.push(
createFullCusProduct({
db: req.db,
db: ctx.db,
attachParams: attachToInsertParams(attachParams, product),
subscriptionIds: subscriptions.map((s) => s.id),
subscriptionScheduleIds: schedule ? [schedule.id] : undefined,
@@ -248,29 +246,39 @@ export const handlePaidProduct = async ({
}
await Promise.all(batchInsert);
if (res) {
const productNames = products.map((p) => p.name).join(", ");
const customerName = customer.name || customer.email || customer.id;
if (req.apiVersion.gte(ApiVersion.V1_1)) {
res.status(200).json(
AttachResultSchema.parse({
message: `Successfully created subscriptions and attached ${productNames} to ${customerName}`,
const customerName = getCustomerDisplay({ customer });
return AttachFunctionResponseSchema.parse({
message: `Successfully created subscriptions and attached product(s) ${productNames} to customer ${customerName}`,
code: SuccessCode.NewProductAttached,
product_ids: products.map((p) => p.id),
customer_id: customer.id || customer.internal_id,
invoice: invoiceOnly
? attachToInvoiceResponse({ invoice })
: undefined,
}),
);
} else {
res.status(200).json({
success: true,
message: `Successfully created subscriptions and attached ${products
.map((p) => p.name)
.join(", ")} to ${customer.name}`,
invoice: invoiceOnly ? invoice : undefined,
});
}
}
// if (res) {
// const productNames = products.map((p) => p.name).join(", ");
// const customerName = customer.name || customer.email || customer.id;
// if (req.apiVersion.gte(ApiVersion.V1_1)) {
// res.status(200).json(
// AttachResultSchema.parse({
// message: `Successfully created subscriptions and attached ${productNames} to ${customerName}`,
// code: SuccessCode.NewProductAttached,
// product_ids: products.map((p) => p.id),
// customer_id: customer.id || customer.internal_id,
// invoice: invoiceOnly
// ? attachToInvoiceResponse({ invoice })
// : undefined,
// }),
// );
// } else {
// res.status(200).json({
// success: true,
// message: `Successfully created subscriptions and attached ${products
// .map((p) => p.name)
// .join(", ")} to ${customer.name}`,
// invoice: invoiceOnly ? invoice : undefined,
// });
// }
// }
};

View File

@@ -1,5 +1,6 @@
import {
type AttachConfig,
AttachFunctionResponseSchema,
AttachScenario,
ErrCode,
SuccessCode,
@@ -9,12 +10,10 @@ import type Stripe from "stripe";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import {
type AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import RecaseError from "@/utils/errorUtils.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { addSubIdToCache } from "../../cusCache/subCacheUtils.js";
import {
cusProductToSchedule,
@@ -31,17 +30,15 @@ import { subToNewSchedule } from "../mergeUtils/subToNewSchedule.js";
import { updateCurSchedule } from "../mergeUtils/updateCurSchedule.js";
export const handleRenewProduct = async ({
req,
res,
ctx,
attachParams,
config,
}: {
req: any;
res: any;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
}) => {
const logger = req.logger;
const { logger, db } = ctx;
const { stripeCli } = attachParams;
let { curScheduledProduct } = attachParamToCusProducts({ attachParams });
@@ -92,7 +89,7 @@ export const handleRenewProduct = async ({
await stripeCli.subscriptionSchedules.release(schedule.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
db,
stripeScheduledId: schedule.id,
updates: {
scheduled_ids: [],
@@ -114,7 +111,7 @@ export const handleRenewProduct = async ({
}
await CusProductService.update({
db: req.db,
db,
cusProductId: curCusProduct.id,
updates: {
canceled: false,
@@ -132,7 +129,7 @@ export const handleRenewProduct = async ({
`RENEW FLOW: adding cur cus product back to schedule ${schedule.id}`,
);
const newItems = await paramsToScheduleItems({
req,
ctx,
attachParams,
config,
schedule,
@@ -146,7 +143,7 @@ export const handleRenewProduct = async ({
})) as Stripe.Subscription;
await updateCurSchedule({
req,
ctx,
attachParams,
schedule,
newPhases: newItems.phases,
@@ -154,7 +151,7 @@ export const handleRenewProduct = async ({
});
await CusProductService.update({
db: req.db,
db,
cusProductId: curCusProduct.id,
updates: {
scheduled_ids: [schedule.id],
@@ -170,7 +167,7 @@ export const handleRenewProduct = async ({
await stripeCli.subscriptionSchedules.release(schedule.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
db,
stripeScheduledId: schedule.id,
updates: {
scheduled_ids: [],
@@ -178,7 +175,7 @@ export const handleRenewProduct = async ({
});
await CusProductService.update({
db: req.db,
db,
cusProductId: curCusProduct.id,
updates: {
canceled: false,
@@ -199,7 +196,7 @@ export const handleRenewProduct = async ({
const periodEnd = getLatestPeriodEnd({ sub: curSub });
await subToNewSchedule({
req,
ctx,
sub: curSub,
attachParams,
config,
@@ -207,7 +204,7 @@ export const handleRenewProduct = async ({
});
await CusProductService.update({
db: req.db,
db,
cusProductId: curCusProduct.id,
updates: {
canceled: false,
@@ -221,7 +218,7 @@ export const handleRenewProduct = async ({
if (curCusProduct) {
try {
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: curCusProduct.internal_customer_id,
org: attachParams.org,
env: attachParams.customer.env,
@@ -229,29 +226,32 @@ export const handleRenewProduct = async ({
attachParams.customer.id || attachParams.customer.internal_id,
scenario: AttachScenario.Renew,
cusProduct: curCusProduct,
logger,
});
} catch (error) {
logger.error("RENEW FLOW: failed to add to webhook queue", { error });
logger.error(`RENEW FLOW: failed to add to webhook queue: ${error}`);
}
}
if (curScheduledProduct) {
await CusProductService.delete({
db: req.db,
db,
cusProductId: curScheduledProduct.id,
});
}
if (res) {
res.status(200).json(
AttachResultSchema.parse({
return AttachFunctionResponseSchema.parse({
code: SuccessCode.RenewedProduct,
message: `Successfully renewed product ${product.name}`,
product_ids: [product.id],
customer_id:
attachParams.customer.id || attachParams.customer.internal_id,
}),
);
}
});
// if (res) {
// res.status(200).json(
// AttachResultSchema.parse({
// code: SuccessCode.RenewedProduct,
// message: `Successfully renewed product ${product.name}`,
// product_ids: [product.id],
// customer_id:
// attachParams.customer.id || attachParams.customer.internal_id,
// }),
// );
// }
};

View File

@@ -1,102 +0,0 @@
import { type AppEnv, AttachScenario, type Organization } from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { MetadataService } from "@/internal/metadata/MetadataService.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { deleteCachedApiCustomer } from "../../../cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
export const handleInvoiceCheckoutPaid = async ({
req,
org,
env,
db,
stripeCli,
invoice,
}: {
req: ExtendedRequest;
org: Organization;
env: AppEnv;
db: DrizzleCli;
stripeCli: Stripe;
invoice: Stripe.Invoice;
}) => {
const { logger } = req;
const metadataId = invoice.metadata?.autumn_metadata_id!;
const metadata = await MetadataService.get({
db,
id: metadataId,
});
const { subIds, anchorToUnix, config, ...rest } = metadata?.data ?? {};
const attachParams = rest as AttachParams;
if (!attachParams) return;
const reqMatch =
attachParams.org.id === org.id && attachParams.customer.env === env;
if (!reqMatch) return;
if (attachParams.productsList) {
console.log("Inserting products list");
for (const productOptions of attachParams.productsList) {
const product = attachParams.products.find(
(p) => p.id === productOptions.product_id,
);
if (!product) {
logger.error(
`checkout.completed: product not found for productOptions: ${JSON.stringify(
productOptions,
)}`,
);
continue;
}
await createFullCusProduct({
db,
attachParams: attachToInsertParams(
attachParams,
product,
productOptions.entity_id || undefined,
),
subscriptionIds: subIds,
anchorToUnix,
scenario: AttachScenario.New,
logger,
productOptions,
});
}
} else {
const batchInsert = [];
for (const product of attachParams.products) {
batchInsert.push(
createFullCusProduct({
db,
attachParams: attachToInsertParams(attachParams, product),
subscriptionIds: subIds,
anchorToUnix,
carryExistingUsages: config.carryUsage,
scenario: AttachScenario.New,
logger: req.logger,
}),
);
}
await Promise.all(batchInsert);
}
req.logger.info(
`✅ invoice.paid, successfully inserted cus products: ${attachParams.products.map((p) => p.id).join(", ")}`,
);
await deleteCachedApiCustomer({
customerId: attachParams.customer.id || "",
orgId: org.id,
env,
});
};

View File

@@ -2,6 +2,7 @@ import {
type AttachBodyV0,
type AttachBranch,
type AttachConfig,
AttachFunctionResponseSchema,
AttachScenario,
CusProductStatus,
isTrialing,
@@ -10,10 +11,7 @@ import {
import type Stripe from "stripe";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import {
type AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
@@ -21,16 +19,12 @@ import {
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import type {
ExtendedRequest,
ExtendedResponse,
} from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import {
getCustomerSub,
paramsToCurSubSchedule,
} from "../../attachUtils/convertAttachParams.js";
import { handleMultiAttachErrors } from "../../attachUtils/handleAttachErrors/handleMultiAttachErrors.js";
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
import { createStripeSub2 } from "../addProductFlow/createStripeSub2.js";
import { handleUpgradeFlowSchedule } from "../upgradeFlow/handleUpgradeFlowSchedule.js";
@@ -41,22 +35,20 @@ import {
} from "./getAddAndRemoveProducts.js";
export const handleMultiAttachFlow = async ({
req,
res,
ctx,
attachParams,
attachBody,
branch,
config,
}: {
req: ExtendedRequest;
res: ExtendedResponse;
ctx: AutumnContext;
attachParams: AttachParams;
attachBody: AttachBodyV0;
branch: AttachBranch;
config: AttachConfig;
}) => {
await handleMultiAttachErrors({ attachParams, attachBody, branch });
const { db, logger } = req;
const { db, logger } = ctx;
const { stripeCli } = attachParams;
const productsList = attachParams.productsList!;
@@ -72,7 +64,7 @@ export const handleMultiAttachFlow = async ({
});
const mergedItemSet = await paramsToSubItems({
req,
ctx,
attachParams,
config,
removeCusProducts,
@@ -109,7 +101,7 @@ export const handleMultiAttachFlow = async ({
config.disableTrial = true;
const updateResult = await updateStripeSub2({
req,
ctx,
attachParams,
config,
curSub: curSub!,
@@ -122,13 +114,12 @@ export const handleMultiAttachFlow = async ({
const schedule = await paramsToCurSubSchedule({ attachParams });
if (schedule) {
await handleUpgradeFlowSchedule({
req,
ctx,
attachParams,
config,
schedule,
curSub,
removeCusProducts,
logger,
});
}
@@ -166,7 +157,7 @@ export const handleMultiAttachFlow = async ({
}
// Expire all existing cus products at the customer level
const batchInsert: any[] = [];
const batchInsert: unknown[] = [];
const newProdList = getProdListWithoutEntities({
attachParams,
productsList,
@@ -190,35 +181,42 @@ export const handleMultiAttachFlow = async ({
product,
productOptions.entity_id || undefined,
),
subscriptionIds: curSub ? [curSub?.id!] : undefined,
subscriptionIds: curSub ? [curSub.id] : undefined,
anchorToUnix,
scenario: AttachScenario.New,
logger,
productOptions,
trialEndsAt:
mergeCusProduct && isTrialing({ cusProduct: mergeCusProduct })
? mergeCusProduct?.trial_ends_at!
? mergeCusProduct?.trial_ends_at || undefined
: undefined,
}),
);
}
console.log("Running multi attach flow!");
if (res) {
const invoice = latestInvoice;
res.status(200).json(
AttachResultSchema.parse(
AttachResultSchema.parse({
return AttachFunctionResponseSchema.parse({
message: `Successfully created subscriptions and attached ${attachParams.products.map((p) => p.name).join(", ")} to ${attachParams.customer.name}`,
code: SuccessCode.NewProductAttached,
product_ids: attachParams.products.map((p) => p.id),
customer_id:
attachParams.customer.id || attachParams.customer.internal_id,
invoice: attachParams.invoiceOnly
? attachToInvoiceResponse({ invoice })
? attachToInvoiceResponse({ invoice: latestInvoice })
: undefined,
}),
),
);
}
});
// if (res) {
// const invoice = latestInvoice;
// res.status(200).json(
// AttachResultSchema.parse(
// AttachResultSchema.parse({
// message: `Successfully created subscriptions and attached ${attachParams.products.map((p) => p.name).join(", ")} to ${attachParams.customer.name}`,
// code: SuccessCode.NewProductAttached,
// product_ids: attachParams.products.map((p) => p.id),
// customer_id:
// attachParams.customer.id || attachParams.customer.internal_id,
// invoice: attachParams.invoiceOnly
// ? attachToInvoiceResponse({ invoice })
// : undefined,
// }),
// ),
// );
// }
};

View File

@@ -1,6 +1,6 @@
import {
ApiVersion,
type AttachConfig,
AttachFunctionResponseSchema,
AttachScenario,
InternalError,
SuccessCode,
@@ -9,15 +9,13 @@ import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubU
import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import {
type AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
attachToInsertParams,
isFreeProduct,
} from "@/internal/products/productUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import {
attachParamsToCurCusProduct,
getCustomerSchedule,
@@ -29,19 +27,17 @@ import { subToNewSchedule } from "../../mergeUtils/subToNewSchedule.js";
import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js";
export const handleScheduleFunction2 = async ({
req,
res,
ctx,
attachParams,
config,
skipInsertCusProduct = false,
}: {
req: any;
res: any;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
skipInsertCusProduct?: boolean;
}) => {
const logger = req.logger;
const { logger, db } = ctx;
const product = attachParams.products[0];
const { stripeCli } = attachParams;
@@ -87,7 +83,7 @@ export const handleScheduleFunction2 = async ({
if (schedule) {
const newItems = await paramsToScheduleItems({
req,
ctx,
schedule: schedule,
attachParams,
config,
@@ -105,13 +101,13 @@ export const handleScheduleFunction2 = async ({
);
await stripeCli.subscriptionSchedules.release(schedule.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
db,
stripeScheduledId: schedule.id,
updates: { scheduled_ids: [] },
});
await CusProductService.update({
db: req.db,
db,
cusProductId: curCusProduct.id,
updates: {
canceled: true,
@@ -123,7 +119,7 @@ export const handleScheduleFunction2 = async ({
} else {
logger.info(`SCHEDULE FLOW: updating schedule ${schedule?.id}`);
schedule = await updateCurSchedule({
req,
ctx,
attachParams,
schedule,
newPhases: newItems.phases || [],
@@ -131,7 +127,7 @@ export const handleScheduleFunction2 = async ({
});
await CusProductService.update({
db: req.db,
db,
cusProductId: curCusProduct.id,
updates: {
scheduled_ids: [schedule.id],
@@ -144,7 +140,7 @@ export const handleScheduleFunction2 = async ({
} else {
logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`);
schedule = await subToNewSchedule({
req,
ctx,
sub: curSub,
attachParams,
config,
@@ -152,7 +148,7 @@ export const handleScheduleFunction2 = async ({
});
await CusProductService.update({
db: req.db,
db,
cusProductId: curCusProduct.id,
updates: {
canceled: true,
@@ -174,7 +170,7 @@ export const handleScheduleFunction2 = async ({
if (!skipInsertCusProduct) {
await createFullCusProduct({
db: req.db,
db,
attachParams: attachToInsertParams(attachParams, product),
startsAt: expectedEnd * 1000,
subscriptionScheduleIds: schedule ? [schedule.id] : [],
@@ -192,7 +188,7 @@ export const handleScheduleFunction2 = async ({
if (curCusProduct) {
try {
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: curCusProduct.internal_customer_id,
org: attachParams.org,
env: attachParams.customer.env,
@@ -204,29 +200,33 @@ export const handleScheduleFunction2 = async ({
: AttachScenario.Downgrade,
cusProduct: curCusProduct,
logger,
});
} catch (error) {
logger.error("SCHEDULE FLOW: failed to add to webhook queue", { error });
}
}
if (res) {
if (req.apiVersion.gte(ApiVersion.V1_1)) {
res.status(200).json(
AttachResultSchema.parse({
return AttachFunctionResponseSchema.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,
message: `Successfully downgraded from ${curCusProduct.product.name} to ${product.name}`,
});
}
}
// if (res) {
// if (req.apiVersion.gte(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,
// message: `Successfully downgraded from ${curCusProduct.product.name} to ${product.name}`,
// });
// }
// }
};

View File

@@ -26,9 +26,10 @@ import {
shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
export const handleQuantityDowngrade = async ({
req,
ctx,
attachParams,
attachConfig,
cusProduct,
@@ -37,7 +38,7 @@ export const handleQuantityDowngrade = async ({
newOptions,
subItem,
}: {
req: any;
ctx: AutumnContext;
attachParams: AttachParams;
attachConfig: AttachConfig;
cusProduct: FullCusProduct;
@@ -46,7 +47,7 @@ export const handleQuantityDowngrade = async ({
newOptions: FeatureOptions;
subItem: Stripe.SubscriptionItem;
}) => {
const { db, logger, org } = req;
const { db, logger, org, features } = ctx;
const { stripeCli, paymentMethod } = attachParams;
const cusPrice = featureToCusPrice({
@@ -98,13 +99,13 @@ export const handleQuantityDowngrade = async ({
});
const product = cusProductToProduct({ cusProduct });
const feature = req.features.find(
const feature = features.find(
(f: Feature) => f.internal_id === newOptions.internal_feature_id,
)!;
const invoiceItem = constructStripeInvoiceItem({
product,
amount: amount,
org: req.org,
org: org,
price: cusPrice.price,
description: getFeatureInvoiceDescription({
feature: feature,

View File

@@ -26,9 +26,10 @@ import {
shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
export const handleQuantityUpgrade = async ({
req,
ctx,
attachParams,
cusProduct,
stripeSubs,
@@ -39,7 +40,7 @@ export const handleQuantityUpgrade = async ({
stripeSub,
subItem,
}: {
req: any;
ctx: AutumnContext;
attachParams: AttachParams;
cusProduct: FullCusProduct;
attachConfig: AttachConfig;
@@ -51,7 +52,7 @@ export const handleQuantityUpgrade = async ({
subItem: Stripe.SubscriptionItem;
}) => {
// Manually calculate prorations...
const { features, org, logger, db } = req;
const { features, org, logger, db } = ctx;
const { stripeCli, now, paymentMethod } = attachParams;
const difference = new Decimal(newOptions.quantity)

View File

@@ -9,11 +9,12 @@ import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeS
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { featureToCusPrice } from "@/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import { handleQuantityDowngrade } from "./handleQuantityDowngrade.js";
import { handleQuantityUpgrade } from "./handleQuantityUpgrade.js";
export const handleUpdateFeatureQuantity = async ({
req,
ctx,
attachParams,
attachConfig,
cusProduct,
@@ -21,7 +22,7 @@ export const handleUpdateFeatureQuantity = async ({
oldOptions,
newOptions,
}: {
req: any;
ctx: AutumnContext;
attachParams: AttachParams;
attachConfig: AttachConfig;
cusProduct: FullCusProduct;
@@ -29,22 +30,7 @@ export const handleUpdateFeatureQuantity = async ({
oldOptions: FeatureOptions;
newOptions: FeatureOptions;
}) => {
const { db, logger } = req;
const { stripeCli } = attachParams;
const prorationBehavior = "always_invoice";
const subToUpdate = stripeSubs?.[0];
// const subToUpdate = await getUsageBasedSub({
// db,
// stripeCli: stripeCli,
// subIds: cusProduct.subscription_ids || [],
// feature: {
// internal_id: newOptions.internal_feature_id,
// id: newOptions.feature_id,
// } as Feature,
// stripeSubs: stripeSubs,
// });
const cusPrice = featureToCusPrice({
internalFeatureId: newOptions.internal_feature_id!,
@@ -68,7 +54,7 @@ export const handleUpdateFeatureQuantity = async ({
if (newOptions.quantity < oldOptions.quantity) {
return await handleQuantityDowngrade({
req,
ctx,
attachParams,
attachConfig,
cusProduct,
@@ -79,7 +65,7 @@ export const handleUpdateFeatureQuantity = async ({
});
} else {
return await handleQuantityUpgrade({
req,
ctx,
attachParams,
attachConfig,
cusProduct,

View File

@@ -1,25 +1,27 @@
import { type AttachConfig, SuccessCode } from "@autumn/shared";
import {
type AttachConfig,
AttachFunctionResponseSchema,
SuccessCode,
} from "@autumn/shared";
import type Stripe from "stripe";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
type AttachParams,
AttachResultSchema,
} from "../../../cusProducts/AttachParams.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../../cusProducts/AttachParams.js";
import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js";
import { handleUpdateFeatureQuantity } from "./updateFeatureQuantity.js";
export const handleUpdateQuantityFunction = async ({
req,
res,
ctx,
attachParams,
config,
}: {
req: any;
res: any;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
}) => {
const { db } = ctx;
// 2. Update quantities
const optionsToUpdate = attachParams.optionsToUpdate!;
const { customer } = attachParams;
@@ -37,7 +39,7 @@ export const handleUpdateQuantityFunction = async ({
for (const options of optionsToUpdate) {
const result = await handleUpdateFeatureQuantity({
req,
ctx,
attachParams,
attachConfig: config,
cusProduct,
@@ -52,19 +54,26 @@ export const handleUpdateQuantityFunction = async ({
}
await CusProductService.update({
db: req.db,
db,
cusProductId: cusProduct.id,
updates: { options: optionsToUpdate.map((o) => o.new) },
});
res.status(200).json(
AttachResultSchema.parse({
customer_id: customer.id || customer.internal_id,
product_ids: attachParams.products.map((p) => p.id),
invoice:
config.invoiceOnly && invoices.length > 0 ? invoices[0] : undefined,
return AttachFunctionResponseSchema.parse({
code: SuccessCode.FeaturesUpdated,
message: `Successfully updated quantity for features: ${optionsToUpdate.map((o) => o.new.feature_id).join(", ")}`,
}),
);
invoice:
config.invoiceOnly && invoices.length > 0 ? invoices[0] : undefined,
});
// res.status(200).json(
// AttachResultSchema.parse({
// customer_id: customer.id || customer.internal_id,
// product_ids: attachParams.products.map((p) => p.id),
// invoice:
// config.invoiceOnly && invoices.length > 0 ? invoices[0] : undefined,
// code: SuccessCode.FeaturesUpdated,
// message: `Successfully updated quantity for features: ${optionsToUpdate.map((o) => o.new.feature_id).join(", ")}`,
// }),
// );
};

View File

@@ -1,11 +1,12 @@
import {
ApiVersion,
AttachBranch,
type AttachConfig,
AttachFunctionResponseSchema,
AttachScenario,
CusProductStatus,
cusProductToProduct,
ProrationBehavior,
SuccessCode,
} from "@autumn/shared";
import type Stripe from "stripe";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
@@ -13,10 +14,7 @@ import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSu
import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import {
type AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import {
@@ -24,7 +22,7 @@ import {
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import {
attachParamsToCurCusProduct,
paramsToCurSub,
@@ -36,14 +34,12 @@ import { updateStripeSub2 } from "./updateStripeSub2.js";
import { shouldCancelSub } from "./upgradeFlowUtils.js";
export const handleUpgradeFlow = async ({
req,
res,
ctx,
attachParams,
config,
branch,
}: {
req: ExtendedRequest;
res?: any;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
branch: AttachBranch;
@@ -51,7 +47,7 @@ export const handleUpgradeFlow = async ({
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
const curSub = await paramsToCurSub({ attachParams });
const logger = req.logger;
const { logger, db } = ctx;
if (curCusProduct?.api_semver) {
attachParams.apiVersion = curCusProduct.api_semver;
@@ -66,7 +62,7 @@ export const handleUpgradeFlow = async ({
});
const newItemSet = await paramsToSubItems({
req,
ctx,
sub: curSub,
attachParams,
config,
@@ -95,7 +91,7 @@ export const handleUpgradeFlow = async ({
if (curScheduledProduct) {
await CusProductService.delete({
db: req.db,
db,
cusProductId: curScheduledProduct.id,
});
}
@@ -127,13 +123,8 @@ export const handleUpgradeFlow = async ({
logger.info(`UPGRADE FLOW, updating sub ${curSub.id}`);
itemSet.subItems = subItems;
// await logPhaseItems({
// db: req.db,
// items: itemSet.subItems,
// });
const res = await updateStripeSub2({
req,
ctx,
attachParams,
config,
curSub: curSub,
@@ -144,19 +135,26 @@ export const handleUpgradeFlow = async ({
if (res?.latestInvoice) {
logger.info(`UPGRADE FLOW: inserting invoice ${res.latestInvoice.id}`);
await insertInvoiceFromAttach({
db: req.db,
db,
attachParams,
stripeInvoice: res.latestInvoice,
logger,
});
}
if (res?.url) {
return AttachFunctionResponseSchema.parse({
checkout_url: res.url,
code: SuccessCode.InvoiceActionRequired,
message: `Payment action required`,
});
}
const schedule = await paramsToCurSubSchedule({ attachParams });
if (schedule) {
await handleUpgradeFlowSchedule({
req,
logger,
ctx,
attachParams,
config,
schedule,
@@ -172,7 +170,7 @@ export const handleUpgradeFlow = async ({
if (curCusProduct) {
logger.info(`UPGRADE FLOW: expiring previous cus product`);
await CusProductService.update({
db: req.db,
db,
cusProductId: curCusProduct.id,
updates: {
subscription_ids: canceled ? undefined : [],
@@ -183,7 +181,7 @@ export const handleUpgradeFlow = async ({
try {
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: curCusProduct.internal_customer_id,
org: attachParams.org,
env: attachParams.customer.env,
@@ -191,7 +189,6 @@ export const handleUpgradeFlow = async ({
attachParams.customer.id || attachParams.customer.internal_id,
scenario: AttachScenario.Expired,
cusProduct: curCusProduct,
logger,
});
} catch (error) {
logger.error("UPGRADE FLOW: failed to add to webhook queue", { error });
@@ -211,7 +208,7 @@ export const handleUpgradeFlow = async ({
}
await createFullCusProduct({
db: req.db,
db,
attachParams: attachToInsertParams(
attachParams,
attachParams.products[0],
@@ -229,24 +226,32 @@ export const handleUpgradeFlow = async ({
});
}
if (res) {
if (req.apiVersion.gte(ApiVersion.V1_1)) {
res.status(200).json(
AttachResultSchema.parse({
customer_id: attachParams.customer.id,
product_ids: attachParams.products.map((p) => p.id),
return AttachFunctionResponseSchema.parse({
code: SuccessCode.UpgradedToNewProduct,
message: `Successfully updated product`,
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`,
});
}
}
// if (res) {
// if (req.apiVersion.gte(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: latestInvoice || undefined })
// : undefined,
// code: "updated_product_successfully",
// message: `Successfully updated product`,
// }),
// );
// } else {
// res.status(200).json({
// success: true,
// message: `Successfully updated product`,
// });
// }
// }
};

View File

@@ -1,40 +1,36 @@
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachConfig, FullCusProduct } from "@autumn/shared";
import Stripe from "stripe";
import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js";
import {
logPhases,
getCurrentPhaseIndex,
} from "../../mergeUtils/phaseUtils/phaseUtils.js";
import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import type { AttachConfig, FullCusProduct } from "@autumn/shared";
import type Stripe from "stripe";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
ACTIVE_STATUSES,
CusProductService,
} from "@/internal/customers/cusProducts/CusProductService.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js";
import { getCurrentPhaseIndex } from "../../mergeUtils/phaseUtils/phaseUtils.js";
import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js";
export const handleUpgradeFlowSchedule = async ({
req,
ctx,
attachParams,
config,
schedule,
curSub,
removeCusProducts,
logger,
fromAddProduct = false,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
schedule: Stripe.SubscriptionSchedule;
curSub: Stripe.Subscription;
removeCusProducts?: FullCusProduct[];
logger: any;
fromAddProduct?: boolean;
}) => {
const { logger } = ctx;
if (fromAddProduct) {
logger.info(`ADD PRODUCT FLOW, updating schedule ${schedule?.id}`);
} else {
@@ -51,11 +47,11 @@ export const handleUpgradeFlowSchedule = async ({
const nextPhaseIndex = currentPhaseIndex + 1;
if (currentPhaseIndex == -1 || nextPhaseIndex >= schedule.phases.length)
if (currentPhaseIndex === -1 || nextPhaseIndex >= schedule.phases.length)
return;
const newItems = await paramsToScheduleItems({
req,
ctx,
schedule,
attachParams,
config,
@@ -72,13 +68,13 @@ export const handleUpgradeFlowSchedule = async ({
// If there are no subsequent phases, release schedule...
// Example: mergedUpgrade4.test.ts, mergedCancel2.test.ts
// pro, pro -> free, pro -> premium, pro (need to cancel initial schedule)
if (newCurPhaseIndex == newItems.phases.length - 1) {
if (newCurPhaseIndex === newItems.phases.length - 1) {
logger.info(
`UPGRADE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`,
);
await stripeCli.subscriptionSchedules.release(schedule!.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
db: ctx.db,
stripeScheduledId: schedule!.id,
updates: { scheduled_ids: [] },
});
@@ -111,7 +107,7 @@ export const handleUpgradeFlowSchedule = async ({
// });
await updateCurSchedule({
req,
ctx,
attachParams,
schedule,
newPhases: newItems.phases,

View File

@@ -11,7 +11,7 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/free
import { SubService } from "@/internal/subscriptions/SubService.js";
import { nullish } from "@/utils/genUtils.js";
import type { ItemSet } from "@/utils/models/ItemSet.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js";
import { createAndFilterContUseItems } from "../../attachUtils/getContUseItems/createContUseInvoiceItems.js";
import {
@@ -20,21 +20,21 @@ import {
} from "../upgradeDiffIntFlow/createUsageInvoiceItems.js";
export const updateStripeSub2 = async ({
req,
ctx,
attachParams,
config,
curSub,
itemSet,
fromCreate = false,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachParams: AttachParams;
config: AttachConfig;
curSub: Stripe.Subscription;
itemSet: ItemSet;
fromCreate?: boolean;
}) => {
const { db, logger } = req;
const { db, logger } = ctx;
const { stripeCli, paymentMethod } = attachParams;
const { invoiceOnly, proration } = config;
@@ -72,15 +72,16 @@ export const updateStripeSub2 = async ({
: fromCreate
? "always_invoice"
: "create_prorations",
// proration_behavior: "create_prorations",
trial_end: trialEnd,
// default_payment_method: paymentMethod?.id,
add_invoice_items: itemSet.invoiceItems,
...((invoiceOnly && {
...(invoiceOnly && {
collection_method: "send_invoice",
days_until_due: 30,
}) as any),
}),
payment_behavior: "error_if_incomplete",
expand: ["latest_invoice"],
});
@@ -120,8 +121,10 @@ export const updateStripeSub2 = async ({
logger,
});
let url = null;
if (proration === ProrationBehavior.Immediately) {
latestInvoice = await createProrationInvoice({
const res = await createProrationInvoice({
ctx,
attachParams,
invoiceOnly,
curSub,
@@ -129,20 +132,30 @@ export const updateStripeSub2 = async ({
logger,
});
latestInvoice = res.invoice;
url = res.url;
console.log(`FINALIZED INVOICE ${latestInvoice?.id}`);
console.log(latestInvoice?.lines.data.map((line) => line.description));
}
// If url is returned, it means invoice action is required, so don't reset balances.
if (!url) {
await resetUsageBalances({
db,
cusEntIds,
cusProduct: curMainProduct!,
});
} else {
// reset balances later when invoice is paid
attachParams.cusEntIds = cusEntIds;
}
return {
updatedSub,
latestInvoice: latestInvoice,
cusEntIds,
replaceables,
url,
};
};

View File

@@ -27,7 +27,6 @@ import {
import RecaseError from "@/utils/errorUtils.js";
import { notNullish, nullOrUndefined } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { handleAttach } from "./handleAttach.js";
import { handleAttachPreview } from "./handleAttachPreview/handleAttachPreview.js";
export const attachRouter: Router = Router();
@@ -249,6 +248,5 @@ export const customerHasPm = async ({
return notNullish(paymentMethod);
};
attachRouter.post("/attach", handleAttach);
attachRouter.post("/attach/preview", handleAttachPreview);
// attachRouter.post("/checkout", handleCheckout);

View File

@@ -2,21 +2,19 @@ import type { FullCustomer, FullProduct } from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getFreeTrialAfterFingerprint } from "@/internal/products/free-trials/freeTrialUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import { getStripeCusData } from "./attachParamsUtils/getStripeCusData.js";
export const checkToAttachParams = async ({
req,
ctx,
customer,
product,
logger,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
customer: FullCustomer;
product: FullProduct;
logger: any;
}) => {
const { org, env, db } = req;
const { org, env, db, logger } = ctx;
// const apiVersion =
// orgToVersion({
@@ -62,10 +60,10 @@ export const checkToAttachParams = async ({
replaceables: [],
// Others
req,
org: req.org,
req: ctx,
org: ctx.org,
entities: customer.entities,
features: req.features,
features: ctx.features,
internalEntityId: customer.entity?.internal_id,
cusProducts: customer.customer_products,

View File

@@ -9,7 +9,6 @@ import {
type FullCustomer,
type FullProduct,
type FullRewardProgram,
type Organization,
} from "@autumn/shared";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
@@ -18,12 +17,8 @@ import type {
InsertCusProductParams,
} from "@/internal/customers/cusProducts/AttachParams.js";
import { newCusToFullCus } from "@/internal/customers/cusUtils/cusUtils.js";
import {
isFreeProduct,
isOneOff,
itemsAreOneOff,
} from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
export const webhookToAttachParams = ({
req,
@@ -64,23 +59,24 @@ export const webhookToAttachParams = ({
};
export const productToInsertParams = ({
req,
ctx,
fullCus,
newProduct,
entities,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
fullCus: FullCustomer;
newProduct: FullProduct;
entities?: Entity[];
}): InsertCusProductParams => {
const { org, features } = ctx;
const params: InsertCusProductParams = {
customer: fullCus,
org: req.org,
org,
product: newProduct,
prices: newProduct.prices,
entitlements: newProduct.entitlements,
features: req.features,
features,
cusProducts: fullCus.customer_products,
freeTrial: null,
optionsList: [],
@@ -93,18 +89,19 @@ export const productToInsertParams = ({
};
export const newCusToAttachParams = ({
req,
ctx,
newCus,
products,
stripeCli,
freeTrial = null,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
newCus: FullCustomer;
products: FullProduct[];
stripeCli: Stripe;
freeTrial?: FreeTrial | null;
}) => {
const { org } = ctx;
if (!newCus.customer_products) {
newCus.customer_products = [];
}
@@ -118,8 +115,8 @@ export const newCusToAttachParams = ({
const attachParams: AttachParams = {
stripeCli,
paymentMethod: null,
req,
org: req.org,
req: ctx,
org,
customer: newCus,
products,
prices: products.flatMap((p) => p.prices),
@@ -136,19 +133,20 @@ export const newCusToAttachParams = ({
};
export const newCusToInsertParams = ({
req,
ctx,
newCus,
product,
freeTrial = null,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
newCus: Customer;
product: FullProduct;
freeTrial?: FreeTrial | null;
}) => {
const { org } = ctx;
return {
req,
org: req.org,
req: ctx,
org,
customer: newCusToFullCus({ newCus }),
product,
prices: product.prices,
@@ -163,26 +161,23 @@ export const newCusToInsertParams = ({
};
export const rewardProgramToAttachParams = ({
req,
ctx,
rewardProgram,
customer,
product,
org,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
rewardProgram: FullRewardProgram;
customer: FullCustomer;
product: FullProduct;
org?: Organization;
}): AttachParams => {
const { org, env, features } = ctx;
const reward = rewardProgram.reward;
const isPaid = !isFreeProduct(product.prices);
const isRecurring =
!isOneOff(product.prices) && !itemsAreOneOff(product.entitlements);
return {
req,
org: org || req.org,
req: ctx,
org,
customer,
products: [product],
prices: product.prices,
@@ -192,11 +187,8 @@ export const rewardProgramToAttachParams = ({
optionsList: [],
cusProducts: customer.customer_products,
entities: [],
features: req.features,
stripeCli: createStripeCli({
org: org || req.org,
env: req.env,
}),
features,
stripeCli: createStripeCli({ org, env }),
paymentMethod: null,
replaceables: [],
} satisfies AttachParams;

View File

@@ -1,7 +1,6 @@
import type { AttachBodyV0 } from "@autumn/shared";
import { nullish } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import type { ExtendedRequest } from "../../../../../utils/models/Request.js";
import type { AttachParams } from "../../../cusProducts/AttachParams.js";
import { processAttachBody } from "./processAttachBody.js";
@@ -52,7 +51,7 @@ export const getAttachParams = async ({
replaceables: [],
rewards,
// From req
req: ctx as ExtendedRequest,
req: ctx,
org: ctx.org,
entities: customer.entities,

View File

@@ -3,9 +3,12 @@ import {
AttachBranch,
type AttachConfig,
AttachFunction,
type AttachFunctionResponse,
AttachFunctionResponseSchema,
CusProductStatus,
} from "@autumn/shared";
import chalk from "chalk";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
@@ -106,21 +109,19 @@ export const getAttachFunction = async ({
};
export const runAttachFunction = async ({
req,
res,
ctx,
branch,
attachParams,
attachBody,
config,
}: {
req: any;
res: any;
ctx: AutumnContext;
branch: AttachBranch;
attachParams: AttachParams;
attachBody: AttachBodyV0;
config: AttachConfig;
}) => {
const { logger, db } = req;
}): Promise<AttachFunctionResponse> => {
const { logger, db } = ctx;
const { stripeCli } = attachParams;
const attachFunction = await getAttachFunction({
@@ -139,8 +140,6 @@ export const runAttachFunction = async ({
attachParams,
});
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
logger.info(`--------------------------------`);
logger.info(
`ATTACHING ${productIdsStr} to ${customer.name} (${customer.id || customer.email}), org: ${org.slug}`,
@@ -166,8 +165,7 @@ export const runAttachFunction = async ({
if (attachFunction === AttachFunction.OneOff) {
return await handleOneOffFunction({
req,
res,
ctx,
attachParams,
config,
});
@@ -175,8 +173,7 @@ export const runAttachFunction = async ({
if (attachFunction === AttachFunction.Renew) {
return await handleRenewProduct({
req,
res,
ctx,
attachParams,
config,
});
@@ -206,8 +203,7 @@ export const runAttachFunction = async ({
if (attachFunction === AttachFunction.MultiAttach) {
return await handleMultiAttachFlow({
req,
res,
ctx,
attachParams,
attachBody,
branch,
@@ -218,17 +214,13 @@ export const runAttachFunction = async ({
if (attachFunction === AttachFunction.CreateCheckout) {
if (config.invoiceCheckout) {
return await handleCreateInvoiceCheckout({
req,
res,
ctx,
attachParams,
attachBody,
config,
branch,
});
}
return await handleCreateCheckout({
req,
res,
ctx,
attachParams,
config,
});
@@ -236,8 +228,7 @@ export const runAttachFunction = async ({
if (attachFunction === AttachFunction.AddProduct) {
return await handleAddProduct({
req,
res,
ctx,
attachParams,
config,
branch,
@@ -246,8 +237,7 @@ export const runAttachFunction = async ({
if (attachFunction === AttachFunction.ScheduleProduct) {
return await handleScheduleFunction2({
req,
res,
ctx,
attachParams,
config,
});
@@ -258,8 +248,7 @@ export const runAttachFunction = async ({
attachFunction === AttachFunction.UpgradeSameInterval
) {
return await handleUpgradeFlow({
req,
res,
ctx,
attachParams,
config,
branch,
@@ -268,10 +257,14 @@ export const runAttachFunction = async ({
if (attachFunction === AttachFunction.UpdatePrepaidQuantity) {
return await handleUpdateQuantityFunction({
req,
res,
ctx,
attachParams,
config,
});
}
return AttachFunctionResponseSchema.parse({
code: "attach_function_not_found",
message: `Attach function not found: ${attachFunction}`,
});
};

View File

@@ -1,104 +1,98 @@
import { AttachBodyV0Schema } from "@autumn/shared";
import { handleAttachRaceCondition } from "@/external/redis/redisUtils.js";
import type {
ExtendedRequest,
ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { checkStripeConnections } from "./attachRouter.js";
import { getAttachParams } from "./attachUtils/attachParams/getAttachParams.js";
import { getAttachBranch } from "./attachUtils/getAttachBranch.js";
import { getAttachConfig } from "./attachUtils/getAttachConfig.js";
import { runAttachFunction } from "./attachUtils/getAttachFunction.js";
import { handleAttachErrors } from "./attachUtils/handleAttachErrors.js";
import { insertCustomItems } from "./attachUtils/insertCustomItems.js";
// import { AttachBodyV0Schema } from "@autumn/shared";
// import { handleAttachRaceCondition } from "@/external/redis/redisUtils.js";
// import type {
// ExtendedRequest,
// ExtendedResponse,
// } from "@/utils/models/Request.js";
// import { routeHandler } from "@/utils/routerUtils.js";
// import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
// import { checkStripeConnections } from "./attachRouter.js";
// import { getAttachParams } from "./attachUtils/attachParams/getAttachParams.js";
// import { getAttachBranch } from "./attachUtils/getAttachBranch.js";
// import { getAttachConfig } from "./attachUtils/getAttachConfig.js";
// import { runAttachFunction } from "./attachUtils/getAttachFunction.js";
// import { handleAttachErrors } from "./attachUtils/handleAttachErrors.js";
// import { insertCustomItems } from "./attachUtils/insertCustomItems.js";
export const handleAttach = async (req: any, res: any) =>
routeHandler({
req,
res,
action: "attach",
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
await handleAttachRaceCondition({ req, res });
// export const handleAttach = async (req: any, res: any) =>
// routeHandler({
// req,
// res,
// action: "attach",
// handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
// await handleAttachRaceCondition({ req, res });
const attachBody = AttachBodyV0Schema.parse(req.body);
// const attachBody = AttachBodyV0Schema.parse(req.body);
const ctx = req as AutumnContext;
// const ctx = req as AutumnContext;
const { attachParams, customPrices, customEnts } = await getAttachParams({
ctx,
attachBody,
});
// const { attachParams, customPrices, customEnts } = await getAttachParams({
// ctx,
// attachBody,
// });
// console.log("Options list: ", attachParams.optionsList);
// throw new Error(
// "Options list: " + JSON.stringify(attachParams.optionsList),
// );
// // Handle existing product
// const branch = await getAttachBranch({
// ctx,
// attachBody,
// attachParams,
// });
// Handle existing product
const branch = await getAttachBranch({
ctx,
attachBody,
attachParams,
});
// const { flags, config } = await getAttachConfig({
// ctx,
// attachParams,
// attachBody,
// branch,
// });
const { flags, config } = await getAttachConfig({
ctx,
attachParams,
attachBody,
branch,
});
// await handleAttachErrors({
// attachParams,
// attachBody,
// branch,
// flags,
// config,
// });
await handleAttachErrors({
attachParams,
attachBody,
branch,
flags,
config,
});
// await checkStripeConnections({
// ctx,
// attachParams,
// useCheckout: config.onlyCheckout,
// });
await checkStripeConnections({
ctx,
attachParams,
useCheckout: config.onlyCheckout,
});
// await insertCustomItems({
// db: req.db,
// customPrices: customPrices || [],
// customEnts: customEnts || [],
// });
await insertCustomItems({
db: req.db,
customPrices: customPrices || [],
customEnts: customEnts || [],
});
// try {
// req.logger.info(`Attach params: `, {
// data: {
// products: attachParams.products.map((p) => ({
// id: p.id,
// name: p.name,
// processor: p.processor,
// version: p.version,
// })),
// prices: attachParams.prices.map((p) => ({
// id: p.id,
// config: p.config,
// })),
// entitlements: attachParams.entitlements.map((e) => ({
// internal_feature_id: e.internal_feature_id,
// feature_id: e.feature_id,
// })),
// freeTrial: attachParams.freeTrial,
// },
// });
// } catch (_error) {}
try {
req.logger.info(`Attach params: `, {
data: {
products: attachParams.products.map((p) => ({
id: p.id,
name: p.name,
processor: p.processor,
version: p.version,
})),
prices: attachParams.prices.map((p) => ({
id: p.id,
config: p.config,
})),
entitlements: attachParams.entitlements.map((e) => ({
internal_feature_id: e.internal_feature_id,
feature_id: e.feature_id,
})),
freeTrial: attachParams.freeTrial,
},
});
} catch (_error) {}
await runAttachFunction({
req,
res,
attachParams,
branch,
attachBody,
config,
});
},
});
// await runAttachFunction({
// ctx,
// attachParams,
// branch,
// attachBody,
// config,
// });
// },
// });

View File

@@ -1,23 +1,26 @@
import Stripe from "stripe";
import { mergeNewScheduleItems } from "./mergeNewSubItems.js";
import { getCusProductsToRemove } from "./paramsToSubItems.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
type AttachConfig,
cusProductToPrices,
type FullCusProduct,
} from "@autumn/shared";
import { differenceInDays } from "date-fns";
import type Stripe from "stripe";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { AttachConfig, FullCusProduct } from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { cusProductToPrices } from "@autumn/shared";
import { isArrearPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
priceToScheduleItem,
scheduleItemInCusProduct,
} from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { isArrearPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { formatPrice } from "@/internal/products/prices/priceUtils.js";
import { differenceInDays } from "date-fns";
import { formatUnixToDateTime } from "@/utils/genUtils.js";
import type { ItemSet } from "@/utils/models/ItemSet.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import { mergeNewScheduleItems } from "./mergeNewSubItems.js";
import { getQuantityToRemove } from "./mergeUtils.js";
import { getCusProductsToRemove } from "./paramsToSubItems.js";
import { mergeAdjacentPhasesWithSameItems } from "./phaseUtils/mergeSimilarPhases.js";
import { preparePhasesForBillingPeriod } from "./phaseUtils/upsertNewPhase.js";
import { getQuantityToRemove } from "./mergeUtils.js";
export const removeCusProductFromScheduleItems = async ({
curScheduleItems,
@@ -92,7 +95,7 @@ export const removeCusProductFromScheduleItems = async ({
if (
itemSet?.subItems.some(
(si) => si.price == (existingScheduleItem.price as Stripe.Price)?.id,
(si) => si.price === (existingScheduleItem.price as Stripe.Price)?.id,
)
) {
continue;
@@ -151,7 +154,7 @@ const logScheduleItems = ({
for (const cusProduct of cusProducts) {
const prices = cusProductToPrices({ cusProduct });
const price = prices.find((p) => {
return p.config.stripe_price_id == item.price;
return p.config.stripe_price_id === item.price;
});
if (price) {
@@ -225,7 +228,8 @@ const computeUpdatedScheduleItems = async ({
};
export const paramsToScheduleItems = async ({
req,
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be used in the future
ctx,
sub,
schedule,
attachParams,
@@ -233,7 +237,7 @@ export const paramsToScheduleItems = async ({
removeCusProducts,
billingPeriodEnd,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
sub?: Stripe.Subscription;
schedule?: Stripe.SubscriptionSchedule;
attachParams: AttachParams;
@@ -241,14 +245,11 @@ export const paramsToScheduleItems = async ({
removeCusProducts?: FullCusProduct[];
billingPeriodEnd?: number;
}) => {
const { logger } = req;
const itemSet = await getStripeSubItems2({
attachParams,
config,
});
let curScheduleItems: any[] = [];
let phaseIndex = -1;
if (billingPeriodEnd && schedule && schedule.phases.length > 1) {

View File

@@ -15,7 +15,7 @@ import { isArrearPrice } from "@/internal/products/prices/priceUtils/usagePriceU
import { formatPrice } from "@/internal/products/prices/priceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ItemSet } from "@/utils/models/ItemSet.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { mergeNewSubItems } from "./mergeNewSubItems.js";
@@ -91,14 +91,15 @@ export const getCusProductsToRemove = ({
};
export const paramsToSubItems = async ({
req,
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be used in the future
ctx,
sub,
attachParams,
config,
removeCusProducts,
addItemSet,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
sub?: Stripe.Subscription;
attachParams: AttachParams;
config: AttachConfig;
@@ -119,10 +120,10 @@ export const paramsToSubItems = async ({
? removeCusProducts!
: getCusProductsToRemove({ attachParams });
console.log(
"Cus products to remove:",
cusProductsToRemove.map((cp) => cp.product.name),
);
// console.log(
// "Cus products to remove:",
// cusProductsToRemove.map((cp) => cp.product.name),
// );
const newSubItems = mergeNewSubItems({
itemSet,

View File

@@ -1,28 +1,27 @@
import type { AttachConfig, FullCusProduct } from "@autumn/shared";
import type Stripe from "stripe";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import { CusProductService } from "../../cusProducts/CusProductService.js";
import { paramsToScheduleItems } from "./paramsToScheduleItems.js";
import { getCusProductsToRemove } from "./paramsToSubItems.js";
export const subToNewSchedule = async ({
req,
ctx,
sub,
attachParams,
config,
endOfBillingPeriod,
removeCusProducts,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
sub: Stripe.Subscription;
attachParams: AttachParams;
config: AttachConfig;
endOfBillingPeriod: number;
removeCusProducts?: FullCusProduct[];
}) => {
const { logger } = req;
const itemSet = await getStripeSubItems2({
attachParams,
config,
@@ -39,7 +38,7 @@ export const subToNewSchedule = async ({
);
const res = await paramsToScheduleItems({
req,
ctx,
sub,
attachParams,
config,
@@ -66,7 +65,6 @@ export const subToNewSchedule = async ({
if (res.phases[0].items.length > 0) {
itemSet.subItems = res.phases[0].items;
const curSubItems = sub.items.data;
// Create schedule from existing subscription
newSchedule = await stripeCli.subscriptionSchedules.create({
@@ -97,7 +95,7 @@ export const subToNewSchedule = async ({
});
await CusProductService.updateByStripeSubId({
db: req.db,
db: ctx.db,
stripeSubId: sub.id!,
updates: {
scheduled_ids: [newSchedule!.id],

View File

@@ -1,17 +1,16 @@
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import Stripe from "stripe";
import { CusProductService } from "../../cusProducts/CusProductService.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import type Stripe from "stripe";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
export const updateCurSchedule = async ({
req,
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be used in the future
ctx,
attachParams,
schedule,
sub,
newPhases,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachParams: AttachParams;
schedule: Stripe.SubscriptionSchedule;
sub: Stripe.Subscription;

View File

@@ -8,24 +8,24 @@ import {
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { activateDefaultProduct } from "../cusProducts/cusProductUtils.js";
export const cancelImmediately = async ({
req,
ctx,
cusProduct,
fullCus,
prorate,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
cusProduct: FullCusProduct;
fullCus: FullCustomer;
prorate: boolean;
}) => {
const { db, org, env, logger } = req;
const { db, org, env } = ctx;
const stripeCli = createStripeCli({ org, env });
const { curScheduledProduct } = getExistingCusProducts({
@@ -58,7 +58,7 @@ export const cancelImmediately = async ({
}
await activateDefaultProduct({
req,
ctx,
productGroup: cusProduct.product.group,
fullCus,
});
@@ -75,13 +75,12 @@ export const cancelImmediately = async ({
console.log("Sending webhook for expired product");
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: fullCus.internal_id,
org,
env,
customerId: fullCus.id || null,
cusProduct,
scenario: AttachScenario.Expired,
logger,
});
};

View File

@@ -8,6 +8,7 @@ import { CusService } from "@/internal/customers/CusService.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { RELEVANT_STATUSES } from "../cusProducts/CusProductService.js";
import { handleCancelProduct } from "./handleCancelProduct.js";
@@ -68,16 +69,8 @@ cancelRouter.post("", async (req, res) =>
});
}
// await expireCusProduct({
// req,
// cusProduct,
// fullCus,
// expireImmediately,
// prorate,
// });
await handleCancelProduct({
req,
ctx: req as unknown as AutumnContext,
cusProduct,
fullCus,
expireImmediately,

View File

@@ -12,7 +12,7 @@ import {
} from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { handleRenewProduct } from "../attach/attachFunctions/handleRenewProduct.js";
import { handleScheduleFunction2 } from "../attach/attachFunctions/scheduleFlow/handleScheduleFlow2.js";
import { handleUpgradeFlow } from "../attach/attachFunctions/upgradeFlow/handleUpgradeFlow.js";
@@ -24,19 +24,19 @@ import {
} from "../cusProducts/cusProductUtils.js";
export const handleCancelProduct = async ({
req,
ctx,
cusProduct, // cus product to expire
fullCus,
expireImmediately = true,
prorate,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
cusProduct: FullCusProduct;
fullCus: FullCustomer;
expireImmediately: boolean;
prorate: boolean;
}) => {
const { org, env, logger } = req;
const { org, env, logger, features } = ctx;
logger.info("--------------------------------");
logger.info(
`🔔 Expiring cutomer product (${
@@ -68,8 +68,7 @@ export const handleCancelProduct = async ({
const product = cusProductToProduct({ cusProduct: curMainProduct! });
await handleRenewProduct({
req,
res: null,
ctx,
attachParams: {
stripeCli,
customer: fullCus,
@@ -84,7 +83,7 @@ export const handleCancelProduct = async ({
optionsList: curMainProduct?.options || [],
replaceables: [],
entities: fullCus.entities,
features: req.features,
features,
},
config: getDefaultAttachConfig(),
});
@@ -116,7 +115,7 @@ export const handleCancelProduct = async ({
// 2. If expire at cycle end, just cancel subscriptions
if (!expireImmediately && !isFree) {
const defaultProduct = await getDefaultProduct({
req,
ctx,
productGroup: product.group,
});
@@ -136,8 +135,7 @@ export const handleCancelProduct = async ({
}
await handleScheduleFunction2({
req,
res: null,
ctx,
attachParams: {
stripeCli,
customer: fullCus,
@@ -152,7 +150,7 @@ export const handleCancelProduct = async ({
optionsList: [],
replaceables: [],
entities: fullCus.entities,
features: req.features,
features,
fromCancel: true,
},
config: getDefaultAttachConfig(),
@@ -166,8 +164,7 @@ export const handleCancelProduct = async ({
// Cancel product immediately
await handleUpgradeFlow({
req,
res: null,
ctx,
attachParams: {
stripeCli,
customer: fullCus,
@@ -183,7 +180,7 @@ export const handleCancelProduct = async ({
optionsList: [],
replaceables: [],
entities: fullCus.entities,
features: req.features,
features,
fromCancel: true,
},
config: {
@@ -199,7 +196,7 @@ export const handleCancelProduct = async ({
// Activate default product
if (!product.is_add_on && !isOneOff(product.prices)) {
await activateDefaultProduct({
req,
ctx,
productGroup: cusProduct.product.group,
fullCus,
curCusProduct: cusProduct,

View File

@@ -1,5 +1,6 @@
import type {
ApiVersion,
AttachConfig,
AttachReplaceable,
AttachScenario,
Customer,
@@ -17,8 +18,7 @@ import type {
Reward,
} from "@autumn/shared";
import type Stripe from "stripe";
import { z } from "zod";
import type { AutumnContext } from "../../../honoUtils/HonoEnv";
// Get misc
@@ -68,19 +68,28 @@ export type AttachParams = {
entityId?: string;
internalEntityId?: string;
checkoutSessionParams?: any;
checkoutSessionParams?: unknown;
apiVersion?: ApiVersion;
scenario?: AttachScenario;
fromMigration?: boolean;
finalizeInvoice?: boolean;
req?: any;
req?: AutumnContext;
fromCancel?: boolean;
setupPayment?: boolean;
// For invoice checkout...
anchorToUnix?: number;
subId?: string;
config?: AttachConfig;
// Invoice action required
stripeInvoiceId?: string;
cusEntIds?: string[];
};
export type InsertCusProductParams = {
req?: any;
req?: AutumnContext;
now?: number;
customer: Customer;
@@ -113,14 +122,14 @@ export type InsertCusProductParams = {
finalizeInvoice?: boolean;
};
export const AttachResultSchema = z.object({
customer_id: z.string(),
product_ids: z.array(z.string()),
code: z.string(),
message: z.string(),
// export const AttachResultSchema = z.object({
// customer_id: z.string(),
// product_ids: z.array(z.string()),
// code: z.string(),
// message: z.string(),
checkout_url: z.string().nullish(),
invoice: z.any().nullish(),
});
// checkout_url: z.string().nullish(),
// invoice: z.any().nullish(),
// });
export type AttachResult = z.infer<typeof AttachResultSchema>;
// export type AttachResult = z.infer<typeof AttachResultSchema>;

View File

@@ -8,10 +8,10 @@ import {
type FullCusProduct,
InternalError,
products,
RecaseError,
} from "@autumn/shared";
import { and, arrayContains, eq, inArray, isNotNull, or } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
export const ACTIVE_STATUSES = [
CusProductStatus.Active,

View File

@@ -13,7 +13,7 @@ import { ProductService } from "@/internal/products/ProductService.js";
import { isDefaultTrialFullProduct } from "@/internal/products/productUtils/classifyProduct.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { nullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handleAddProduct.js";
import { newCusToAttachParams } from "../attach/attachUtils/attachParams/convertToParams.js";
import { initStripeCusAndProducts } from "../handlers/handleCreateCustomer.js";
@@ -21,13 +21,13 @@ import { CusProductService, RELEVANT_STATUSES } from "./CusProductService.js";
import { getExistingCusProducts } from "./cusProductUtils/getExistingCusProducts.js";
export const getDefaultProduct = async ({
req,
ctx,
productGroup,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
productGroup: string;
}) => {
const { db, org, env, logger } = req;
const { db, org, env } = ctx;
const defaultProducts = await ProductService.listDefault({
db,
orgId: org.id,
@@ -44,17 +44,17 @@ export const getDefaultProduct = async ({
// This function is only used in cancellation flows
export const activateDefaultProduct = async ({
req,
ctx,
productGroup,
fullCus,
curCusProduct,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
productGroup: string;
fullCus: FullCustomer;
curCusProduct?: FullCusProduct;
}) => {
const { db, org, env, logger } = req;
const { db, org, env, logger } = ctx;
// 1. Expire current product
const defaultProducts = await ProductService.listDefault({
db,
@@ -103,9 +103,9 @@ export const activateDefaultProduct = async ({
}
await handleAddProduct({
req,
ctx,
attachParams: newCusToAttachParams({
req,
ctx,
newCus: fullCus,
products: [defaultProd],
stripeCli,
@@ -116,13 +116,13 @@ export const activateDefaultProduct = async ({
};
export const activateFutureProduct = async ({
req,
ctx,
cusProduct,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
cusProduct: FullCusProduct;
}) => {
const { db, org, env, logger } = req;
const { db, org, env, logger } = ctx;
const cusProducts = await CusProductService.list({
db,
@@ -147,176 +147,18 @@ export const activateFutureProduct = async ({
});
await addProductsUpdatedWebhookTask({
req,
ctx,
internalCustomerId: cusProduct.internal_customer_id,
org,
env,
customerId: null,
scenario: AttachScenario.New,
cusProduct: futureProduct,
logger,
});
return futureProduct;
};
// export const processFullCusProduct = ({
// cusProduct,
// subs,
// org,
// entities = [],
// apiVersion,
// }: {
// cusProduct: FullCusProduct;
// org: Organization;
// subs?: Subscription[];
// entities?: Entity[];
// apiVersion: ApiVersionClass;
// }) => {
// // Process prices
// const prices = cusProduct.customer_prices.map((cp) => {
// const price = cp.price;
// if (price.config?.type === PriceType.Fixed) {
// const config = price.config as FixedPriceConfig;
// return {
// amount: config.amount,
// interval: config.interval,
// };
// } else {
// const config = price.config as UsagePriceConfig;
// const priceOptions = getPriceOptions(price, cusProduct.options);
// const usageTier = getUsageTier(price, priceOptions?.quantity!);
// const cusEnt = getRelatedCusEnt({
// cusPrice: cp,
// cusEnts: cusProduct.customer_entitlements,
// });
// const ent = cusEnt?.entitlement;
// const singleTier =
// ent?.allowance === 0 && config.usage_tiers.length === 1;
// if (singleTier) {
// return {
// amount: usageTier.amount,
// interval: config.interval,
// quantity: priceOptions?.quantity,
// };
// } else {
// // Add allowance to tiers
// const allowance = ent?.allowance;
// let tiers;
// if (notNullish(allowance) && allowance! > 0) {
// tiers = [
// {
// to: allowance,
// amount: 0,
// },
// ...config.usage_tiers.map((tier) => {
// const isLastTier = tier.to === -1 || tier.to === TierInfinite;
// return {
// to: isLastTier ? tier.to : Number(tier.to) + allowance!,
// amount: tier.amount,
// };
// }),
// ];
// } else {
// tiers = config.usage_tiers.map((tier) => {
// const isLastTier = tier.to === -1 || tier.to === TierInfinite;
// return {
// to: isLastTier ? tier.to : Number(tier.to) + allowance!,
// amount: tier.amount,
// };
// });
// }
// return {
// tiers: tiers,
// name: "",
// quantity: priceOptions?.quantity,
// };
// }
// }
// });
// const trialing =
// cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
// const subIds = cusProduct.subscription_ids;
// let stripeSubData = {};
// if (subIds && subIds.length > 0 && apiVersion.gte(ApiVersion.V0_2)) {
// const baseSub = subs?.find(
// (s) => s.id === subIds[0] || (s as Subscription).stripe_id === subIds[0],
// );
// stripeSubData = {
// current_period_end: baseSub?.current_period_end
// ? baseSub.current_period_end * 1000
// : null,
// current_period_start: baseSub?.current_period_start
// ? baseSub.current_period_start * 1000
// : null,
// };
// }
// if (!subIds && trialing) {
// stripeSubData = {
// current_period_start: cusProduct.starts_at,
// current_period_end: cusProduct.trial_ends_at,
// };
// }
// if (apiVersion.gte(ApiVersion.V1_1)) {
// if ((!subIds || subIds.length === 0) && trialing) {
// stripeSubData = {
// current_period_start: cusProduct.starts_at,
// current_period_end: cusProduct.trial_ends_at,
// };
// }
// return ApiSubscriptionSchema.parse({
// id: cusProduct.product.id,
// name: cusProduct.product.name,
// group: cusProduct.product.group || null,
// status: trialing ? CusProductStatus.Trialing : cusProduct.status,
// canceled_at: cusProduct.canceled_at,
// is_default: cusProduct.product.is_default || false,
// is_add_on: cusProduct.product.is_add_on || false,
// stripe_subscription_ids: cusProduct.subscription_ids || [],
// started_at: cusProduct.starts_at,
// entity_id: cusProduct.internal_entity_id
// ? entities?.find((e) => e.internal_id === cusProduct.internal_entity_id)
// ?.id
// : cusProduct.entity_id || undefined,
// ...stripeSubData,
// });
// } else {
// const cusProductResponse = {
// id: cusProduct.product.id,
// name: cusProduct.product.name,
// group: cusProduct.product.group,
// status: trialing ? CusProductStatus.Trialing : cusProduct.status,
// created_at: cusProduct.created_at,
// canceled_at: cusProduct.canceled_at,
// processor: {
// type: cusProduct.processor?.type,
// subscription_id: cusProduct.processor?.subscription_id || null,
// },
// subscription_ids: cusProduct.subscription_ids || [],
// prices: prices,
// starts_at: cusProduct.starts_at,
// ...stripeSubData,
// };
// return cusProductResponse;
// }
// };
export const searchCusProducts = ({
productId,
internalProductId,
@@ -401,5 +243,5 @@ export const getFeatureQuantity = ({
const option = options.find(
(o) => o.internal_feature_id === internalFeatureId,
);
return nullish(option?.quantity) ? 1 : option?.quantity!;
return nullish(option?.quantity) ? 1 : option?.quantity;
};

View File

@@ -1,5 +1,5 @@
import { redis } from "@/external/redis/initRedis.js";
import { logger } from "../../../../external/logtail/logtailUtils.js";
import { redis } from "../../../../external/redis/initRedis.js";
/**
* Delete all cached ApiCustomer data from Redis

View File

@@ -38,7 +38,6 @@ export const setCachedApiInvoices = async ({
// Build master api customer invoices (customer-level only)
const masterApiInvoices = invoicesToResponse({
invoices: customerLevelInvoices,
logger,
});
// Then write to Redis

View File

@@ -64,7 +64,6 @@ export const getApiCustomerBase = async ({
fullCus.invoices && ctx.expand.includes(CusExpand.Invoices)
? invoicesToResponse({
invoices: fullCus.invoices,
logger: ctx.logger,
})
: undefined,
});

View File

@@ -15,7 +15,6 @@ import { isDefaultTrialFullProduct } from "@/internal/products/productUtils/clas
import { isFreeProduct } from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { generateId } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { createFullCusProduct } from "../add-product/createFullCusProduct.js";
import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handleAddProduct.js";
@@ -154,13 +153,13 @@ export const createNewCustomer = async ({
});
await handleAddProduct({
req: ctx as unknown as ExtendedRequest,
ctx,
config: {
...getDefaultAttachConfig(),
requirePaymentMethod: false,
},
attachParams: newCusToAttachParams({
req: ctx as unknown as ExtendedRequest,
ctx,
newCus: newCustomer as FullCustomer,
products: [defaultProd],
stripeCli,
@@ -171,7 +170,7 @@ export const createNewCustomer = async ({
await createFullCusProduct({
db,
attachParams: newCusToInsertParams({
req: ctx as unknown as ExtendedRequest,
ctx,
newCus: newCustomer,
product: defaultProd,
freeTrial: defaultProd?.free_trial || null,

View File

@@ -1,6 +1,6 @@
import {
ApiBaseEntitySchema,
type ApiCustomer,
ApiEntityV1Schema,
type Customer,
type CustomerData,
type CustomerLegacyData,
@@ -183,7 +183,7 @@ export const getOrCreateApiCustomer = async ({
customerId,
});
const apiEntity = ApiEntityV1Schema.parse(newEntity);
const apiEntity = ApiBaseEntitySchema.parse(newEntity);
apiCustomer.entities = [...(apiCustomer.entities || []), apiEntity];
}

View File

@@ -10,7 +10,6 @@ import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { nullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "../../../utils/models/Request.js";
import { CusService } from "../CusService.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { handleDecreaseAndTransfer } from "./handleTransferProduct/handleDecreaseAndTransfer.js";
@@ -130,7 +129,7 @@ export const handleTransferProductV2 = createRoute({
});
await addProductsUpdatedWebhookTask({
req: ctx as ExtendedRequest,
ctx,
internalCustomerId: customer.internal_id,
org: ctx.org,
env: ctx.env,
@@ -141,7 +140,6 @@ export const handleTransferProductV2 = createRoute({
entity_id: toEntity?.id || null,
internal_entity_id: toEntity?.internal_id || null,
},
logger: ctx.logger,
});
}

View File

@@ -44,7 +44,6 @@ export const getApiEntityExpand = async ({
return {
invoices: invoicesToResponse({
invoices,
logger,
}),
};
};

View File

@@ -1,10 +1,16 @@
import Stripe from "stripe";
import { AttachParams } from "../customers/cusProducts/AttachParams.js";
import { InvoiceService, processInvoice } from "./InvoiceService.js";
import type {
Invoice,
InvoiceItem,
Price,
UsagePriceConfig,
} from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { getStripeExpandedInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
import { Invoice, InvoiceItem, Price, UsagePriceConfig } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { findPriceInStripeItems } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import type { Logger } from "../../external/logtail/logtailUtils.js";
import type { AttachParams } from "../customers/cusProducts/AttachParams.js";
import { InvoiceService, processInvoice } from "./InvoiceService.js";
// Purpose of this function is to insert an invoice from attach params when sub is updated -> Correct product ID is set...
export const insertInvoiceFromAttach = async ({
@@ -18,7 +24,7 @@ export const insertInvoiceFromAttach = async ({
attachParams: AttachParams;
invoiceId?: string;
stripeInvoice?: Stripe.Invoice;
logger: any;
logger: Logger;
}) => {
try {
if (!stripeInvoice) {
@@ -29,12 +35,12 @@ export const insertInvoiceFromAttach = async ({
}
// Create or update
let invoice = await InvoiceService.getByStripeId({
const invoice = await InvoiceService.getByStripeId({
db,
stripeId: stripeInvoice.id!,
});
let autumnInvoiceItems = await getInvoiceItems({
const autumnInvoiceItems = await getInvoiceItems({
stripeInvoice,
prices: attachParams.prices,
logger,
@@ -88,13 +94,7 @@ export const insertInvoiceFromAttach = async ({
}
};
export const invoicesToResponse = ({
invoices,
logger,
}: {
invoices: Invoice[];
logger: any;
}) => {
export const invoicesToResponse = ({ invoices }: { invoices: Invoice[] }) => {
return invoices.map((i) =>
processInvoice({
invoice: i,
@@ -111,13 +111,13 @@ export const getInvoiceItems = async ({
}: {
stripeInvoice: Stripe.Invoice;
prices: Price[];
logger: any;
logger: Logger;
}) => {
let invoiceItems: InvoiceItem[] = [];
const invoiceItems: InvoiceItem[] = [];
try {
for (const line of stripeInvoice.lines.data) {
let price = findPriceInStripeItems({
const price = findPriceInStripeItems({
prices,
lineItem: line,
});
@@ -126,7 +126,7 @@ export const getInvoiceItems = async ({
continue;
}
let usageConfig = price.config as UsagePriceConfig;
const usageConfig = price.config as UsagePriceConfig;
invoiceItems.push({
price_id: price.id!,
stripe_id: line.id,

View File

@@ -1,9 +1,14 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnMetadata, metadata } from "@autumn/shared";
import { eq } from "drizzle-orm";
import {
type Metadata,
type MetadataInsert,
type MetadataType,
metadata,
} from "@autumn/shared";
import { and, eq } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
export class MetadataService {
static async insert({ db, data }: { db: DrizzleCli; data: AutumnMetadata }) {
static async insert({ db, data }: { db: DrizzleCli; data: MetadataInsert }) {
await db.insert(metadata).values(data);
}
@@ -18,6 +23,33 @@ export class MetadataService {
return null;
}
return data[0] as AutumnMetadata;
return data[0] as Metadata;
}
static async getByStripeInvoiceId({
db,
stripeInvoiceId,
type,
}: {
db: DrizzleCli;
stripeInvoiceId: string;
type?: MetadataType;
}) {
const meta = await db.query.metadata.findFirst({
where: and(
eq(metadata.stripe_invoice_id, stripeInvoiceId),
type ? eq(metadata.type, type) : undefined,
),
});
if (!meta) {
return null;
}
return meta as Metadata;
}
static async delete({ db, id }: { db: DrizzleCli; id: string }) {
await db.delete(metadata).where(eq(metadata.id, id));
}
}

View File

@@ -1,44 +1,7 @@
import type { AutumnMetadata } from "@autumn/shared";
import { addDays } from "date-fns";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { generateId } from "@/utils/genUtils.js";
import type { AttachParams } from "../customers/cusProducts/AttachParams.js";
import { MetadataService } from "./MetadataService.js";
export const createCheckoutMetadata = async ({
db,
attachParams,
}: {
db: DrizzleCli;
attachParams: AttachParams;
}) => {
const metaId = generateId("meta");
const {
req: _req,
checkoutSessionParams: _checkoutSessionParams,
stripeCli: _stripeCli,
paymentMethod: _paymentMethod,
...rest
} = attachParams;
const attachClone = structuredClone(rest);
const metadata: AutumnMetadata = {
id: metaId,
created_at: Date.now(),
expires_at: addDays(Date.now(), 10).getTime(), // 10 days
data: {
...attachClone,
},
};
await MetadataService.insert({ db, data: metadata });
return metaId;
};
export const getMetadataFromCheckoutSession = async (
checkoutSession: Stripe.Checkout.Session,
db: DrizzleCli,

View File

@@ -11,6 +11,8 @@ import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { Logger } from "../../../external/logtail/logtailUtils.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { deleteCachedApiCustomer } from "../../customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
import { migrationToAttachParams } from "../migrationUtils/migrationToAttachParams.js";
import { runMigrationAttach } from "../migrationUtils/runMigrationAttach.js";
@@ -34,7 +36,7 @@ export const migrateCustomer = async ({
orgId: string;
fromProduct: FullProduct;
toProduct: FullProduct;
logger: any;
logger: Logger;
features: Feature[];
migrationJob?: MigrationJob;
}) => {
@@ -75,7 +77,7 @@ export const migrateCustomer = async ({
});
await runMigrationAttach({
req,
ctx: req as unknown as AutumnContext,
attachParams,
fromProduct,
});

View File

@@ -3,6 +3,7 @@ import type Stripe from "stripe";
import { getStripeCusData } from "@/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv";
export const migrationToAttachParams = async ({
req,
@@ -30,6 +31,8 @@ export const migrationToAttachParams = async ({
allowNoStripe: true,
});
const ctx = req as unknown as AutumnContext;
const attachParams: AttachParams = {
stripeCli,
stripeCus,
@@ -44,10 +47,10 @@ export const migrationToAttachParams = async ({
freeTrial: newProduct.free_trial || null,
replaceables: [],
req,
req: ctx,
org,
entities: customer.entities,
features: req.features,
features: ctx.features,
internalEntityId,
cusProducts: customer.customer_products,

View File

@@ -11,7 +11,7 @@ import { checkSameCustom } from "@/internal/customers/attach/attachUtils/getAtta
import { intervalsAreSame } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv";
const getAttachFunction = async ({
attachParams,
@@ -32,15 +32,15 @@ const getAttachFunction = async ({
};
export const runMigrationAttach = async ({
req,
ctx,
attachParams,
fromProduct,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachParams: AttachParams;
fromProduct: FullProduct;
}) => {
const { logger } = req;
const { logger } = ctx;
const sameIntervals = intervalsAreSame({ attachParams });
const branch = AttachBranch.NewVersion;
@@ -88,13 +88,13 @@ export const runMigrationAttach = async ({
if (attachFunction === AttachFunction.AddProduct) {
return await handleAddProduct({
req,
ctx,
attachParams,
config,
});
} else if (attachFunction === AttachFunction.UpgradeSameInterval) {
await handleUpgradeFlow({
req,
ctx,
attachParams,
config,
branch:

View File

@@ -103,15 +103,15 @@ const validateProductItem = ({
}
}
if (isFeatureItem(item)) {
if (item.included_usage === 0 && feature?.type !== FeatureType.Boolean) {
throw new RecaseError({
message: `Included usage for feature ${item.feature_id} must be greater than 0`,
code: ErrCode.InvalidInputs,
statusCode: StatusCodes.BAD_REQUEST,
});
}
}
// if (isFeatureItem(item)) {
// if (item.included_usage === 0 && feature?.type !== FeatureType.Boolean) {
// throw new RecaseError({
// message: `Included usage for feature ${item.feature_id} must be greater than 0`,
// code: ErrCode.InvalidInputs,
// statusCode: StatusCodes.BAD_REQUEST,
// });
// }
// }
// 5. If it's a price, can't have day, minute or hour interval
if (isFeaturePriceItem(item) || isPriceItem(item)) {

View File

@@ -21,6 +21,7 @@ import { CusService } from "@/internal/customers/CusService.js";
import { isStripeConnected } from "@/internal/orgs/orgUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { RewardRedemptionService } from "../RewardRedemptionService.js";
import { ReferralResponseCodes } from "../referralUtils.js";
@@ -100,7 +101,7 @@ export const triggerFreePaidProduct = async ({
const fullCus = [fullReferrer, fullRedeemer][i];
const attachParams = rewardProgramToAttachParams({
req,
ctx: req as unknown as AutumnContext,
rewardProgram,
customer: fullCus,
product: fullProduct,
@@ -144,7 +145,7 @@ export const triggerFreePaidProduct = async ({
});
await handleAddProduct({
req,
ctx: req as unknown as AutumnContext,
attachParams,
branch: AttachBranch.New,
config: {

View File

@@ -18,6 +18,7 @@ import { ProductService } from "@/internal/products/ProductService.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { deleteCachedApiCustomer } from "../../customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
import { RewardRedemptionService } from "../RewardRedemptionService.js";
import { ReferralResponseCodes } from "../referralUtils.js";
@@ -117,7 +118,7 @@ export const triggerFreeProduct = async ({
}
const attachParams: InsertCusProductParams = {
req,
req: req as unknown as AutumnContext,
org,
product: fullProduct,
prices: fullProduct.prices,

View File

@@ -150,10 +150,27 @@ export const attachPaymentMethod = async ({
}: {
stripeCli: Stripe;
stripeCusId: string;
type: "success" | "fail";
type: "success" | "fail" | "authenticate";
}) => {
try {
const token = type === "fail" ? "tok_chargeCustomerFail" : "tok_visa";
if (type === "authenticate") {
await stripeCli.paymentMethods.attach("pm_card_authenticationRequired", {
customer: stripeCusId,
});
const pms = await stripeCli.paymentMethods.list({
customer: stripeCusId,
});
await stripeCli.customers.update(stripeCusId, {
invoice_settings: {
default_payment_method: pms.data[0].id,
},
});
return;
}
const pm = await stripeCli.paymentMethods.create({
type: "card",
card: {

View File

@@ -17,7 +17,7 @@ export const initCustomerV3 = async ({
}: {
ctx: TestContext;
customerId: string;
attachPm?: "success" | "fail";
attachPm?: "success" | "fail" | "authenticate";
customerData?: CustomerData;
withTestClock?: boolean;
withDefault?: boolean;

View File

@@ -1,22 +1,22 @@
import { beforeAll, describe, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { ApiVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import {
constructProduct,
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { constructPriceItem } from "../../src/internal/products/product-items/productItemUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { attachAuthenticatePaymentMethod } from "../../src/external/stripe/stripeCusUtils.js";
import { initCustomerV3 } from "../../src/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "../../src/utils/scriptUtils/testUtils/initProductsV0.js";
import { expectProductAttached } from "../utils/expectUtils/expectProductAttached.js";
import { expectSubItemsCorrect } from "../utils/expectUtils/expectSubUtils.js";
import { completeInvoiceConfirmation } from "../utils/stripeUtils/completeInvoiceConfirmation.js";
// UNCOMMENT FROM HERE
const pro = constructProduct({
type: "pro",
isDefault: true,
isDefault: false,
items: [
constructFeatureItem({
@@ -27,13 +27,9 @@ const pro = constructProduct({
],
});
const oneOff = constructRawProduct({
id: "one-off",
const premium = constructProduct({
type: "premium",
items: [
constructPriceItem({
price: 10,
interval: null,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
@@ -43,7 +39,7 @@ const oneOff = constructRawProduct({
describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
const customerId = "temp";
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
beforeAll(async () => {
await initCustomerV3({
@@ -56,40 +52,61 @@ describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
await initProductsV0({
ctx,
products: [pro, oneOff],
products: [pro, premium],
prefix: customerId,
});
});
test("should attach pro product", async () => {
// await autumn.customers.get(customerId);
const res = await autumn.attach({
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
await autumn.attach({
customer_id: customerId,
product_id: oneOff.id,
await attachAuthenticatePaymentMethod({
ctx,
customerId,
});
await autumn.attach({
const res = await autumn.attach({
customer_id: customerId,
product_id: oneOff.id,
product_id: premium.id,
});
const customer = await autumn.customers.get(customerId);
console.log("Customer:", customer);
expectProductAttached({
customer,
product: pro,
});
// await autumn.attach({
// customer_id: customerId,
// product_id: oneOff.id,
// });
// await autumn.attach({
// customer_id: customerId,
// product_id: oneOff.id,
// });
await expectSubItemsCorrect({
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
// const customer = await autumn.customers.get(customerId);
// console.log("Customer:", customer);
await completeInvoiceConfirmation({
url: res.checkout_url,
});
});
test("should have premium product attached", async () => {
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: premium,
});
await expectSubItemsCorrect({
customerId,
product: premium,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
});

View File

@@ -1,13 +1,13 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { getExpectedInvoiceTotal } from "@tests/utils/expectUtils/expectInvoiceUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,11 +1,11 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import chalk from "chalk";
import type { Stripe } from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { getMainCusProduct } from "@tests/utils/cusProductUtils/cusProductUtils.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
@@ -63,6 +63,7 @@ describe(`${chalk.yellowBright(
});
expect(res.invoice).toBeDefined();
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,

View File

@@ -1,8 +1,8 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import chalk from "chalk";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -63,7 +63,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing v0.2 / v1.2 response for at
product_id: premium.id,
});
// expect(attachResponse.checkout_url).toBeDefined();
expect(Object.keys(attachResponse)).toEqual(["success", "message"]);
});

View File

@@ -63,7 +63,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing v0.2 / v1.2 response for at
product_id: pro.id,
});
console.log(attachResponse);
expect(Object.keys(attachResponse)).toEqual(["success", "message"]);
});

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