chore: added hover texts, fixed some ui bugs
This commit is contained in:
1
bun.lock
1
bun.lock
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "autumn",
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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(
|
||||
|
||||
65
server/src/cron/invoiceCron/runInvoiceCron.ts
Normal file
65
server/src/cron/invoiceCron/runInvoiceCron.ts
Normal 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");
|
||||
};
|
||||
7
server/src/cron/utils/CronContext.ts
Normal file
7
server/src/cron/utils/CronContext.ts
Normal 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;
|
||||
}
|
||||
6
server/src/external/logtail/logtailUtils.ts
vendored
6
server/src/external/logtail/logtailUtils.ts
vendored
@@ -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();
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
33
server/src/external/stripe/stripeCusUtils.ts
vendored
33
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
stripeCli,
|
||||
paymentMethod: paymentMethod || null,
|
||||
invoiceId: invoice.id!,
|
||||
logger,
|
||||
voidIfFailed: true,
|
||||
});
|
||||
const {
|
||||
paid,
|
||||
error,
|
||||
invoice: subInvoice,
|
||||
} = await payForInvoice({
|
||||
stripeCli,
|
||||
paymentMethod: paymentMethod || null,
|
||||
invoiceId: invoice.id!,
|
||||
logger,
|
||||
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}`,
|
||||
statusCode: 500,
|
||||
data: `Stripe error: ${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: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
invoice: subInvoice,
|
||||
url: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
56
server/src/external/stripe/webhookHandlers/handleInvoicePaid/handleInvoiceCheckoutPaid.ts
vendored
Normal file
56
server/src/external/stripe/webhookHandlers/handleInvoicePaid/handleInvoiceCheckoutPaid.ts
vendored
Normal 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,
|
||||
});
|
||||
};
|
||||
45
server/src/external/stripe/webhookHandlers/handleInvoicePaid/handleInvoicePaidMetadata.ts
vendored
Normal file
45
server/src/external/stripe/webhookHandlers/handleInvoicePaid/handleInvoicePaidMetadata.ts
vendored
Normal 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,
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,21 +125,23 @@ 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) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
logger.warn(
|
||||
`schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`,
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`,
|
||||
);
|
||||
} 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}`,
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
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,
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
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,
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
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 };
|
||||
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,
|
||||
invoice: config.finalizeInvoice ? undefined : invoice, // if finalizeInvoice, checkout_url is used
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
return AttachFunctionResponseSchema.parse({
|
||||
message: `Successfully attached ${products.map((p) => p.name).join(", ")} to ${customer.name}`,
|
||||
code: SuccessCode.FreeProductAttached,
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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 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}`,
|
||||
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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const customerName = getCustomerDisplay({ customer });
|
||||
const productNames = products.map((p) => p.name).join(", ");
|
||||
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,
|
||||
});
|
||||
// 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,
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
const productNames = products.map((p) => p.name).join(", ");
|
||||
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 ? 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,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
code: SuccessCode.RenewedProduct,
|
||||
message: `Successfully renewed product ${product.name}`,
|
||||
product_ids: [product.id],
|
||||
customer_id:
|
||||
attachParams.customer.id || attachParams.customer.internal_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return AttachFunctionResponseSchema.parse({
|
||||
code: SuccessCode.RenewedProduct,
|
||||
message: `Successfully renewed product ${product.name}`,
|
||||
});
|
||||
// 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,
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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({
|
||||
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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
return AttachFunctionResponseSchema.parse({
|
||||
message: `Successfully created subscriptions and attached ${attachParams.products.map((p) => p.name).join(", ")} to ${attachParams.customer.name}`,
|
||||
code: SuccessCode.NewProductAttached,
|
||||
invoice: attachParams.invoiceOnly
|
||||
? 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,
|
||||
// }),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return AttachFunctionResponseSchema.parse({
|
||||
code: SuccessCode.DowngradeScheduled,
|
||||
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}`,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
code: SuccessCode.FeaturesUpdated,
|
||||
message: `Successfully updated quantity for features: ${optionsToUpdate.map((o) => o.new.feature_id).join(", ")}`,
|
||||
}),
|
||||
);
|
||||
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(", ")}`,
|
||||
// }),
|
||||
// );
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return AttachFunctionResponseSchema.parse({
|
||||
code: SuccessCode.UpgradedToNewProduct,
|
||||
message: `Successfully updated product`,
|
||||
invoice: attachParams.invoiceOnly
|
||||
? attachToInvoiceResponse({ invoice: latestInvoice || undefined })
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// 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`,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
await resetUsageBalances({
|
||||
db,
|
||||
cusEntIds,
|
||||
cusProduct: curMainProduct!,
|
||||
});
|
||||
// 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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,7 +64,6 @@ export const getApiCustomerBase = async ({
|
||||
fullCus.invoices && ctx.expand.includes(CusExpand.Invoices)
|
||||
? invoicesToResponse({
|
||||
invoices: fullCus.invoices,
|
||||
logger: ctx.logger,
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const getApiEntityExpand = async ({
|
||||
return {
|
||||
invoices: invoicesToResponse({
|
||||
invoices,
|
||||
logger,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
0
server/tests/attach/misc/attach-misc2.test.ts
Normal file
0
server/tests/attach/misc/attach-misc2.test.ts
Normal 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"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user