midway adding stripe connect, refactored a bunch

This commit is contained in:
John Yeo
2025-10-17 13:38:35 +01:00
parent c9f5dd242b
commit 411f6a6bc4
148 changed files with 2455 additions and 1833 deletions

906
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -94,7 +94,7 @@
"recaseai": "^0.0.37",
"resend": "^4.1.1",
"semver": "^7.7.2",
"stripe": "^18.4.0",
"stripe": "18.4.0",
"svix": "^1.45.1",
"tsc-alias": "^1.8.16",
"ws": "^8.18.0",

View File

@@ -15,8 +15,8 @@ import {
import type Stripe from "stripe";
import { initDrizzle } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createSupabaseClient } from "@/external/supabaseUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";

View File

@@ -12,7 +12,7 @@ import { UTCDate } from "@date-fns/utc";
import chalk from "chalk";
import { format, getDate, getMonth, setDate } from "date-fns";
import { Decimal } from "decimal.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";

View File

@@ -0,0 +1,23 @@
import { AppEnv, type Organization } from "@autumn/shared";
export const orgToAccountId = ({
org,
env,
noDefaultAccount = false,
}: {
org: Organization;
env: AppEnv;
noDefaultAccount?: boolean;
}): string | undefined => {
if (env === AppEnv.Sandbox) {
if (noDefaultAccount) {
return org.stripe_connect?.test_account_id;
}
return (
org.stripe_connect?.test_account_id ||
org.stripe_connect?.default_account_id
);
} else {
return org.stripe_connect?.live_account_id;
}
};

View File

@@ -0,0 +1,47 @@
import {
AppEnv,
ErrCode,
type Organization,
RecaseError,
} from "@autumn/shared";
import Stripe from "stripe";
import { decryptData } from "@/utils/encryptUtils.js";
import { orgToAccountId } from "./connectUtils.js";
import { initMasterStripe } from "./initMasterStripe.js";
export const createStripeCli = ({
org,
env,
// apiVersion,
legacyVersion,
}: {
org: Organization;
env: AppEnv;
// apiVersion?: string;
legacyVersion?: boolean;
}) => {
// Look at test account flow first
const accountId = orgToAccountId({ org, env });
if (accountId) return initMasterStripe({ accountId, legacyVersion });
const encrypted =
env === AppEnv.Sandbox
? org.stripe_config?.test_api_key
: org.stripe_config?.live_api_key;
if (!encrypted) {
throw new RecaseError({
message: `Please connect your Stripe ${env === AppEnv.Sandbox ? "test" : "live"} secret key. You can find it here: https://dashboard.stripe.com${env === AppEnv.Sandbox ? "/test" : ""}/apikeys`,
code: ErrCode.StripeConfigNotFound,
statusCode: 400,
});
}
const decrypted = decryptData(encrypted);
return new Stripe(decrypted, {
apiVersion: legacyVersion
? ("2025-02-24.acacia" as any)
: "2025-07-30.basil",
});
};

View File

@@ -0,0 +1,25 @@
import { InternalError } from "@autumn/shared";
import "dotenv/config";
import Stripe from "stripe";
export const initMasterStripe = (params?: {
accountId?: string;
legacyVersion?: boolean;
}) => {
if (!process.env.STRIPE_SECRET_KEY) {
throw new InternalError({
message: "STRIPE_SECRET_KEY env variable is not set",
});
}
if (!params) {
return new Stripe(process.env.STRIPE_SECRET_KEY || "");
}
return new Stripe(process.env.STRIPE_SECRET_KEY || "", {
stripeAccount: params?.accountId,
apiVersion: params?.legacyVersion
? ("2025-02-24.acacia" as any)
: "2025-07-30.basil",
});
};

View File

@@ -103,20 +103,5 @@ export const createLogger = () => {
return createLoggerStructure(pinoLogger);
};
// export const createLogtailAll = () => {
// if (
// !process.env.LOGTAIL_ALL_SOURCE_TOKEN ||
// !process.env.LOGTAIL_ALL_INGESTING_HOST
// ) {
// return null;
// }
// const logtail = new Logtail(process.env.LOGTAIL_ALL_SOURCE_TOKEN!, {
// endpoint: process.env.LOGTAIL_ALL_INGESTING_HOST!,
// });
// return logtail;
// };
export const logger = createLogger();
export type Logger = ReturnType<typeof createLogger>;

View File

@@ -1,16 +0,0 @@
import dotenv from "dotenv";
dotenv.config();
import { PostHog } from "posthog-node";
import { logger } from "../logtail/logtailUtils.js";
export const createPosthogCli = () => {
if (!process.env.POSTHOG_API_KEY) {
logger.warn("POSTHOG_API_KEY not set, skipping posthog");
return null;
}
return new PostHog(process.env.POSTHOG_API_KEY, {
host: process.env.POSTHOG_HOST_URL ?? "https://us.i.posthog.com",
});
};

View File

@@ -1,20 +0,0 @@
import { EventMessage, PostHog } from "posthog-node";
export const posthogCapture = ({
posthog,
params,
}: {
posthog?: PostHog;
params: EventMessage;
}) => {
try {
if (process.env.NODE_ENV === "development" || !posthog) {
return;
}
posthog.capture(params);
} catch (error) {
console.error("Failed to capture posthog event", params);
console.error(error);
}
};

View File

@@ -29,9 +29,9 @@ export const handleAttachRaceCondition = async ({
const originalJson = res.json;
res.json = async function (body: any) {
try {
await clearLock({ lockKey, logger: req.logtail });
await clearLock({ lockKey, logger: req.logger });
} catch (error) {
req.logtail.warn("❗️❗️ Error clearing lock", {
req.logger.warn("❗️❗️ Error clearing lock", {
error,
});
}
@@ -44,7 +44,7 @@ export const handleAttachRaceCondition = async ({
throw error;
}
req.logtail.warn("❗️❗️ Error acquiring lock", {
req.logger.warn("❗️❗️ Error acquiring lock", {
error,
});
return null;

View File

@@ -1,6 +1,6 @@
import type { User } from "better-auth";
import { LoopsClient } from "loops";
import { logger } from "../logtail/logtailUtils.js";
import { User } from "better-auth";
const createLoopsCli = () => {
return new LoopsClient(process.env.LOOPS_API_KEY || "");
@@ -10,9 +10,9 @@ export const createLoopsContact = async (user: User) => {
if (!process.env.LOOPS_API_KEY) return;
try {
let email = user.email;
let firstName = user.name?.split(" ")[0] || "";
let lastName = user.name?.split(" ")[1] || "";
const email = user.email;
const firstName = user.name?.split(" ")[0] || "";
const lastName = user.name?.split(" ")[1] || "";
const loops = createLoopsCli();
const resp = await loops.createContact(email, {

View File

@@ -12,9 +12,9 @@ import {
RewardType,
type UsagePriceConfig,
} from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { createStripeCli } from "../utils.js";
const couponToStripeDuration = ({
coupon,

View File

@@ -8,9 +8,9 @@ import {
import { StatusCodes } from "http-status-codes";
import type { Stripe } from "stripe";
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 { createStripeCli } from "./utils.js";
export const getStripeCus = async ({
stripeCli,

View File

@@ -1,11 +1,10 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { Stripe } from "stripe";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AppEnv, Organization, products } from "@autumn/shared";
import type { AppEnv, Organization } from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { initProductInStripe } from "@/internal/products/productUtils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { createStripeCli } from "./utils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
export async function ensureStripeProducts({
db,
@@ -55,9 +54,9 @@ export async function ensureStripeProductsWithEnv({
const updatedOrg = await OrgService.get({ db, orgId: req.org.id });
const batchInit: Promise<void>[] = [];
for (let fullProduct of fullProducts) {
for (const fullProduct of fullProducts) {
const initProduct = async () => {
let existsInStripe = products.data.find(
const existsInStripe = products.data.find(
(p) => p.id === fullProduct.processor?.id,
);

View File

@@ -5,8 +5,8 @@ import {
type Product,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import RecaseError from "@/utils/errorUtils.js";
import { createStripeCli } from "./utils.js";
export const createStripeProduct = async (
org: Organization,

View File

@@ -1,32 +1,29 @@
import express, { Router } from "express";
import stripe, { Stripe } from "stripe";
import { type AppEnv, AuthType, type Organization } from "@autumn/shared";
import chalk from "chalk";
import express, { type Router } from "express";
import stripe, { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { AppEnv, AuthType, Organization } from "@autumn/shared";
import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js";
import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js";
import { handleSubDeleted } from "./webhookHandlers/handleSubDeleted.js";
import { handleSubCreated } from "./webhookHandlers/handleSubCreated.js";
import {
getStripeWebhookSecret,
isStripeConnected,
unsetOrgStripeKeys,
} from "@/internal/orgs/orgUtils.js";
import { handleInvoicePaid } from "./webhookHandlers/handleInvoicePaid.js";
import { handleRequestError } from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js";
import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js";
import { handleInvoiceCreated } from "./webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.js";
import { handleInvoiceFinalized } from "./webhookHandlers/handleInvoiceFinalized.js";
import { handleSubscriptionScheduleCanceled } from "./webhookHandlers/handleSubScheduleCanceled.js";
import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { createStripeCli } from "./utils.js";
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
import { CusService } from "@/internal/customers/CusService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { handleInvoicePaid } from "./webhookHandlers/handleInvoicePaid.js";
import { handleInvoiceUpdated } from "./webhookHandlers/handleInvoiceUpdated.js";
import { disconnectStripe } from "@/internal/orgs/handlers/handleDeleteStripe.js";
import { handleSubCreated } from "./webhookHandlers/handleSubCreated.js";
import { handleSubDeleted } from "./webhookHandlers/handleSubDeleted.js";
import { handleSubscriptionScheduleCanceled } from "./webhookHandlers/handleSubScheduleCanceled.js";
import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js";
export const stripeWebhookRouter: Router = express.Router();
@@ -37,7 +34,7 @@ const logStripeWebhook = ({
req: ExtendedRequest;
event: Stripe.Event;
}) => {
req.logtail.info(
req.logger.info(
`${chalk.yellow("STRIPE").padEnd(18)} ${event.type.padEnd(30)} ${req.org.slug} | ${event.id}`,
);
};
@@ -47,7 +44,7 @@ stripeWebhookRouter.post(
express.raw({ type: "application/json" }),
async (request: any, response: any) => {
const sig = request.headers["stripe-signature"];
let event;
let event: Stripe.Event;
const { orgId, env } = request.params;
const { db } = request;
@@ -103,13 +100,13 @@ stripeWebhookRouter.post(
// event = request.body;
request.logtail = request.logtail.child({
request.logger = request.logger.child({
context: {
context: {
// body: request.body,
event_type: event.type,
event_id: event.id,
// @ts-ignore
// @ts-expect-error
object_id: `${event.data?.object?.id}` || "N/A",
authType: AuthType.Stripe,
org_id: orgId,
@@ -119,7 +116,7 @@ stripeWebhookRouter.post(
},
});
let logger = request.logtail;
const logger = request.logger;
logStripeWebhook({ req: request, event });
try {
@@ -135,7 +132,7 @@ stripeWebhookRouter.post(
});
break;
case "customer.subscription.updated":
case "customer.subscription.updated": {
const subscription = event.data.object;
await handleSubscriptionUpdated({
req: request,
@@ -147,6 +144,7 @@ stripeWebhookRouter.post(
logger,
});
break;
}
case "customer.subscription.deleted":
await handleSubDeleted({
@@ -157,7 +155,7 @@ stripeWebhookRouter.post(
});
break;
case "checkout.session.completed":
case "checkout.session.completed": {
const checkoutSession = event.data.object;
await handleCheckoutSessionCompleted({
req: request,
@@ -168,9 +166,10 @@ stripeWebhookRouter.post(
logger,
});
break;
}
// Triggered when payment through Stripe is successful
case "invoice.paid":
case "invoice.paid": {
const invoice = event.data.object;
await handleInvoicePaid({
db,
@@ -181,6 +180,7 @@ stripeWebhookRouter.post(
req: request,
});
break;
}
case "invoice.updated":
await handleInvoiceUpdated({
@@ -191,7 +191,7 @@ stripeWebhookRouter.post(
});
break;
case "invoice.created":
case "invoice.created": {
const createdInvoice = event.data.object;
await handleInvoiceCreated({
db,
@@ -201,8 +201,9 @@ stripeWebhookRouter.post(
logger,
});
break;
}
case "invoice.finalized":
case "invoice.finalized": {
const finalizedInvoice = event.data.object;
await handleInvoiceFinalized({
db,
@@ -212,8 +213,9 @@ stripeWebhookRouter.post(
logger,
});
break;
}
case "subscription_schedule.canceled":
case "subscription_schedule.canceled": {
const canceledSchedule = event.data.object;
await handleSubscriptionScheduleCanceled({
db,
@@ -223,6 +225,7 @@ stripeWebhookRouter.post(
logger,
});
break;
}
case "customer.discount.deleted":
await handleCusDiscountDeleted({
@@ -310,7 +313,7 @@ export const handleStripeWebhookRefresh = async ({
logger: any;
}) => {
if (coreEvents.includes(eventType)) {
let stripeCusId = data.object.customer;
const stripeCusId = data.object.customer;
if (!stripeCusId) {
logger.warn(
`stripe webhook cache refresh, object doesn't contain customer id`,
@@ -324,7 +327,7 @@ export const handleStripeWebhookRefresh = async ({
return;
}
let cus = await CusService.getByStripeId({
const cus = await CusService.getByStripeId({
db,
stripeId: stripeCusId,
});

View File

@@ -1,47 +1,10 @@
import {
AppEnv,
BillingInterval,
ErrCode,
type Feature,
Infinite,
type Organization,
type UsagePriceConfig,
} from "@autumn/shared";
import Stripe from "stripe";
import { decryptData } from "@/utils/encryptUtils.js";
import RecaseError from "@/utils/errorUtils.js";
export const createStripeCli = ({
org,
env,
// apiVersion,
legacyVersion,
}: {
org: Organization;
env: AppEnv;
// apiVersion?: string;
legacyVersion?: boolean;
}) => {
const encrypted =
env === AppEnv.Sandbox
? org.stripe_config?.test_api_key
: org.stripe_config?.live_api_key;
if (!encrypted) {
throw new RecaseError({
message: `Please connect your Stripe ${env === AppEnv.Sandbox ? "test" : "live"} secret key. You can find it here: https://dashboard.stripe.com${env === AppEnv.Sandbox ? "/test" : ""}/apikeys`,
code: ErrCode.StripeConfigNotFound,
statusCode: 400,
});
}
const decrypted = decryptData(encrypted);
return new Stripe(decrypted, {
apiVersion: legacyVersion
? ("2025-02-24.acacia" as any)
: "2025-07-30.basil",
});
};
import type Stripe from "stripe";
export const calculateMetered1Price = ({
product,

View File

@@ -7,6 +7,7 @@ import {
} from "@autumn/shared";
import type { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.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";
@@ -16,10 +17,8 @@ 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 { createStripeCli } from "../utils.js";
import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js";
import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js";
import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSets.js";

View File

@@ -1,5 +1,6 @@
import { AttachBranch } from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js";
import { handleOneOffFunction } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
import { getDefaultAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
@@ -7,7 +8,6 @@ import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams
import { isOneOff } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { getCusPaymentMethod } from "../../stripeCusUtils.js";
import { createStripeCli } from "../../utils.js";
export const handleSetupCheckout = async ({
req,

View File

@@ -1,10 +1,10 @@
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
import { notNullish } from "@/utils/genUtils.js";
import { createStripeCli } from "../utils.js";
export async function handleCusDiscountDeleted({
db,

View File

@@ -10,6 +10,7 @@ import {
} from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { EntityService } from "@/internal/api/entities/EntityService.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
@@ -25,7 +26,6 @@ import {
} from "../../stripeInvoiceUtils.js";
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
import { getStripeSubs } from "../../stripeSubUtils.js";
import { createStripeCli } from "../../utils.js";
import { handleContUsePrices } from "./handleContUsePrices.js";
import { handlePrepaidPrices } from "./handlePrepaidPrices.js";
import { handleUsagePrices } from "./handleUsagePrices.js";

View File

@@ -1,23 +1,23 @@
import {
AppEnv,
type AppEnv,
CusProductStatus,
FullCustomerPrice,
InvoiceStatus,
Organization,
type FullCustomerPrice,
type InvoiceStatus,
type Organization,
} from "@autumn/shared";
import Stripe from "stripe";
import { createStripeCli } from "../utils.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import {
getFullStripeInvoice,
getStripeExpandedInvoice,
invoiceToSubId,
updateInvoiceIfExists,
} from "../stripeInvoiceUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
export const handleInvoiceFinalized = async ({
db,
@@ -68,11 +68,11 @@ export const handleInvoiceFinalized = async ({
return;
}
let prices = activeProducts.flatMap((cp) =>
const prices = activeProducts.flatMap((cp) =>
cp.customer_prices.map((cpr: FullCustomerPrice) => cpr.price),
);
let invoiceItems = await getInvoiceItems({
const invoiceItems = await getInvoiceItems({
stripeInvoice: invoice,
prices: prices,
logger,

View File

@@ -7,6 +7,7 @@ import type {
} from "@autumn/shared";
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";
@@ -22,7 +23,6 @@ import {
} from "../stripeInvoiceUtils.js";
import { lineItemInCusProduct } from "../stripeSubUtils/stripeSubItemUtils.js";
import { getStripeSubs } from "../stripeSubUtils.js";
import { createStripeCli } from "../utils.js";
import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js";
const handleOneOffInvoicePaid = async ({
@@ -153,7 +153,7 @@ export const handleInvoicePaid = async ({
env: AppEnv;
event: Stripe.Event;
}) => {
const logger = req.logtail;
const logger = req.logger;
const stripeCli = createStripeCli({ org, env });
const invoice = await getFullStripeInvoice({
stripeCli,
@@ -238,7 +238,7 @@ export const handleInvoicePaid = async ({
cusProducts.map((cp) => `${cp.product.name} - ${cp.product.id}`),
);
if (cusProducts.length == 0) {
if (cusProducts.length === 0) {
cusProducts = activeCusProducts;
}
} catch (error) {

View File

@@ -10,6 +10,7 @@ import { Decimal } from "decimal.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
import { generateId } from "@/utils/genUtils.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
@@ -18,7 +19,6 @@ import {
deleteCouponFromSub,
} from "../stripeCouponUtils/deleteCouponFromCus.js";
import { invoiceToSubId } from "../stripeInvoiceUtils.js";
import { createStripeCli } from "../utils.js";
export const handleInvoicePaidDiscount = async ({
db,

View File

@@ -1,27 +1,26 @@
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
type AppEnv,
BillingType,
CusProductStatus,
FullCusProduct,
FullCustomerPrice,
Organization,
Price,
type FullCusProduct,
type FullCustomerPrice,
type Organization,
type Price,
} from "@autumn/shared";
import { AppEnv } from "@autumn/shared";
import Stripe from "stripe";
import { createStripeCli } from "../utils.js";
import { getStripeExpandedInvoice } from "../stripeInvoiceUtils.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { generateId } from "@/utils/genUtils.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getFullStripeSub } from "../stripeSubUtils.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { generateId } from "@/utils/genUtils.js";
import { getStripeExpandedInvoice } from "../stripeInvoiceUtils.js";
import {
getEarliestPeriodEnd,
getEarliestPeriodStart,
} from "../stripeSubUtils/convertSubUtils.js";
import { getFullStripeSub } from "../stripeSubUtils.js";
export const handleSubCreated = async ({
db,
@@ -56,7 +55,7 @@ export const handleSubCreated = async ({
}
// Update autumn sub
let autumnSub = await SubService.getFromScheduleId({
const autumnSub = await SubService.getFromScheduleId({
db,
scheduleId: subscription.schedule as string,
});
@@ -105,9 +104,9 @@ export const handleSubCreated = async ({
cusProds.length,
);
let batchUpdate = [];
const batchUpdate = [];
for (const cusProd of cusProds) {
let subIds = cusProd.subscription_ids
const subIds = cusProd.subscription_ids
? [...cusProd.subscription_ids]
: [];
subIds.push(subscription.id);
@@ -128,7 +127,7 @@ export const handleSubCreated = async ({
stripeInvoiceId: subscription.latest_invoice as string,
});
let invoiceItems = await getInvoiceItems({
const invoiceItems = await getInvoiceItems({
stripeInvoice: invoice,
prices: cusProd.customer_prices.map(
(cpr: FullCustomerPrice) => cpr.price,
@@ -155,19 +154,19 @@ export const handleSubCreated = async ({
}
// Get cus prods for sub
let cusProds = await CusProductService.getByStripeSubId({
const cusProds = await CusProductService.getByStripeSubId({
db,
stripeSubId: subscription.id,
orgId: org.id,
env,
});
let handleInArrearWithEntity = async (cusProd: FullCusProduct) => {
const handleInArrearWithEntity = async (cusProd: FullCusProduct) => {
if (!cusProd.internal_entity_id) {
return;
}
let arrearPrices = cusProd.customer_prices
const arrearPrices = cusProd.customer_prices
.map((cp) => cp.price)
.filter(
(p: Price) =>
@@ -178,9 +177,9 @@ export const handleSubCreated = async ({
return;
}
let itemsToDelete = [];
const itemsToDelete = [];
for (const arrearPrice of arrearPrices) {
let subItem = subscription.items.data.find(
const subItem = subscription.items.data.find(
(i) => i.price.id == arrearPrice.config?.stripe_price_id,
);
@@ -211,7 +210,7 @@ export const handleSubCreated = async ({
}
};
let batchUpdate = [];
const batchUpdate = [];
for (const cusProd of cusProds) {
batchUpdate.push(handleInArrearWithEntity(cusProd));
}

View File

@@ -1,9 +1,7 @@
import { AppEnv } from "@autumn/shared";
import Stripe from "stripe";
import { Organization } from "@autumn/shared";
import { createStripeCli } from "../utils.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import type { AppEnv, Organization } from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
export const handleSubscriptionScheduleCanceled = async ({

View File

@@ -5,10 +5,10 @@ import {
type Organization,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
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 { createStripeCli } from "../utils.js";
import { handleSchedulePhaseCompleted } from "./handleSubUpdated/handleSchedulePhaseCompleted.js";
import {
handleSubCanceled,

View File

@@ -4,6 +4,7 @@ import {
cusProductToProduct,
} from "@autumn/shared";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { activateFutureProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
@@ -11,7 +12,6 @@ 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 { createStripeCli } from "../../utils.js";
export const handleSchedulePhaseCompleted = async ({
req,

View File

@@ -102,7 +102,7 @@ export const handleSubCanceled = async ({
const canceledFromPortal = canceled && !isAutumnDowngrade;
const { db, env, logtail: logger } = req;
const { db, env, logger } = req;
if (!canceledFromPortal || updatedCusProducts.length === 0) return;

View File

@@ -1,13 +1,14 @@
import { AttachScenario, type FullCusProduct } from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js";
import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheUtils.js";
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 { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachScenario, FullCusProduct } from "@autumn/shared";
import Stripe from "stripe";
import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
const isSubRenewed = ({
previousAttributes,
sub,
@@ -62,7 +63,7 @@ export const handleSubRenewed = async ({
sub: Stripe.Subscription;
updatedCusProducts: FullCusProduct[];
}) => {
const { db, org, env, logtail: logger } = req;
const { db, org, env, logger } = req;
const { renewed } = isSubRenewed({
previousAttributes: prevAttributes,
@@ -76,7 +77,7 @@ export const handleSubRenewed = async ({
if (subScenario === AttachScenario.Renew) return;
const customer = updatedCusProducts[0].customer;
let cusProducts = await CusProductService.list({
const cusProducts = await CusProductService.list({
db,
internalCustomerId: customer!.internal_id,
});
@@ -93,13 +94,13 @@ export const handleSubRenewed = async ({
if (!org.config.sync_status) return;
let { curScheduledProduct } = getExistingCusProducts({
const { curScheduledProduct } = getExistingCusProducts({
product: updatedCusProducts[0].product,
cusProducts,
internalEntityId: updatedCusProducts[0].internal_entity_id,
});
let deletedCusProducts: FullCusProduct[] = [];
const deletedCusProducts: FullCusProduct[] = [];
if (curScheduledProduct) {
logger.info(
@@ -115,7 +116,7 @@ export const handleSubRenewed = async ({
}
try {
for (let cusProd of updatedCusProducts) {
for (const cusProd of updatedCusProducts) {
await addProductsUpdatedWebhookTask({
req,
internalCustomerId: cusProd.internal_customer_id,

View File

@@ -1,5 +1,4 @@
import { AppEnv } from "@autumn/shared";
import { Organization } from "@autumn/shared";
import { AppEnv, type Organization } from "@autumn/shared";
import { Svix } from "svix";
import { logger } from "../logtail/logtailUtils.js";
@@ -35,7 +34,7 @@ export const getSvixAppId = ({
env: AppEnv;
}) => {
const svixConfig = org.svix_config;
return env == AppEnv.Live
return env === AppEnv.Live
? svixConfig?.live_app_id
: svixConfig?.sandbox_app_id;
};

View File

@@ -0,0 +1,315 @@
import { type AppEnv, AuthType, type Organization } from "@autumn/shared";
import chalk from "chalk";
import express, { type Router } from "express";
import type { Context } from "hono";
import { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { initMasterStripe } from "@/external/connect/initMasterStripe.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { CusService } from "@/internal/customers/CusService.js";
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { unsetOrgStripeKeys } from "@/internal/orgs/orgUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { Logger } from "../logtail/logtailUtils.js";
import { handleCheckoutSessionCompleted } from "../stripe/webhookHandlers/handleCheckoutCompleted.js";
import { handleCusDiscountDeleted } from "../stripe/webhookHandlers/handleCusDiscountDeleted.js";
import { handleInvoiceCreated } from "../stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.js";
import { handleInvoiceFinalized } from "../stripe/webhookHandlers/handleInvoiceFinalized.js";
import { handleInvoicePaid } from "../stripe/webhookHandlers/handleInvoicePaid.js";
import { handleInvoiceUpdated } from "../stripe/webhookHandlers/handleInvoiceUpdated.js";
import { handleSubCreated } from "../stripe/webhookHandlers/handleSubCreated.js";
import { handleSubDeleted } from "../stripe/webhookHandlers/handleSubDeleted.js";
import { handleSubscriptionScheduleCanceled } from "../stripe/webhookHandlers/handleSubScheduleCanceled.js";
import { handleSubscriptionUpdated } from "../stripe/webhookHandlers/handleSubUpdated.js";
export const connectWebhookRouter: Router = express.Router();
const logStripeWebhook = ({
logger,
org,
event,
}: {
logger: Logger;
org: Organization;
event: Stripe.Event;
}) => {
logger.info(
`${chalk.yellow("STRIPE").padEnd(18)} ${event.type.padEnd(30)} ${org.slug} | ${event.id}`,
);
};
export const handleConnectWebhook = async (c: Context<HonoEnv>) => {
const ctx = c.get("ctx");
const { db, logger } = ctx;
const masterStripe = initMasterStripe();
let event: Stripe.Event;
try {
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || "";
const rawBody = await c.req.text();
const signature = c.req.header("stripe-signature") || "";
event = await masterStripe.webhooks.constructEventAsync(
rawBody,
signature,
webhookSecret,
);
} catch (err: any) {
logger.error(`Webhook verification error: ${err.message}`, { error: err });
return c.json({ error: err.message }, 400);
}
const accountId = event.account;
if (!accountId) {
return c.json({ error: "Account ID not found" }, 400);
}
const { org, features, env } = await OrgService.getByAccountId({
db,
accountId,
});
ctx.org = org;
ctx.features = features;
ctx.env = env;
ctx.logger = ctx.logger.child({
context: {
context: {
event_type: event.type,
event_id: event.id,
// @ts-expect-error
object_id: `${event.data?.object?.id}` || "N/A",
authType: AuthType.Stripe,
org_id: org.id,
org_slug: org.slug,
env,
},
},
});
logStripeWebhook({ logger, org, event });
try {
const stripeCli = createStripeCli({ org, env });
switch (event.type) {
case "customer.subscription.created":
await handleSubCreated({
db,
org,
subData: event.data.object,
env,
logger,
});
break;
case "customer.subscription.updated": {
const subscription = event.data.object;
await handleSubscriptionUpdated({
req: ctx as ExtendedRequest,
db,
org,
subscription,
previousAttributes: event.data.previous_attributes,
env,
logger,
});
break;
}
case "customer.subscription.deleted":
await handleSubDeleted({
req: ctx as ExtendedRequest,
stripeCli,
data: event.data.object,
logger,
});
break;
case "checkout.session.completed": {
const checkoutSession = event.data.object;
await handleCheckoutSessionCompleted({
req: ctx as ExtendedRequest,
db,
data: checkoutSession,
org,
env,
logger,
});
break;
}
// Triggered when payment through Stripe is successful
case "invoice.paid": {
const invoice = event.data.object;
await handleInvoicePaid({
db,
org,
invoiceData: invoice,
env,
event,
req: ctx as ExtendedRequest,
});
break;
}
case "invoice.updated":
await handleInvoiceUpdated({
stripeCli,
env,
event,
req: ctx as ExtendedRequest,
});
break;
case "invoice.created": {
const createdInvoice = event.data.object;
await handleInvoiceCreated({
db,
org,
data: createdInvoice,
env,
logger,
});
break;
}
case "invoice.finalized": {
const finalizedInvoice = event.data.object;
await handleInvoiceFinalized({
db,
org,
data: finalizedInvoice,
env,
logger,
});
break;
}
case "subscription_schedule.canceled": {
const canceledSchedule = event.data.object;
await handleSubscriptionScheduleCanceled({
db,
org,
env,
schedule: canceledSchedule,
logger,
});
break;
}
case "customer.discount.deleted":
await handleCusDiscountDeleted({
db,
org,
discount: event.data.object,
env,
logger,
res: ctx as ExtendedRequest,
});
break;
}
} catch (error) {
if (error instanceof Stripe.errors.StripeError) {
if (error.message.includes("No such customer")) {
logger.warn(`stripe customer missing: ${error.message}`);
return c.json({ message: "ok" }, 200);
}
if (error.message.includes("Expired API Key provided")) {
// Disconnect Stripe
await unsetOrgStripeKeys({
db,
org,
env,
});
return c.json({ message: "ok" }, 200);
}
}
logger.error(`Stripe webhook, error: ${error}`, { error });
return c.json({ message: "Internal server error" }, 500);
}
try {
await handleStripeWebhookRefresh({
eventType: event.type,
data: event.data,
db,
org,
env,
logger,
});
} catch (error) {
logger.error(`Stripe webhook, error refreshing cache!`, { error });
}
return c.json({ message: "Webhook received" }, 200);
};
const coreEvents = [
"customer.subscription.created",
"customer.subscription.updated",
"customer.subscription.deleted",
"invoice.paid",
"invoice.created",
"invoice.finalized",
"subscription_schedule.canceled",
"checkout.session.completed",
];
export const handleStripeWebhookRefresh = async ({
eventType,
data,
db,
org,
env,
logger,
}: {
eventType: string;
data: any;
db: DrizzleCli;
org: Organization;
env: AppEnv;
logger: any;
}) => {
if (coreEvents.includes(eventType)) {
const stripeCusId = data.object.customer;
if (!stripeCusId) {
logger.warn(
`stripe webhook cache refresh, object doesn't contain customer id`,
{
data: {
eventType,
object: data.object,
},
},
);
return;
}
const cus = await CusService.getByStripeId({
db,
stripeId: stripeCusId,
});
if (!cus) {
logger.warn(
`Searched for customer by stripe id, but not found: ${stripeCusId}`,
);
return;
}
// logger.info(`Deleting cache for customer ${cus.id}`);
await deleteCusCache({
db,
customerId: cus.id!,
org,
env,
});
}
};

View File

@@ -1,7 +1,6 @@
import express, { Router } from "express";
import { stripeWebhookRouter } from "../stripe/stripeWebhooks.js";
import express, { type Router } from "express";
import { autumnWebhookRouter } from "../autumn/autumnWebhookRouter.js";
import { stripeWebhookRouter } from "../stripe/stripeWebhooks.js";
const webhooksRouter: Router = express.Router();
@@ -9,4 +8,6 @@ webhooksRouter.use("/stripe", stripeWebhookRouter);
webhooksRouter.use("/autumn", autumnWebhookRouter);
// webhooksRouter.use("/connect", connectWebhookRouter);
export default webhooksRouter;

View File

@@ -29,7 +29,6 @@ import { client, db } from "./db/initDrizzle.js";
import { CacheManager } from "./external/caching/CacheManager.js";
import { ClickHouseManager } from "./external/clickhouse/ClickHouseManager.js";
import { logger } from "./external/logtail/logtailUtils.js";
import { createPosthogCli } from "./external/posthog/createPosthogCli.js";
import webhooksRouter from "./external/webhooks/webhooksRouter.js";
import { redirectToHono } from "./initHono.js";
import { apiRouter } from "./internal/api/apiRouter.js";
@@ -102,8 +101,6 @@ const init = async () => {
app.all("/api/auth/*", toNodeHandler(auth));
const posthog = createPosthogCli();
// Initialize managers in parallel for faster startup
await Promise.all([
QueueManager.getInstance(),
@@ -115,7 +112,6 @@ const init = async () => {
req.env = req.env = req.headers.app_env || AppEnv.Sandbox;
req.db = db;
req.clickhouseClient = await ClickHouseManager.getClient();
req.posthog = posthog;
req.id = req.headers["rndr-id"] || generateId("local_req");
req.timestamp = Date.now();
@@ -139,12 +135,11 @@ const init = async () => {
// Store span on request for potential use in other middleware/handlers
req.span = span;
req.logtail = logger.child({
req.logger = logger.child({
context: {
req: reqContext,
},
});
req.logger = req.logtail;
const endSpan = () => {
try {
@@ -177,7 +172,7 @@ const init = async () => {
app.use(express.json());
app.use(async (req: any, res: any, next: any) => {
req.logtail.info(`${req.method} ${req.originalUrl}`, {
req.logger.info(`${req.method} ${req.originalUrl}`, {
context: {
body: req.body,
},

View File

@@ -1,6 +1,7 @@
import { getRequestListener } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { handleConnectWebhook } from "./external/webhooks/connectWebhookRouter.js";
import { analyticsMiddleware } from "./honoMiddlewares/analyticsMiddleware.js";
import { apiVersionMiddleware } from "./honoMiddlewares/apiVersionMiddleware.js";
import { baseMiddleware } from "./honoMiddlewares/baseMiddleware.js";
@@ -12,6 +13,8 @@ import { secretKeyMiddleware } from "./honoMiddlewares/secretKeyMiddleware.js";
import { traceMiddleware } from "./honoMiddlewares/traceMiddleware.js";
import type { HonoEnv } from "./honoUtils/HonoEnv.js";
import { cusRouter } from "./internal/customers/cusRouter.js";
import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.js";
import { honoOrgRouter } from "./internal/orgs/orgRouter.js";
import { honoPlatformRouter } from "./internal/platform/honoPlatformRouter.js";
import { honoProductRouter } from "./internal/products/productRouter.js";
import { auth } from "./utils/auth.js";
@@ -65,33 +68,27 @@ export const createHonoApp = () => {
return auth.handler(c.req.raw);
});
// OAuth callback (needs to be before middleware)
app.get("/stripe/oauth_callback", handleOAuthCallback);
// Step 1: Base middleware - sets up ctx (db, logger, etc.)
app.use("*", baseMiddleware);
// Step 2: Tracing middleware - handles OpenTelemetry spans
app.use("*", traceMiddleware);
// Step 4: Auth middleware - verifies secret key and populates auth context
// Webhook routes (after baseMiddleware for logging, but baseMiddleware skips body parsing)
app.post("/webhooks/connect", handleConnectWebhook);
app.use("/v1/*", secretKeyMiddleware);
// Step 5: Org config middleware - allows config overrides via header
app.use("/v1/*", orgConfigMiddleware);
// Step 3: API Version middleware - validates x-api-version header
app.use("/v1/*", apiVersionMiddleware);
// Step 6: Refresh cache middleware - clears customer cache after successful mutations
app.use("/v1/*", refreshCacheMiddleware);
// Step 7: Analytics middleware - enriches logger context and logs responses
app.use("/v1/*", analyticsMiddleware);
// Step 8: Query middleware - handles query parsing and validation
app.use("/v1/*", queryMiddleware());
app.route("v1/customers", cusRouter);
app.route("v1/products", honoProductRouter);
app.route("v1/platform", honoPlatformRouter);
app.route("v1/organization", honoOrgRouter);
// Error handler - must be defined after all routes and middleware
app.onError(errorMiddleware);

View File

@@ -5,7 +5,7 @@ import { ADMIN_USER_IDs } from "@/utils/constants.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
export const withAdminAuth = async (req: any, res: any, next: NextFunction) => {
const { logtail: logger, userId } = req as ExtendedRequest;
const { logger } = req as ExtendedRequest;
try {
const data = await auth.api.getSession({

View File

@@ -13,8 +13,6 @@ import { handleCreateBillingPortal } from "../customers/handlers/handleCreateBil
import { featureRouter } from "../features/featureRouter.js";
import { internalFeatureRouter } from "../features/internalFeatureRouter.js";
import { migrationRouter } from "../migrations/migrationRouter.js";
import { handleConnectStripe } from "../orgs/handlers/handleConnectStripe.js";
import { handleDeleteStripe } from "../orgs/handlers/handleDeleteStripe.js";
import { handleGetOrg } from "../orgs/handlers/handleGetOrg.js";
import { platformRouter } from "../platform/platformRouter.js";
import { productBetaRouter, productRouter } from "../products/productRouter.js";
@@ -68,9 +66,9 @@ apiRouter.post("/billing_portal", handleCreateBillingPortal);
apiRouter.use("/query", analyticsRouter);
apiRouter.use("/platform", platformRouter);
// Used for tests...
apiRouter.post("/organization/stripe", handleConnectStripe);
apiRouter.delete("/organization/stripe", handleDeleteStripe);
// // Used for tests...
// apiRouter.post("/organization/stripe", ...handleConnectStripe);
// apiRouter.delete("/organization/stripe", ...handleDeleteStripe);
apiRouter.get("/organization", handleGetOrg);
export { apiRouter };

View File

@@ -72,7 +72,7 @@ export const handleBatchCustomers = async (req: any, res: any) =>
offset: query.offset,
features: req.features,
statuses: query.statuses ?? [],
logger: req.logtail,
logger: req.logger,
apiVersion: req.apiVersion,
});
},

View File

@@ -1,6 +1,21 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
type AppEnv,
BillingType,
type Customer,
type Entitlement,
type Entity,
EntityExpand,
ErrCode,
type Feature,
type FullCustomerEntitlement,
type FullCustomerPrice,
type Organization,
type UsagePriceConfig,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { submitUsageToStripe } from "@/external/stripe/stripeMeterUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import {
getBillingType,
@@ -8,21 +23,6 @@ import {
} from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import {
AppEnv,
BillingType,
Customer,
Entitlement,
Entity,
EntityExpand,
ErrCode,
Feature,
FullCustomerEntitlement,
FullCustomerPrice,
Organization,
UsagePriceConfig,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
export const getLinkedCusEnt = ({
linkedFeature,
@@ -32,7 +32,7 @@ export const getLinkedCusEnt = ({
cusEnts: any;
}) => {
// Get linked cus ent...
let linkedCusEnt = cusEnts.find(
const linkedCusEnt = cusEnts.find(
(e: any) => e.entitlement.feature.id === linkedFeature.id,
);
@@ -48,7 +48,7 @@ export const entityFeatureIdExists = ({
}: {
cusEnt: FullCustomerEntitlement;
}) => {
let ent = cusEnt.entitlement;
const ent = cusEnt.entitlement;
return notNullish(ent.entity_feature_id);
};
@@ -102,7 +102,7 @@ export const removeEntityFromCusEnt = async ({
env: AppEnv;
}) => {
// isLinked
let isLinked = isLinkedToEntity({
const isLinked = isLinkedToEntity({
cusEnt,
entity,
});
@@ -111,22 +111,22 @@ export const removeEntityFromCusEnt = async ({
return;
}
let entitlement = cusEnt.entitlement;
const entitlement = cusEnt.entitlement;
console.log(
`Linked cus ent: ${entitlement.feature.id}, isLinked: ${isLinked}`,
);
// Delete cus ent ids
let newEntities = structuredClone(cusEnt.entities!);
const newEntities = structuredClone(cusEnt.entities!);
// TODO: Send usage to stripe if cus price exists
let stripeCli = createStripeCli({
const stripeCli = createStripeCli({
org,
env,
});
if (cusPrice) {
let config = cusPrice.price.config as UsagePriceConfig;
let billingType = getBillingType(config);
const config = cusPrice.price.config as UsagePriceConfig;
const billingType = getBillingType(config);
if (billingType == BillingType.UsageInArrear) {
let usage = -newEntities[entity.id]?.balance;
@@ -163,8 +163,8 @@ export const removeEntityFromCusEnt = async ({
export const parseEntityExpand = (expand: string): EntityExpand[] => {
if (expand) {
let options = expand.split(",");
let result: EntityExpand[] = [];
const options = expand.split(",");
const result: EntityExpand[] = [];
for (const option of options) {
if (!Object.values(EntityExpand).includes(option as EntityExpand)) {
throw new RecaseError({

View File

@@ -1,25 +1,25 @@
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { EntityService } from "../EntityService.js";
import { StatusCodes } from "http-status-codes";
import { CusProductStatus, ErrCode } from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import { adjustAllowance } from "@/trigger/adjustAllowance.js";
import { StatusCodes } from "http-status-codes";
import { handleCustomerRaceCondition } from "@/external/redis/redisUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import {
findLinkedCusEnts,
findMainCusEntForFeature,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
import {
deleteEntityFromCusEnt,
replaceEntityInCusEnt,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/linkedCusEntUtils.js";
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
import { cancelSubsForEntity } from "@/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.js";
import { adjustAllowance } from "@/trigger/adjustAllowance.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { EntityService } from "../EntityService.js";
export const handleDeleteEntity = async (req: any, res: any) => {
try {
const { org, env, db, logtail: logger, features } = req;
const { org, env, db, logger, features } = req;
const { customer_id, entity_id } = req.params;
await handleCustomerRaceCondition({
@@ -73,9 +73,9 @@ export const handleDeleteEntity = async (req: any, res: any) => {
const feature = features.find((f: any) => f.id === entity?.feature_id);
for (const cusProduct of cusProducts) {
let cusEnts = cusProduct.customer_entitlements;
const cusEnts = cusProduct.customer_entitlements;
let mainCusEnt = findMainCusEntForFeature({
const mainCusEnt = findMainCusEntForFeature({
cusEnts,
feature,
});
@@ -97,12 +97,12 @@ export const handleDeleteEntity = async (req: any, res: any) => {
logger,
});
let linkedCusEnts = findLinkedCusEnts({
const linkedCusEnts = findLinkedCusEnts({
cusEnts: cusProduct.customer_entitlements,
feature: mainCusEnt.entitlement.feature,
});
let replaceable =
const replaceable =
newReplaceables && newReplaceables.length > 0
? newReplaceables[0]
: null;
@@ -121,14 +121,14 @@ export const handleDeleteEntity = async (req: any, res: any) => {
for (const linkedCusEnt of linkedCusEnts) {
let newEntities;
if (replaceable) {
let { newEntities: newEntities_ } = replaceEntityInCusEnt({
const { newEntities: newEntities_ } = replaceEntityInCusEnt({
cusEnt: linkedCusEnt,
entityId: entity.id,
replaceable,
});
newEntities = newEntities_;
} else {
let { newEntities: newEntities_ } = deleteEntityFromCusEnt({
const { newEntities: newEntities_ } = deleteEntityFromCusEnt({
cusEnt: linkedCusEnt,
entityId: entity.id,
});

View File

@@ -27,7 +27,7 @@ checkRouter.post("", async (req: any, res: any) => {
entity_id,
} = req.body;
const { logtail: logger, db } = req;
const { logger, db } = req;
if (!customer_id) {
throw new RecaseError({

View File

@@ -1,8 +1,11 @@
import { CusProductStatus, FullCusProduct, SuccessCode } from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
import { ProductService } from "@/internal/products/ProductService.js";
import {
CusProductStatus,
type FullCusProduct,
SuccessCode,
} from "@autumn/shared";
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
import { getOrgAndFeatures } from "@/internal/orgs/orgUtils.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { notNullish } from "@/utils/genUtils.js";
import { getProductCheckPreview } from "./getProductCheckPreview.js";
@@ -21,12 +24,10 @@ export const handleProductCheck = async ({
with_preview,
entity_data,
} = req.body;
const { orgId, env, logtail: logger, db } = req;
let { org, features } = await getOrgAndFeatures({ req });
const { orgId, env, logger, db } = req;
// 1. Get customer and org
let [customer, product] = await Promise.all([
const [customer, product] = await Promise.all([
getOrCreateCustomer({
req,
customerId: customer_id,
@@ -53,15 +54,15 @@ export const handleProductCheck = async ({
if (customer.entity) {
cusProducts = cusProducts.filter(
(cusProduct: FullCusProduct) =>
cusProduct.internal_entity_id == customer.entity!.internal_id,
cusProduct.internal_entity_id === customer.entity!.internal_id,
);
}
let cusProduct: FullCusProduct | undefined = cusProducts.find(
const cusProduct: FullCusProduct | undefined = cusProducts.find(
(cusProduct: FullCusProduct) => cusProduct.product.id === product_id,
);
let preview = with_preview
const preview = with_preview
? await getProductCheckPreview({
req,
customer,
@@ -96,7 +97,7 @@ export const handleProductCheck = async ({
return;
}
let onTrial =
const onTrial =
notNullish(cusProduct.trial_ends_at) &&
cusProduct.trial_ends_at! > Date.now();

View File

@@ -178,7 +178,7 @@ export const handleEventSent = async ({
customer_id,
customer_data,
event_data,
logger: req.logtail,
logger: req.logger,
entityId: event_data.entity_id,
entityData: event_data.entity_data,
features,

View File

@@ -141,7 +141,7 @@ export const handleUsageEvent = async ({
entity_id,
idempotency_key,
} = req.body;
const { logtail: logger } = req;
const { logger } = req;
if (!customer_id || !feature_id) {
throw new RecaseError({

View File

@@ -1,8 +1,7 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { Router } from "express";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { handleRequestError } from "@/utils/errorUtils.js";
import { Router } from "express";
export const invoiceRouter: Router = Router();

View File

@@ -10,8 +10,8 @@ import { OrgService } from "@/internal/orgs/OrgService.js";
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
import { triggerRedemption } from "@/internal/rewards/referralUtils.js";
import { triggerFreeProduct } from "@/internal/rewards/referralUtils/triggerFreeProduct.js";
import { triggerRedemption } from "@/internal/rewards/referralUtils.js";
import { getRewardCat } from "@/internal/rewards/rewardUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { generateId, notNullish } from "@/utils/genUtils.js";
@@ -24,7 +24,7 @@ export default async (req: any, res: any) =>
res,
action: "redeem referral code",
handler: async (req, res) => {
const { orgId, env, logtail: logger, db } = req;
const { orgId, env, logger, db } = req;
const { code, customer_id: customerId } = req.body;
// 1. Get redeemed by customer, and referral code

View File

@@ -23,7 +23,7 @@ export default async (req: any, res: any) =>
res,
action: "create coupon",
handler: async (req, res) => {
const { db, orgId, env, logtail: logger } = req;
const { db, orgId, env, logger } = req;
const rewardBody = req.body;
const rewardData = CreateRewardSchema.parse(rewardBody);

View File

@@ -1,5 +1,5 @@
import { ErrCode } from "@autumn/shared";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
import RecaseError from "@/utils/errorUtils.js";

View File

@@ -1,6 +1,6 @@
import { ErrCode, PriceType, RewardCategory } from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
@@ -16,7 +16,7 @@ export default async (req: any, res: any) =>
action: "update coupon",
handler: async (req, res) => {
const { internalId } = req.params;
const { orgId, env, db, logtail: logger } = req;
const { orgId, env, db, logger } = req;
const rewardBody = req.body;
const org = await OrgService.getFromReq(req);

View File

@@ -5,8 +5,8 @@ import {
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 { createStripeCli } from "@/external/stripe/utils.js";
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js";
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
@@ -31,7 +31,7 @@ export const handleCreateCheckout = async ({
config: AttachConfig;
returnCheckout?: boolean;
}) => {
const { db, logtail: logger } = req;
const { db, logger } = req;
const { customer, org, freeTrial, successUrl, rewards } = attachParams;

View File

@@ -41,7 +41,7 @@ export const handleRenewProduct = async ({
attachParams: AttachParams;
config: AttachConfig;
}) => {
const logger = req.logtail;
const logger = req.logger;
const { stripeCli } = attachParams;
let { curScheduledProduct } = attachParamToCusProducts({ attachParams });

View File

@@ -5,12 +5,12 @@ import {
type FullCusProduct,
} from "@autumn/shared";
import { Router } from "express";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
import {
createStripeCusIfNotExists,
getCusPaymentMethod,
} from "@/external/stripe/stripeCusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusService } from "@/internal/customers/CusService.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
@@ -140,7 +140,7 @@ export const checkStripeConnections = async ({
useCheckout?: boolean;
}) => {
const { org, customer, products, stripeCus, stripeCli } = attachParams;
const logger = req.logtail;
const logger = req.logger;
const env = customer.env;
// 2. If invoice only and no email, save email

View File

@@ -1,5 +1,5 @@
import type { FullCustomer, FullProduct } from "@autumn/shared";
import { createStripeCli } from "@/external/stripe/utils.js";
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";

View File

@@ -12,7 +12,7 @@ import {
type Organization,
} from "@autumn/shared";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import type {
AttachParams,
InsertCusProductParams,

View File

@@ -1,6 +1,6 @@
import { type AttachBody, ErrCode } from "@autumn/shared";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
import RecaseError from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";

View File

@@ -1,6 +1,6 @@
import { ErrCode } from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";

View File

@@ -1,9 +1,13 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { CusProductStatus, FullCusProduct, FullCustomer } from "@autumn/shared";
import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js";
import {
CusProductStatus,
type FullCusProduct,
type FullCustomer,
} from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js";
export const cancelEndOfCycle = async ({
req,

View File

@@ -1,20 +1,18 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
AttachScenario,
CusProductStatus,
FullCusProduct,
FullCustomer,
cusProductToProduct,
type FullCusProduct,
type FullCustomer,
} from "@autumn/shared";
import { cusProductToProduct } from "@autumn/shared";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { activateDefaultProduct } from "../cusProducts/cusProductUtils.js";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
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 { 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,

View File

@@ -1,6 +1,10 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { FullCusProduct, FullCustomer, CusProductStatus } from "@autumn/shared";
import {
CusProductStatus,
type FullCusProduct,
type FullCustomer,
} from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { cusProductToSchedule } from "../cusProducts/cusProductUtils/convertCusProduct.js";

View File

@@ -11,7 +11,7 @@ import {
ProrationBehavior,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";

View File

@@ -1,14 +1,10 @@
import {
CusExpand,
FullCusEntWithFullCusProduct,
Organization,
} from "@autumn/shared";
import { AppEnv } from "autumn-js";
import { type CusExpand, type Organization } from "@autumn/shared";
import type { AppEnv } from "autumn-js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import { buildBaseCusCacheKey } from "./cusCacheUtils.js";
import { getCusWithCache } from "./getCusWithCache.js";
import { initUpstash } from "./upstashUtils.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const refreshCusCache = async ({
db,
@@ -43,14 +39,14 @@ export const refreshCusCache = async ({
for (const key of list) {
const refresh = async () => {
const keyName = key;
let params = keyName.split(":");
let expandParam = params.find((p) => p.startsWith("expand_"));
let expand = expandParam
const params = keyName.split(":");
const expandParam = params.find((p) => p.startsWith("expand_"));
const expand = expandParam
? expandParam.replace("expand_", "").split(",")
: [];
let entityIdParam = params.find((p) => p.startsWith("entity_"));
let entityId = entityIdParam
const entityIdParam = params.find((p) => p.startsWith("entity_"));
const entityId = entityIdParam
? entityIdParam.replace("entity_", "")
: undefined;

View File

@@ -15,7 +15,7 @@ import {
} from "@autumn/shared";
import { logger } from "better-auth";
import { Decimal } from "decimal.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import {

View File

@@ -19,8 +19,8 @@ import {
type UsagePriceConfig,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { ProductService } from "@/internal/products/ProductService.js";
import {

View File

@@ -2,8 +2,8 @@ import { ErrCode } from "@autumn/shared";
import { Router } from "express";
import { Hono } from "hono";
import { StatusCodes } from "http-status-codes";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { CusSearchService } from "@/internal/customers/CusSearchService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
@@ -101,7 +101,7 @@ expressCusRouter.get(
org,
env: req.env,
customer,
logger: req.logtail,
logger: req.logger,
});
if (!newCus) {

View File

@@ -8,7 +8,7 @@ import {
type FullCustomer,
type FullProduct,
} from "@autumn/shared";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { addCustomerCreatedTask } from "@/internal/analytics/handlers/handleCustomerCreated.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";

View File

@@ -1,7 +1,11 @@
import {
type AppEnv,
CusExpand,
type FullCustomer,
type Organization,
} from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AppEnv, CusExpand, FullCustomer, Organization } from "@autumn/shared";
export const getCusPaymentMethodRes = async ({
org,
@@ -18,12 +22,12 @@ export const getCusPaymentMethodRes = async ({
return undefined;
}
let stripeCli = createStripeCli({
const stripeCli = createStripeCli({
org,
env,
});
let paymentMethod = await getCusPaymentMethod({
const paymentMethod = await getCusPaymentMethod({
stripeCli,
stripeId: fullCus.processor?.id,
errorIfNone: false,

View File

@@ -7,8 +7,8 @@ import {
RewardType,
} from "@autumn/shared";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
export const getCusRewards = async ({
org,

View File

@@ -8,10 +8,10 @@ import {
} from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { lineItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { stripeDiscountToResponse } from "./stripeDiscountToResponse.js";
export const getCusUpcomingInvoice = async ({

View File

@@ -1,7 +1,7 @@
import { ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { RewardService } from "@/internal/rewards/RewardService.js";

View File

@@ -1,8 +1,8 @@
import { ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js";
@@ -81,7 +81,7 @@ export const handleCreateBillingPortal = async (req: any, res: any) =>
org,
env: req.env,
customer,
logger: req.logtail,
logger: req.logger,
});
if (!newCus) {
@@ -120,14 +120,14 @@ export const handleCreateBillingPortal = async (req: any, res: any) =>
) {
try {
// Create a default billing portal configuration
req.logtail?.info(
req.logger?.info(
`Creating default billing portal configuration for customer ${customer.id}`,
);
const configuration =
await createDefaultBillingPortalConfiguration(stripeCli);
req.logtail?.info(
req.logger?.info(
"Successfully created billing portal configuration",
{
configurationId: configuration.id,
@@ -142,13 +142,10 @@ export const handleCreateBillingPortal = async (req: any, res: any) =>
configuration: configuration.id,
});
} catch (configError: any) {
req.logtail?.error(
"Failed to create billing portal configuration",
{
error: configError.message,
orgId: org.id,
},
);
req.logger?.error("Failed to create billing portal configuration", {
error: configError.message,
orgId: org.id,
});
throw new RecaseError({
message: `Failed to create billing portal configuration: ${configError.message}`,
code: ErrCode.StripeError,

View File

@@ -1,30 +1,24 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import {
CusProductStatus,
cusProductToPrices,
ErrCode,
type FullCusProduct,
type FullCustomer,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import {
ACTIVE_STATUSES,
CusProductService,
} from "@/internal/customers/cusProducts/CusProductService.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import {
cancelCusProductSubscriptions,
expireAndActivate,
fullCusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
ErrCode,
CusProductStatus,
FullCusProduct,
Organization,
AppEnv,
FullCustomer,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { CusService } from "../CusService.js";
import { cusProductToPrices } from "@autumn/shared";
export const expireCusProduct = async ({
req,
cusProduct, // cus product to expire
@@ -61,8 +55,8 @@ export const expireCusProduct = async ({
// }
// 1. If main product, can't expire if there's scheduled product
let isMain = !cusProduct.product.is_add_on;
let { curScheduledProduct: futureProduct } = getExistingCusProducts({
const isMain = !cusProduct.product.is_add_on;
const { curScheduledProduct: futureProduct } = getExistingCusProducts({
product: cusProduct.product,
cusProducts: fullCus.customer_products,
internalEntityId: cusProduct.internal_entity_id,
@@ -173,7 +167,7 @@ export const handleCusProductExpired = async (req: any, res: any) => {
const { db } = req;
const customerProductId = req.params.customer_product_id;
let cusProduct = await CusProductService.get({
const cusProduct = await CusProductService.get({
db,
id: customerProductId,
orgId: req.orgId,

View File

@@ -1,12 +1,15 @@
import { type AppEnv, ErrCode, type Organization } from "@autumn/shared";
import chalk from "chalk";
import RecaseError from "@/utils/errorUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { StatusCodes } from "http-status-codes";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { deleteStripeCustomer } from "@/external/stripe/stripeCusUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
import RecaseError from "@/utils/errorUtils.js";
import type {
ExtendedRequest,
ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
export const deleteCusById = async ({
db,
@@ -40,7 +43,7 @@ export const deleteCusById = async ({
});
}
let response = {
const response = {
customer,
success: true,
};
@@ -80,7 +83,7 @@ export const handleDeleteCustomer = async (req: any, res: any) =>
res,
action: "delete customer",
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
const { env, logtail: logger, db, org } = req;
const { env, logger, db, org } = req;
const { delete_in_stripe } = req.query;
const data = await deleteCusById({

View File

@@ -7,7 +7,7 @@ import {
type FullCustomer,
getStartingBalance,
} from "@autumn/shared";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";

View File

@@ -1,22 +1,18 @@
import { handleRequestError } from "@/utils/errorUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import RecaseError from "@/utils/errorUtils.js";
import { ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { getCusEntsInFeatures } from "../cusUtils/cusUtils.js";
import { ErrCode, getCusEntBalance } from "@autumn/shared";
import { Decimal } from "decimal.js";
import { StatusCodes } from "http-status-codes";
import { CusService } from "@/internal/customers/CusService.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import {
deductAllowanceFromCusEnt,
deductFromUsageBasedCusEnt,
} from "@/trigger/updateBalanceTask.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { getCusEntBalance } from "@autumn/shared";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { getCusEntsInFeatures } from "../cusUtils/cusUtils.js";
const getCusFeaturesAndOrg = async (req: any, customerId: string) => {
// 1. Get customer
@@ -45,7 +41,7 @@ const getCusFeaturesAndOrg = async (req: any, customerId: string) => {
export const handleUpdateBalances = async (req: any, res: any) => {
try {
const logger = req.logtail;
const logger = req.logger;
const cusId = req.params.customer_id;
const { env, db, features } = req;
const { balances } = req.body;
@@ -75,7 +71,7 @@ export const handleUpdateBalances = async (req: any, res: any) => {
const { cusEnts, cusPrices } = await getCusEntsInFeatures({
customer,
internalFeatureIds: featuresToUpdate.map((f: any) => f.internal_id!),
logger: req.logtail,
logger: req.logger,
});
logger.info("--------------------------------");
@@ -121,7 +117,7 @@ export const handleUpdateBalances = async (req: any, res: any) => {
continue;
}
let { unlimited } = getUnlimitedAndUsageAllowed({
const { unlimited } = getUnlimitedAndUsageAllowed({
cusEnts,
internalFeatureId: feature!.internal_id!,
});
@@ -135,21 +131,21 @@ export const handleUpdateBalances = async (req: any, res: any) => {
}
// Get deductions
let newBalance = balance.balance;
const newBalance = balance.balance;
let curBalance = new Decimal(0);
let properties = structuredClone(balance);
const properties = structuredClone(balance);
delete properties.feature_id;
delete properties.balance;
for (const cusEnt of cusEnts) {
let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
let deductionIntCount = balance.interval_count || 1;
const cusEntIntCount = cusEnt.entitlement.interval_count || 1;
const deductionIntCount = balance.interval_count || 1;
let intCountMatch = notNullish(balance.interval_count)
const intCountMatch = notNullish(balance.interval_count)
? cusEntIntCount === deductionIntCount
: true;
let intMatch = notNullish(balance.interval)
const intMatch = notNullish(balance.interval)
? balance.interval === cusEnt.entitlement.interval
: true;
@@ -161,7 +157,7 @@ export const handleUpdateBalances = async (req: any, res: any) => {
continue;
}
let { balance: cusEntBalance } = getCusEntBalance({
const { balance: cusEntBalance } = getCusEntBalance({
cusEnt,
entityId: balance.entity_id,
});
@@ -169,7 +165,7 @@ export const handleUpdateBalances = async (req: any, res: any) => {
curBalance = curBalance.add(new Decimal(cusEntBalance!));
}
let toDeduct = curBalance.sub(newBalance).toNumber();
const toDeduct = curBalance.sub(newBalance).toNumber();
if (toDeduct == 0) {
logger.info(`Skipping ${feature!.id} -- no change`);
@@ -197,8 +193,8 @@ export const handleUpdateBalances = async (req: any, res: any) => {
const cusEnt = notNullish(interval)
? cusEnts.find((cusEnt) => {
let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
let deductionIntCount = featureDeduction.intervalCount || 1;
const cusEntIntCount = cusEnt.entitlement.interval_count || 1;
const deductionIntCount = featureDeduction.intervalCount || 1;
return (
cusEnt.internal_feature_id === feature!.internal_id! &&
@@ -233,14 +229,14 @@ export const handleUpdateBalances = async (req: any, res: any) => {
}
for (const cusEnt of cusEnts) {
let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
let deductionIntCount = featureDeduction.intervalCount || 1;
const cusEntIntCount = cusEnt.entitlement.interval_count || 1;
const deductionIntCount = featureDeduction.intervalCount || 1;
let intCountMatch = notNullish(featureDeduction.intervalCount)
const intCountMatch = notNullish(featureDeduction.intervalCount)
? cusEntIntCount === deductionIntCount
: true;
let intMatch = notNullish(featureDeduction.interval)
const intMatch = notNullish(featureDeduction.interval)
? featureDeduction.interval === cusEnt.entitlement.interval
: true;

View File

@@ -1,6 +1,6 @@
import { CreateCustomerSchema, ErrCode, ProcessorType } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import RecaseError from "@/utils/errorUtils.js";
@@ -139,7 +139,7 @@ export const handleUpdateCustomer = async (req: any, res: any) =>
customer: finalCustomer,
org,
env: req.env,
logger: req.logtail,
logger: req.logger,
cusProducts: finalCustomer.customer_products,
expand: parseCusExpand(req.query.expand as string),
features,

View File

@@ -1,18 +1,19 @@
import { handleRequestError } from "@/utils/errorUtils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import RecaseError from "@/utils/errorUtils.js";
import { ErrCode, FullCustomerEntitlement } from "@autumn/shared";
import {
ErrCode,
type FullCustomerEntitlement,
getCusEntBalance,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import { StatusCodes } from "http-status-codes";
import { adjustAllowance } from "@/trigger/adjustAllowance.js";
import { CusPriceService } from "@/internal/customers/cusProducts/cusPrices/CusPriceService.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { CusService } from "@/internal/customers/CusService.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { CusPriceService } from "@/internal/customers/cusProducts/cusPrices/CusPriceService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { getCusEntBalance } from "@autumn/shared";
import { adjustAllowance } from "@/trigger/adjustAllowance.js";
import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
const getCusOrgAndCusPrice = async ({
@@ -41,7 +42,7 @@ const getCusOrgAndCusPrice = async ({
export const handleUpdateEntitlement = async (req: any, res: any) => {
try {
const { db, logtail: logger } = req;
const { db } = req;
const { customer_entitlement_id } = req.params;
const { balance, next_reset_at, entity_id } = req.body;
@@ -96,27 +97,29 @@ export const handleUpdateEntitlement = async (req: any, res: any) => {
});
}
let { balance: masterBalance } = getCusEntBalance({
const { balance: masterBalance } = getCusEntBalance({
cusEnt,
entityId: entity_id,
});
const deducted = new Decimal(masterBalance!).minus(balance).toNumber();
let originalBalance = structuredClone(masterBalance);
const originalBalance = structuredClone(masterBalance);
let { newBalance, newEntities, newAdjustment } = performDeductionOnCusEnt({
cusEnt: {
...cusEnt,
customer_product: cusProduct!,
const { newBalance, newEntities, newAdjustment } = performDeductionOnCusEnt(
{
cusEnt: {
...cusEnt,
customer_product: cusProduct!,
},
toDeduct: deducted,
addAdjustment: true,
allowNegativeBalance: cusEnt.usage_allowed || false,
entityId: entity_id,
},
toDeduct: deducted,
addAdjustment: true,
allowNegativeBalance: cusEnt.usage_allowed || false,
entityId: entity_id,
});
);
let updates = {
const updates = {
balance: newBalance,
next_reset_at,
entities: newEntities,
@@ -130,7 +133,7 @@ export const handleUpdateEntitlement = async (req: any, res: any) => {
});
if (cusPrice && customer) {
let fullCusProduct = await CusProductService.get({
const fullCusProduct = await CusProductService.get({
db,
id: cusEnt.customer_product_id,
orgId: req.orgId,
@@ -150,7 +153,7 @@ export const handleUpdateEntitlement = async (req: any, res: any) => {
customer: customer,
originalBalance: originalBalance!,
newBalance: balance,
logger: req.logtail,
logger: req.logger,
});
if (newReplaceables && newReplaceables.length > 0) {

View File

@@ -1,26 +1,25 @@
import { Router } from "express";
import { CusService } from "./CusService.js";
import { ProductService } from "../products/ProductService.js";
import {
CusExpand,
CusProductStatus,
cusProductToProduct,
ErrCode,
FullCusProduct, productToCusProduct
type FullCusProduct,
productToCusProduct,
} from "@autumn/shared";
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { CusBatchService } from "../api/batch/CusBatchService.js";
import { EventService } from "../api/events/EventService.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { isStripeConnected } from "../orgs/orgUtils.js";
import { ProductService } from "../products/ProductService.js";
import { mapToProductV2 } from "../products/productV2Utils.js";
import { RewardRedemptionService } from "../rewards/RewardRedemptionService.js";
import { CusReadService } from "./CusReadService.js";
import { StatusCodes } from "http-status-codes";
import { cusProductToProduct } from "@autumn/shared";
import { isStripeConnected } from "../orgs/orgUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { CusSearchService } from "./CusSearchService.js";
import { CusBatchService } from "../api/batch/CusBatchService.js";
import { CusService } from "./CusService.js";
import { ACTIVE_STATUSES } from "./cusProducts/CusProductService.js";
export const cusRouter: Router = Router();
@@ -128,7 +127,7 @@ cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => {
const { customer_id } = req.params;
const orgId = req.orgId;
let internalCustomer = await CusService.get({
const internalCustomer = await CusService.get({
db,
orgId,
env,
@@ -144,7 +143,7 @@ cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => {
}
// Get all redemptions for this customer
let [referred, redeemed, stripeCus] = await Promise.all([
const [referred, redeemed, stripeCus] = await Promise.all([
RewardRedemptionService.getByReferrer({
db,
internalCustomerId: internalCustomer.internal_id,
@@ -169,11 +168,11 @@ cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => {
},
]);
let redeemedCustomerIds = redeemed.map(
const redeemedCustomerIds = redeemed.map(
(redemption: any) => redemption.referral_code.internal_customer_id,
);
let redeemedCustomers = await CusReadService.getInInternalIds({
const redeemedCustomers = await CusReadService.getInInternalIds({
db,
internalIds: redeemedCustomerIds,
});
@@ -243,7 +242,7 @@ cusRouter.get(
"/:customer_id/product/:product_id",
async (req: any, res: any) => {
try {
const { org, env, db, features, logtail: logger } = req;
const { org, env, db, features, logger } = req;
const { customer_id, product_id } = req.params;
const { version, customer_product_id, entity_id } = req.query;
@@ -270,10 +269,10 @@ cusRouter.get(
});
}
let cusProducts = customer.customer_products;
let entity = customer.entity;
const cusProducts = customer.customer_products;
const entity = customer.entity;
let cusProduct = productToCusProduct({
const cusProduct = productToCusProduct({
cusProducts,
productId: product_id,
internalEntityId: entity?.internal_id,
@@ -282,7 +281,7 @@ cusRouter.get(
inStatuses: ACTIVE_STATUSES,
});
let product = cusProduct
const product = cusProduct
? cusProductToProduct({ cusProduct })
: await ProductService.getFull({
db,
@@ -295,9 +294,7 @@ cusRouter.get(
: undefined,
});
let productV2 = mapToProductV2({ product: product!, features });
const productV2 = mapToProductV2({ product: product!, features });
res.status(200).json({
cusProduct,

View File

@@ -1,23 +1,23 @@
import { withOrgAuth } from "@/middleware/authMiddleware.js";
import { AppEnv } from "@autumn/shared";
import * as crypto from "crypto";
import { Router } from "express";
import { ApiKeyService } from "./ApiKeyService.js";
import { OrgService } from "../orgs/OrgService.js";
import { createKey } from "./api-keys/apiKeyUtils.js";
import { getSvixDashboardUrl } from "@/external/svix/svixHelpers.js";
import { handleRequestError } from "@/utils/errorUtils.js";
import type Stripe from "stripe";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { CacheType } from "@/external/caching/cacheActions.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { encryptData } from "@/utils/encryptUtils.js";
import Stripe from "stripe";
import {
checkKeyValid,
createWebhookEndpoint,
} from "@/external/stripe/stripeOnboardingUtils.js";
import { getSvixDashboardUrl } from "@/external/svix/svixHelpers.js";
import { withOrgAuth } from "@/middleware/authMiddleware.js";
import { encryptData } from "@/utils/encryptUtils.js";
import { handleRequestError } from "@/utils/errorUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { OrgService } from "../orgs/OrgService.js";
import { clearOrgCache } from "../orgs/orgUtils/clearOrgCache.js";
import * as crypto from "crypto";
import { isStripeConnected } from "../orgs/orgUtils.js";
import { ApiKeyService } from "./ApiKeyService.js";
import { createKey } from "./api-keys/apiKeyUtils.js";
export const devRouter: Router = Router();
@@ -82,7 +82,7 @@ devRouter.delete("/api_key/:id", withOrgAuth, async (req: any, res) => {
const { db, orgId } = req;
const { id } = req.params;
let data = await ApiKeyService.delete({
const data = await ApiKeyService.delete({
db,
id,
orgId,
@@ -94,8 +94,8 @@ devRouter.delete("/api_key/:id", withOrgAuth, async (req: any, res) => {
return;
}
let batchInvalidate = [];
for (let apiKey of data) {
const batchInvalidate = [];
for (const apiKey of data) {
batchInvalidate.push(
CacheManager.invalidate({
action: CacheType.SecretKey,
@@ -240,14 +240,14 @@ export const handleGetOtp = async (req: any, res: any) =>
userId: req.user?.id,
});
let org = await OrgService.get({
const org = await OrgService.get({
db: req.db,
orgId: cacheData.orgId,
});
let stripeConnected = isStripeConnected({ org, env: AppEnv.Sandbox });
const stripeConnected = isStripeConnected({ org, env: AppEnv.Sandbox });
let responseData = {
const responseData = {
...cacheData,
stripe_connected: stripeConnected,
sandboxKey,
@@ -265,9 +265,9 @@ export const handleGetOtp = async (req: any, res: any) =>
if (!stripeConnected) {
// we need to generate a key for the CLI to use.
let key = generateRandomKey();
const key = generateRandomKey();
responseData.stripeFlowAuthKey = key;
let stripeCacheData = {
const stripeCacheData = {
orgId: cacheData.orgId,
};
await CacheManager.setJson(key, stripeCacheData, OTP_TTL);
@@ -283,7 +283,7 @@ devRouter.post("/cli/stripe", async (req: any, res: any) => {
res,
action: "Get Stripe Flow Auth Key",
handler: async () => {
const { db, logtail: logger } = req;
const { db, logger } = req;
const key = req.headers["authorization"];
if (!key) {
res.status(401).json({ message: "Unauthorized" });
@@ -345,7 +345,7 @@ devRouter.post("/cli/stripe", async (req: any, res: any) => {
},
});
let redisClient = await CacheManager.getClient();
const redisClient = await CacheManager.getClient();
if (!redisClient) {
res.status(500).json({ message: "Cache client not initialized" });
return;

View File

@@ -112,7 +112,7 @@ export const handlePostEntityRequest = async (req: any, res: any) =>
res,
action: "create entity",
handler: async (req: any, res: any) => {
const { logtail: logger, org } = req;
const { logger, org } = req;
const apiVersion = orgToVersion({
org,

View File

@@ -1,9 +1,13 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import {
CusProductStatus,
type Entity,
type FullCusProduct,
} from "@autumn/shared";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { cancelCurSubs } from "@/internal/customers/change-product/handleDowngrade/cancelCurSubs.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { cusProductsToStripeSubs } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { CusProductStatus, Entity, FullCusProduct } from "@autumn/shared";
import type { ExtendedRequest } from "@/utils/models/Request.js";
export const cancelSubsForEntity = async ({
req,
@@ -14,10 +18,10 @@ export const cancelSubsForEntity = async ({
cusProducts: FullCusProduct[];
entity: Entity;
}) => {
const { org, env, db, logtail: logger } = req;
const { org, env, db, logger } = req;
try {
let stripeCli = createStripeCli({ org, env });
let curSubs = await cusProductsToStripeSubs({
const stripeCli = createStripeCli({ org, env });
const curSubs = await cusProductsToStripeSubs({
cusProducts,
stripeCli,
});

View File

@@ -11,7 +11,7 @@ export const handleCreateFeature = async (req: any, res: any) => {
try {
console.log("Trying to create feature");
const data = req.body;
const { db, orgId, env, logtail: logger } = req;
const { db, orgId, env, logger } = req;
const parsedFeature = validateFeature(data);
const feature: Feature = {

View File

@@ -259,7 +259,7 @@ export const handleUpdateFeature = async (
handler: async (req: any, res: any) => {
const featureId = req.params.feature_id;
const data = req.body;
const { db, orgId, env, logtail: logger } = req;
const { db, orgId, env, logger } = req;
// 1. Get feature by ID
const features = await FeatureService.getFromReq(req);

View File

@@ -4,7 +4,7 @@ import { Autumn } from "autumn-js";
import { autumnHandler } from "autumn-js/express";
import { Router } from "express";
import rateLimit from "express-rate-limit";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { withAuth, withOrgAuth } from "../middleware/authMiddleware.js";
import { adminRouter } from "./admin/adminRouter.js";
import { withAdminAuth } from "./admin/withAdminAuth.js";

View File

@@ -8,7 +8,7 @@ import type {
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
@@ -56,7 +56,6 @@ export const migrateCustomer = async ({
org,
features,
logger,
logtail: logger,
timestamp: Date.now(),
} as ExtendedRequest;

View File

@@ -10,8 +10,8 @@ import {
MigrationJobStep,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { MigrationService } from "../MigrationService.js";
import { migrateCustomer } from "./migrateCustomer.js";

View File

@@ -11,9 +11,10 @@ import {
organizations,
user,
} from "@autumn/shared";
import { and, eq, sql } from "drizzle-orm";
import { and, eq, or, sql } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
import { FeatureService } from "../features/FeatureService.js";
import { clearOrgCache } from "./orgUtils/clearOrgCache.js";
export class OrgService {
@@ -284,4 +285,54 @@ export class OrgService {
return result;
}
static async getByAccountId({
db,
accountId,
}: {
db: DrizzleCli;
accountId: string;
}) {
const result = await db.query.organizations.findFirst({
where: or(
eq(
sql`${organizations.stripe_connect}->>'default_account_id'`,
accountId,
),
eq(sql`${organizations.stripe_connect}->>'test_account_id'`, accountId),
eq(sql`${organizations.stripe_connect}->>'live_account_id'`, accountId),
),
});
if (!result) {
throw new RecaseError({
message: "Organization not found",
code: ErrCode.OrgNotFound,
statusCode: 404,
});
}
const defaultAccountId = result?.stripe_connect?.default_account_id;
const testAccountId = result?.stripe_connect?.test_account_id;
const env =
defaultAccountId === accountId || testAccountId === accountId
? AppEnv.Sandbox
: AppEnv.Live;
const features = await FeatureService.list({
db,
orgId: result?.id || "",
env,
});
return {
features,
org: {
...(result as Organization),
config: OrgConfigSchema.parse(result.config || {}),
},
env,
};
}
}

View File

@@ -1,18 +1,17 @@
import { AppEnv, ErrCode } from "@autumn/shared";
import Stripe from "stripe";
import { z } from "zod";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { ensureStripeProductsWithEnv } from "@/external/stripe/stripeEnsureUtils.js";
import {
checkKeyValid,
createWebhookEndpoint,
} from "@/external/stripe/stripeOnboardingUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { encryptData } from "@/utils/encryptUtils.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { nullish } from "@/utils/genUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { OrgService } from "../OrgService.js";
import { clearOrgCache } from "../orgUtils/clearOrgCache.js";
import { isStripeConnected } from "../orgUtils.js";
export const connectStripe = async ({
@@ -49,6 +48,10 @@ export const connectStripe = async ({
test_webhook_secret: encryptData(webhook.secret as string),
env,
defaultCurrency: account.default_currency,
metadata: {
org_id: orgId,
env: env,
},
};
} else {
return {
@@ -56,92 +59,14 @@ export const connectStripe = async ({
live_webhook_secret: encryptData(webhook.secret as string),
env,
defaultCurrency: account.default_currency,
metadata: {
org_id: orgId,
env: env,
},
};
}
};
export const connectAllStripe = async ({
db,
orgId,
logger,
testApiKey,
liveApiKey,
defaultCurrency,
successUrl,
}: {
db: any;
orgId: string;
logger: any;
testApiKey: string;
liveApiKey: string;
defaultCurrency?: string;
successUrl: string;
}) => {
// 1. Check if API keys are valid
try {
await clearOrgCache({
db,
orgId,
logger,
});
await checkKeyValid(testApiKey);
await checkKeyValid(liveApiKey);
// Get default currency from Stripe
const stripe = new Stripe(testApiKey);
const account = await stripe.accounts.retrieve();
if (nullish(defaultCurrency) && nullish(account.default_currency)) {
throw new RecaseError({
message: "Default currency not set",
code: ErrCode.StripeKeyInvalid,
statusCode: 500,
});
} else if (nullish(defaultCurrency)) {
defaultCurrency = account.default_currency;
}
} catch (error: any) {
// console.error("Error checking stripe keys", error);
throw new RecaseError({
message: error.message || "Invalid Stripe API keys",
code: ErrCode.StripeKeyInvalid,
statusCode: 500,
data: error,
});
}
// 2. Create webhook endpoint
let testWebhook: Stripe.WebhookEndpoint;
let liveWebhook: Stripe.WebhookEndpoint;
try {
testWebhook = await createWebhookEndpoint(
testApiKey,
AppEnv.Sandbox,
orgId,
);
liveWebhook = await createWebhookEndpoint(liveApiKey, AppEnv.Live, orgId);
} catch (error) {
throw new RecaseError({
message: "Error creating stripe webhook",
code: ErrCode.StripeKeyInvalid,
statusCode: 500,
data: error,
});
}
return {
defaultCurrency,
stripeConfig: {
test_api_key: encryptData(testApiKey),
live_api_key: encryptData(liveApiKey),
test_webhook_secret: encryptData(testWebhook.secret as string),
live_webhook_secret: encryptData(liveWebhook.secret as string),
success_url: successUrl,
},
};
};
const connectStripeBody = z.object({
secret_key: z.string().optional(),
success_url: z.string().optional(),
@@ -298,7 +223,6 @@ export const handleGetStripe = async (req: any, res: any) => {
}
const stripeCli = createStripeCli({ org, env: req.env });
const account_details = await stripeCli.accounts.retrieve();
res.status(200).json(account_details);

View File

@@ -1,9 +1,12 @@
import { AppEnv, customers, ErrCode, type Organization } from "@autumn/shared";
import { and, eq } from "drizzle-orm";
import type { Response } from "express";
import Stripe from "stripe";
import { initMasterStripe } from "@/external/connect/initMasterStripe.js";
import type { Logger } from "@/external/logtail/logtailUtils.js";
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AppEnv, customers, ErrCode, Organization } from "@autumn/shared";
import { and, eq } from "drizzle-orm";
import { Response } from "express";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { deleteStripeWebhook } from "../orgUtils.js";
const deleteSvixWebhooks = async ({
@@ -61,12 +64,58 @@ const deleteStripeWebhooks = async ({
}
};
const deleteStripeAccounts = async ({
org,
logger,
}: {
org: Organization;
logger: Logger;
}) => {
const stripe = initMasterStripe();
if (org.stripe_connect.test_account_id) {
try {
await stripe.accounts.del(org.stripe_connect.test_account_id);
} catch (error) {
if (error instanceof Stripe.errors.StripeError) {
logger.error(
`Failed to delete stripe test acocunt ID for ${org.id}, ${org.slug}. ${error.message})`,
);
}
}
}
if (org.stripe_connect.live_account_id) {
try {
await stripe.accounts.del(org.stripe_connect.live_account_id);
} catch (error) {
if (error instanceof Stripe.errors.StripeError) {
logger.error(
`Failed to delete stripe live account ID for ${org.id}, ${org.slug}. ${error.message})`,
);
}
}
}
if (org.stripe_connect.default_account_id) {
try {
await stripe.accounts.del(org.stripe_connect.default_account_id);
} catch (error) {
if (error instanceof Stripe.errors.StripeError) {
logger.error(
`Failed to delete stripe default account ID for ${org.id}, ${org.slug}. ${error.message})`,
);
}
}
}
};
export const handleDeleteOrg = async (req: ExtendedRequest, res: Response) => {
try {
const { org, db, logtail: logger } = req;
const { org, db, logger } = req;
// 1. Check if any customers
let hasCustomers = await db.query.customers.findFirst({
const hasCustomers = await db.query.customers.findFirst({
where: and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Live)),
});
@@ -85,8 +134,12 @@ export const handleDeleteOrg = async (req: ExtendedRequest, res: Response) => {
logger.info("2. Deleting stripe webhooks");
await deleteStripeWebhooks({ org, logger });
// 4. Delete stripe accounts
logger.info("3. Deleting stripe accounts");
await deleteStripeAccounts({ org, logger });
// 4. Delete all sandbox customers
logger.info("3. Deleting sandbox customers");
logger.info("4. Deleting sandbox customers");
await db
.delete(customers)
.where(

View File

@@ -1,69 +0,0 @@
import { routeHandler } from "@/utils/routerUtils.js";
import { OrgService } from "../OrgService.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { clearOrgCache } from "../orgUtils/clearOrgCache.js";
import { AppEnv, Organization } from "@autumn/shared";
import { isStripeConnected } from "../orgUtils.js";
export const disconnectStripe = async ({
org,
env,
}: {
org: Organization;
env: AppEnv;
}) => {
if (isStripeConnected({ org, env })) {
const stripeCli = createStripeCli({ org, env });
const webhooks = await stripeCli.webhookEndpoints.list();
for (const webhook of webhooks.data) {
if (webhook.url.includes(org.id) && webhook.url.includes(env)) {
await stripeCli.webhookEndpoints.del(webhook.id);
}
}
}
};
export const handleDeleteStripe = async (req: any, res: any) =>
routeHandler({
req,
res,
action: "delete stripe",
handler: async (req: any, res: any) => {
const org = await OrgService.getFromReq(req);
let { db, orgId, logtail: logger } = req;
await clearOrgCache({
db,
orgId,
logger,
});
try {
await disconnectStripe({ org, env: req.env });
} catch (error) {
logger.error(`Failed to disconnect stripe for ${org.id}, ${org.slug}`, {
error,
});
}
// Update stripe config:
const newStripeConfig = structuredClone(req.org.stripe_config);
if (req.env === AppEnv.Sandbox) {
newStripeConfig.test_api_key = null;
} else {
newStripeConfig.live_api_key = null;
}
await OrgService.update({
db,
orgId: req.orgId,
updates: {
stripe_config: newStripeConfig,
},
});
res.status(200).json({
message: "Stripe disconnected",
});
},
});

View File

@@ -1,7 +1,10 @@
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
import type {
ExtendedRequest,
ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { createOrgResponse } from "../orgUtils.js";
import { OrgService } from "../OrgService.js";
import { createOrgResponse } from "../orgUtils.js";
export const handleGetOrg = async (req: any, res: any) =>
routeHandler({

View File

@@ -1,8 +1,7 @@
import { session as authSession, member } from "@autumn/shared";
import { and, eq } from "drizzle-orm";
import { handleFrontendReqError } from "@/utils/errorUtils.js";
import { OrgService } from "../OrgService.js";
import { auth } from "@/utils/auth.js";
import { eq, and } from "drizzle-orm";
import { member, session as authSession } from "@autumn/shared";
export const handleGetOrgMembers = async (req: any, res: any) => {
try {
@@ -77,7 +76,7 @@ export const handleRemoveMember = async (req: any, res: any) => {
);
} catch (error) {
// Log but don't fail the request if session revocation fails
req.logtail?.warn(
req.logger?.warn(
`Failed to revoke sessions for user ${existingMember.userId} in org ${orgId}:`,
error,
);

View File

@@ -0,0 +1,88 @@
import { AppEnv, type StripeConfig } from "@autumn/shared";
import { z } from "zod/v4";
import { ensureStripeProductsWithEnv } from "@/external/stripe/stripeEnsureUtils.js";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { OrgService } from "../../OrgService.js";
import { handleStripeSecretKey } from "../../orgUtils/handleStripeSecretKey.js";
// Connecting stripe
const validateConnectStripeRequest = () => {};
const addSuccessUrlToUpdates = ({
success_url,
env,
configUpdates,
}: {
success_url?: string;
env: AppEnv;
configUpdates: StripeConfig;
}) => {
if (success_url === undefined) return;
if (env === AppEnv.Sandbox) {
configUpdates.sandbox_success_url = success_url;
} else {
configUpdates.success_url = success_url;
}
};
export const handleConnectStripe = createRoute({
body: z.object({
secret_key: z.string().optional(),
success_url: z.string().optional(),
default_currency: z.string().optional(),
}),
handler: async (c) => {
const ctx = c.get("ctx");
const { db, org, logger, env } = ctx;
const body = c.req.valid("json");
const configUpdates: StripeConfig = org.stripe_config || {};
if (body.secret_key) {
const result = await handleStripeSecretKey({
orgId: org.id,
secretKey: body.secret_key,
env,
});
if (env === AppEnv.Sandbox) {
configUpdates.test_api_key = result.test_api_key;
configUpdates.test_webhook_secret = result.test_webhook_secret;
} else {
configUpdates.live_api_key = result.live_api_key;
configUpdates.live_webhook_secret = result.live_webhook_secret;
}
}
addSuccessUrlToUpdates({
success_url: body.success_url,
env,
configUpdates,
});
const newOrg = await OrgService.update({
db,
orgId: org.id,
updates: {
default_currency: body.default_currency,
stripe_config: configUpdates,
},
});
if (newOrg) {
await ensureStripeProductsWithEnv({
db,
logger,
req: ctx as ExtendedRequest,
org: newOrg,
env,
});
}
return c.json({
message: "Connect Stripe",
});
},
});

View File

@@ -0,0 +1,110 @@
import {
AppEnv,
type Organization,
type StripeConnectConfig,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { orgToAccountId } from "@/external/connect/connectUtils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { initMasterStripe } from "@/external/connect/initMasterStripe.js";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { OrgService } from "../../OrgService.js";
import { clearOrgCache } from "../../orgUtils/clearOrgCache.js";
import { isStripeConnected } from "../../orgUtils.js";
export const disconnectStripe = async ({
org,
env,
}: {
org: Organization;
env: AppEnv;
}) => {
if (isStripeConnected({ org, env, throughSecretKey: true })) {
const stripeCli = createStripeCli({ org, env });
const webhooks = await stripeCli.webhookEndpoints.list();
for (const webhook of webhooks.data) {
if (webhook.url.includes(org.id) && webhook.url.includes(env)) {
await stripeCli.webhookEndpoints.del(webhook.id);
}
}
}
const accountId = orgToAccountId({ org, env, noDefaultAccount: true });
if (accountId) {
const masterStripe = initMasterStripe();
await masterStripe.accounts.del(accountId);
}
};
export const clearStripeConfig = async ({
db,
org,
env,
}: {
db: DrizzleCli;
org: Organization;
env: AppEnv;
}) => {
const newStripeConfig: any = structuredClone(org.stripe_config) || {};
if (env === AppEnv.Sandbox) {
newStripeConfig.test_api_key = null;
} else {
newStripeConfig.live_api_key = null;
}
await OrgService.update({
db,
orgId: org.id,
updates: {
stripe_config: newStripeConfig,
},
});
};
export const handleDeleteStripe = createRoute({
handler: async (c) => {
const ctx = c.get("ctx");
const { db, org, logger, env } = ctx;
await clearOrgCache({
db,
orgId: org.id,
logger,
});
try {
await disconnectStripe({ org, env });
} catch (error) {
logger.error(`Failed to disconnect stripe for ${org.id}, ${org.slug}`, {
error,
});
}
// Update stripe config:
if (isStripeConnected({ org, env, throughSecretKey: true })) {
await clearStripeConfig({ db, org, env });
} else if (orgToAccountId({ org, env, noDefaultAccount: true })) {
const newStripeConnect: StripeConnectConfig =
structuredClone(org.stripe_connect) || {};
if (env === AppEnv.Sandbox) {
delete newStripeConnect.test_account_id;
} else {
delete newStripeConnect.live_account_id;
}
await OrgService.update({
db,
orgId: org.id,
updates: {
stripe_connect: newStripeConnect,
},
});
}
return c.json({});
},
});

View File

@@ -0,0 +1,132 @@
import { AppEnv, organizations } from "@autumn/shared";
import { eq } from "drizzle-orm";
import type { Context } from "hono";
import { initDrizzle } from "@/db/initDrizzle.js";
import { initMasterStripe } from "@/external/connect/initMasterStripe.js";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { clearOrgCache } from "../../orgUtils/clearOrgCache.js";
export const handleGetOAuthUrl = createRoute({
handler: async (c) => {
const ctx = c.get("ctx");
const { org } = ctx;
const baseUrl = new URL(
`https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=${process.env.STRIPE_CLIENT_ID}&scope=read_write`,
);
// Add state + redirect_uri
baseUrl.searchParams.set("state", `${org.id}|${ctx.env}`);
// baseUrl.searchParams.set(
// "redirect_uri",
// `${process.env.BETTER_AUTH_URL}/stripe/oauth_callback`,
// );
baseUrl.searchParams.set(
"redirect_uri",
`https://express.dev.useautumn.com/stripe/oauth_callback`,
);
return c.json({
oauth_url: baseUrl.toString(),
});
},
});
export const handleOAuthCallback = async (c: Context<HonoEnv>) => {
const query = c.req.query();
const { code, state, error } = query;
// Build frontend redirect URL
const frontendUrl = process.env.CLIENT_URL || "http://localhost:5173";
const redirectUrl = new URL(`${frontendUrl}/developer/configure-stripe`);
// Handle OAuth error from Stripe
if (error) {
console.error("Stripe OAuth error:", error);
redirectUrl.searchParams.set("error", error);
return c.redirect(redirectUrl.toString());
}
// Validate required parameters
if (!code || !state) {
console.error("Missing code or state parameter");
redirectUrl.searchParams.set("error", "missing_parameters");
return c.redirect(redirectUrl.toString());
}
// Parse state to get orgId and env
const [orgId, env] = state.split("|");
if (!orgId || !env) {
console.error("Invalid state format");
redirectUrl.searchParams.set("error", "invalid_state");
return c.redirect(redirectUrl.toString());
}
console.log(`Org ID: ${orgId}, Env: ${env}, Code: ${code}`);
try {
const stripe = initMasterStripe();
// Exchange authorization code for access token
const response = await stripe.oauth.token({
grant_type: "authorization_code",
code,
});
const accountId = response.stripe_user_id;
console.log("Connected Stripe account:", accountId);
// Get database connection
const { db } = initDrizzle();
// Fetch the organization
const [org] = await db
.select()
.from(organizations)
.where(eq(organizations.id, orgId));
if (!org) {
console.error("Organization not found:", orgId);
redirectUrl.searchParams.set("error", "org_not_found");
return c.redirect(redirectUrl.toString());
}
// Update organization with connected account based on environment
const currentConnect = org.stripe_connect || {
default_account_id: "",
test_account_id: undefined,
live_account_id: undefined,
};
const updatedStripeConnect = {
...currentConnect,
[env === AppEnv.Sandbox ? "test_account_id" : "live_account_id"]:
accountId,
};
await db
.update(organizations)
.set({
stripe_connect: updatedStripeConnect,
})
.where(eq(organizations.id, orgId));
// Clear organization cache
await clearOrgCache({ db, orgId });
console.log(`Successfully connected Stripe account for org ${orgId}`);
// Redirect to success
redirectUrl.searchParams.set("success", "true");
return c.redirect(redirectUrl.toString());
} catch (error: unknown) {
console.error("Error in OAuth callback:", error);
redirectUrl.searchParams.set(
"error",
error instanceof Error ? error.message : "unknown_error",
);
return c.redirect(redirectUrl.toString());
}
};

View File

@@ -1,17 +1,18 @@
import { Router } from "express";
import { AppEnv, chatResults } from "@autumn/shared";
import { eq } from "drizzle-orm";
import { routeHandler } from "@/utils/routerUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { AppEnv } from "@autumn/shared";
import { parseChatResultFeatures } from "./parseChatFeatures.js";
import { parseChatProducts } from "./parseChatProducts.js";
import { chatResults } from "@autumn/shared";
import { ProductService } from "@/internal/products/ProductService.js";
import { Router } from "express";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
import RecaseError from "@/utils/errorUtils.js";
import type {
ExtendedRequest,
ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { parseChatResultFeatures } from "./parseChatFeatures.js";
import { parseChatProducts } from "./parseChatProducts.js";
export const onboardingRouter: Router = Router();
@@ -21,7 +22,7 @@ onboardingRouter.post("", async (req: Request, res: any) =>
res,
action: "onboarding",
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
const { db, logtail: logger, org } = req;
const { db, logger, org } = req;
const { token } = req.body;
if (!token) {
@@ -32,7 +33,7 @@ onboardingRouter.post("", async (req: Request, res: any) =>
});
}
let chatResult = await db.query.chatResults.findFirst({
const chatResult = await db.query.chatResults.findFirst({
where: eq(chatResults.id, token),
});
@@ -44,33 +45,33 @@ onboardingRouter.post("", async (req: Request, res: any) =>
});
}
let curProducts = await ProductService.listFull({
const curProducts = await ProductService.listFull({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
let curFeatures = await FeatureService.list({
const curFeatures = await FeatureService.list({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
let newProducts = chatResult.data.products.filter((product) => {
const newProducts = chatResult.data.products.filter((product) => {
return !curProducts.some((p) => p.id === product.id);
});
let newFeatures = chatResult.data.features.filter((feature) => {
const newFeatures = chatResult.data.features.filter((feature) => {
return !curFeatures.some((f) => f.id === feature.id);
});
if (newFeatures.length > 0 || newProducts.length > 0) {
let backendFeatures = parseChatResultFeatures({
const backendFeatures = parseChatResultFeatures({
features: newFeatures,
orgId: org.id,
});
let { products, prices, ents } = await parseChatProducts({
const { products, prices, ents } = await parseChatProducts({
db,
logger,
orgId: org.id,

View File

@@ -1,10 +1,8 @@
import express, { type Router } from "express";
import {
handleConnectStripe,
handleGetStripe,
} from "./handlers/handleConnectStripe.js";
import { Hono } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { handleGetStripe } from "./handlers/handleConnectStripe_old.js";
import { handleDeleteOrg } from "./handlers/handleDeleteOrg.js";
import { handleDeleteStripe } from "./handlers/handleDeleteStripe.js";
import { handleGetInvites } from "./handlers/handleGetInvites.js";
import { handleGetOrg } from "./handlers/handleGetOrg.js";
import {
@@ -12,6 +10,9 @@ import {
handleRemoveMember,
} from "./handlers/handleGetOrgMembers.js";
import { handleGetUploadUrl } from "./handlers/handleGetUploadUrl.js";
import { handleConnectStripe } from "./handlers/stripeHandlers/handleConnectStripe.js";
import { handleDeleteStripe } from "./handlers/stripeHandlers/handleDeleteStripe.js";
import { handleGetOAuthUrl } from "./handlers/stripeHandlers/handleGetOAuthUrl.js";
export const orgRouter: Router = express.Router();
orgRouter.get("/members", handleGetOrgMembers);
@@ -30,113 +31,10 @@ orgRouter.get("", handleGetOrg);
orgRouter.get("/stripe", handleGetStripe);
orgRouter.post("/stripe", handleConnectStripe);
// orgRouter.post("/stripe", handleConnectStripe);
orgRouter.delete("/stripe", handleDeleteStripe);
export const honoOrgRouter = new Hono<HonoEnv>();
// async (req: any, res) => {
// try {
// let { testApiKey, liveApiKey, successUrl, defaultCurrency } = req.body;
// let { db, orgId, logtail: logger } = req;
// if (!testApiKey || !liveApiKey || !successUrl) {
// throw new RecaseError({
// message: "Missing required fields",
// code: ErrCode.StripeKeyInvalid,
// statusCode: 400,
// });
// }
// // 1. Check if API keys are valid
// try {
// await clearOrgCache({
// db,
// orgId,
// logger,
// });
// console.log("Connecting Stripe");
// await checkKeyValid(testApiKey);
// await checkKeyValid(liveApiKey);
// // Get default currency from Stripe
// let stripe = new Stripe(testApiKey);
// let account = await stripe.accounts.retrieve();
// if (nullish(defaultCurrency) && nullish(account.default_currency)) {
// throw new RecaseError({
// message: "Default currency not set",
// code: ErrCode.StripeKeyInvalid,
// statusCode: 500,
// });
// } else if (nullish(defaultCurrency)) {
// defaultCurrency = account.default_currency;
// }
// } catch (error: any) {
// throw new RecaseError({
// message: error.message || "Invalid Stripe API keys",
// code: ErrCode.StripeKeyInvalid,
// statusCode: 500,
// data: error,
// });
// }
// // 2. Create webhook endpoint
// let testWebhook: Stripe.WebhookEndpoint;
// let liveWebhook: Stripe.WebhookEndpoint;
// try {
// testWebhook = await createWebhookEndpoint(
// testApiKey,
// AppEnv.Sandbox,
// req.orgId
// );
// liveWebhook = await createWebhookEndpoint(
// liveApiKey,
// AppEnv.Live,
// req.orgId
// );
// } catch (error) {
// throw new RecaseError({
// message: "Error creating stripe webhook",
// code: ErrCode.StripeKeyInvalid,
// statusCode: 500,
// data: error,
// });
// }
// // 1. Update org in Supabase first
// const updatedOrg = await OrgService.update({
// db,
// orgId: req.orgId,
// updates: {
// stripe_connected: true,
// default_currency: defaultCurrency,
// stripe_config: {
// test_api_key: encryptData(testApiKey),
// live_api_key: encryptData(liveApiKey),
// test_webhook_secret: encryptData(testWebhook.secret as string),
// live_webhook_secret: encryptData(liveWebhook.secret as string),
// success_url: successUrl,
// },
// },
// });
// // 2. Ensure products are created in Stripe (after org is updated)
// await ensureStripeProducts({
// db,
// logger,
// req,
// org: updatedOrg as Organization,
// });
// res.status(200).json({
// message: "Stripe connected",
// });
// } catch (error: any) {
// handleRequestError({
// req,
// error,
// res,
// action: "connect stripe",
// });
// }
// }
honoOrgRouter.delete("/stripe", ...handleDeleteStripe);
honoOrgRouter.post("/stripe", ...handleConnectStripe);
honoOrgRouter.get("/stripe/oauth_url", ...handleGetOAuthUrl);

View File

@@ -1,23 +1,24 @@
import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
AppEnv,
ErrCode,
FrontendOrg,
Organization,
type FrontendOrg,
type Organization,
type OrgConfig,
organizations,
OrgConfig,
} from "@autumn/shared";
import { createStripeCli } from "@/external/stripe/utils.js";
import { OrgService } from "./OrgService.js";
import { FeatureService } from "../features/FeatureService.js";
import { notNullish } from "@/utils/genUtils.js";
import Stripe from "stripe";
import { toSuccessUrl } from "./orgUtils/convertOrgUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { eq } from "drizzle-orm";
import Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { orgToAccountId } from "@/external/connect/connectUtils.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { FeatureService } from "../features/FeatureService.js";
import { OrgService } from "./OrgService.js";
import { clearOrgCache } from "./orgUtils/clearOrgCache.js";
import { toSuccessUrl } from "./orgUtils/convertOrgUtils.js";
export const shouldReconnectStripe = async ({
org,
@@ -33,7 +34,7 @@ export const shouldReconnectStripe = async ({
if (!isStripeConnected({ org, env })) return true;
try {
const stripeCli = createStripeCli({ org, env: env! });
const stripeCli = createStripeCli({ org, env });
const newKey = new Stripe(stripeKey);
const oldAccount = await stripeCli.accounts.retrieve();
@@ -49,14 +50,52 @@ export const shouldReconnectStripe = async ({
export const isStripeConnected = ({
org,
env,
throughSecretKey = false,
throughAccountId = false,
excludeDefault = false,
}: {
org: Organization;
env?: AppEnv;
throughSecretKey?: boolean;
throughAccountId?: boolean;
excludeDefault?: boolean;
}) => {
const testAccountId = orgToAccountId({
org,
env: AppEnv.Sandbox,
noDefaultAccount: excludeDefault,
});
const liveAccountId = orgToAccountId({
org,
env: AppEnv.Live,
noDefaultAccount: excludeDefault,
});
if (env === AppEnv.Sandbox) {
return notNullish(org.stripe_config?.test_api_key);
if (throughAccountId) {
return notNullish(testAccountId);
}
if (throughSecretKey) {
return notNullish(org.stripe_config?.test_api_key);
}
return (
notNullish(org.stripe_config?.test_api_key) || notNullish(testAccountId)
);
} else if (env === AppEnv.Live) {
return notNullish(org.stripe_config?.live_api_key);
if (throughAccountId) {
return notNullish(liveAccountId);
}
if (throughSecretKey) {
return notNullish(org.stripe_config?.live_api_key);
}
return (
notNullish(org.stripe_config?.live_api_key) || notNullish(liveAccountId)
);
} else {
return (
notNullish(org.stripe_config?.test_api_key) &&
@@ -90,7 +129,7 @@ export const deleteStripeWebhook = async ({
org: Organization;
env: AppEnv;
}) => {
if (!isStripeConnected({ org, env })) return;
if (!isStripeConnected({ org, env, throughSecretKey: true })) return;
const stripeCli = createStripeCli({ org, env });
const webhookEndpoints = await stripeCli.webhookEndpoints.list({
@@ -146,6 +185,19 @@ export const createOrgResponse = ({
org: Organization;
env: AppEnv;
}): FrontendOrg => {
const accountId = orgToAccountId({ org, env, noDefaultAccount: true });
const secretKeyConnected = isStripeConnected({
org,
env,
throughSecretKey: true,
});
const stripeConnection = secretKeyConnected
? "secret_key"
: accountId
? "oauth"
: "default";
return {
id: org.id,
name: org.name,
@@ -164,7 +216,8 @@ export const createOrgResponse = ({
success_url: toSuccessUrl({ org, env }) || "",
default_currency: org.default_currency || "usd",
stripe_connected: isStripeConnected({ org, env }),
stripe_connection: stripeConnection,
created_at: new Date(org.createdAt).getTime(),
test_pkey: org.test_pkey,
live_pkey: org.live_pkey,
@@ -172,9 +225,9 @@ export const createOrgResponse = ({
};
export const getOrgAndFeatures = async ({ req }: { req: any }) => {
let { orgId, env } = req;
const { orgId, env } = req;
let [org, features] = await Promise.all([
const [org, features] = await Promise.all([
OrgService.getFromReq(req),
FeatureService.getFromReq(req),
]);

View File

@@ -0,0 +1,46 @@
import "dotenv/config";
import type { User } from "better-auth";
import type { Organization } from "better-auth/plugins";
import { initMasterStripe } from "@/external/connect/initMasterStripe.js";
export const createConnectAccount = async ({
org,
user,
}: {
org: Organization;
user: User;
}) => {
const stripe = initMasterStripe();
const account = await stripe.accounts.create({
business_type: "company",
email: user.email,
country: "us",
company: {
name: org.name,
},
});
// const account = await stripe.v2.core.accounts.create({
// contact_email: user.email,
// display_name: org.name,
// dashboard: "full",
// identity: {
// country: "us",
// },
// configuration: {
// merchant: {},
// },
// defaults: {
// responsibilities: {
// losses_collector: "stripe",
// fees_collector: "stripe",
// },
// },
// });
return account;
};

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