chore: made env variables optional

This commit is contained in:
John Yeo
2025-06-15 17:29:08 +01:00
parent ad0ac93b5f
commit dda910c238
58 changed files with 933 additions and 1665 deletions

20
package-lock.json generated
View File

@@ -12741,6 +12741,7 @@
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"ai": "^4.3.10",
"autumn-js": "^0.0.64",
"better-auth": "^1.2.9",
"body-parser": "^1.20.3",
"bullmq": "^5.31.1",
@@ -12795,6 +12796,25 @@
"typescript": "^5.7.3"
}
},
"server/node_modules/autumn-js": {
"version": "0.0.64",
"resolved": "https://registry.npmjs.org/autumn-js/-/autumn-js-0.0.64.tgz",
"integrity": "sha512-Fa5lr9A0ywYNcbny/dQBRKSGaqnTuUvqtiBegHrr5Z3wCw9A/Z2LRH/f8AqHYQSFOXVkS763ngLlGLxfJpYgQQ==",
"license": "MIT",
"dependencies": {
"rou3": "^0.6.1",
"swr": "^2.3.3"
},
"peerDependencies": {
"@tanstack/react-query": "^5.76.1",
"react": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"server/node_modules/date-fns": {
"version": "4.1.0",
"license": "MIT",

View File

@@ -51,8 +51,12 @@ export const account = pgTable("account", {
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
accessTokenExpiresAt: timestamp("access_token_expires_at", {
withTimezone: true,
}),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
withTimezone: true,
}),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").notNull(),
@@ -63,11 +67,11 @@ export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").$defaultFn(
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).$defaultFn(
() => /* @__PURE__ */ new Date(),
),
updatedAt: timestamp("updated_at").$defaultFn(
updatedAt: timestamp("updated_at", { withTimezone: true }).$defaultFn(
() => /* @__PURE__ */ new Date(),
),
}).enableRLS();
@@ -77,7 +81,7 @@ export const organization = pgTable("organization", {
name: text("name").notNull(),
slug: text("slug").unique(),
logo: text("logo"),
createdAt: timestamp("created_at").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
metadata: text("metadata"),
}).enableRLS();
@@ -90,7 +94,7 @@ export const member = pgTable("member", {
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
role: text("role").default("member").notNull(),
createdAt: timestamp("created_at").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
}).enableRLS();
export const invitation = pgTable("invitation", {
@@ -101,7 +105,7 @@ export const invitation = pgTable("invitation", {
email: text("email").notNull(),
role: text("role"),
status: text("status").default("pending").notNull(),
expiresAt: timestamp("expires_at").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
inviterId: text("inviter_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),

View File

@@ -47,6 +47,7 @@
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"ai": "^4.3.10",
"autumn-js": "^0.0.64",
"better-auth": "^1.2.9",
"body-parser": "^1.20.3",
"bullmq": "^5.31.1",

View File

@@ -15,10 +15,10 @@ export class CacheManager {
console.log("Initializing Cache Manager...");
if (this.initialized) return;
const redisUrl = process.env.REDIS_BACKUP_URL;
const redisUrl = process.env.REDIS_BACKUP_URL || process.env.REDIS_URL;
if (!redisUrl) {
throw new Error("Redis URL not configured");
throw new Error("Cache error: no redis connection string set in env");
}
this.client = new Redis(redisUrl, {

View File

@@ -39,6 +39,10 @@ const createLogMethod = (pinoMethod: any, logtailMethod: any) => {
pinoMethod(message);
}
if (!logtailMethod) {
return;
}
// Logtail format: message first, then object (if exists)
if (Object.keys(mergedObj).length > 0) {
logtailMethod(message, mergedObj);
@@ -48,34 +52,42 @@ const createLogMethod = (pinoMethod: any, logtailMethod: any) => {
};
};
export const createLogtail = () => {
const logtail = new Logtail(process.env.LOGTAIL_SOURCE_TOKEN!, {
endpoint: process.env.LOGTAIL_INGESTING_HOST!,
});
export const createLogger = ({
sourceToken,
ingestingHost,
}: {
sourceToken: string;
ingestingHost: string;
}) => {
let logtail: any;
if (sourceToken && ingestingHost) {
logtail = new Logtail(sourceToken, {
endpoint: ingestingHost,
});
}
// Create a custom logger that logs to both Logtail and console
const logger = {
debug: createLogMethod(
pinoLogger.debug.bind(pinoLogger),
logtail.debug.bind(logtail),
logtail?.debug.bind(logtail),
),
info: createLogMethod(
pinoLogger.info.bind(pinoLogger),
logtail.info.bind(logtail),
logtail?.info.bind(logtail),
),
warn: createLogMethod(
pinoLogger.warn.bind(pinoLogger),
logtail.warn.bind(logtail),
logtail?.warn.bind(logtail),
),
error: createLogMethod(
pinoLogger.error.bind(pinoLogger),
logtail.error.bind(logtail),
logtail?.error.bind(logtail),
),
use: (fn: any) => {
logtail.use(fn);
logtail?.use(fn);
},
getLogtail: () => logtail,
flush: () => logtail.flush(),
flush: () => logtail?.flush(),
};
return logger;
@@ -93,7 +105,21 @@ export const createLogtailWithContext = (context: any) => {
return logtail;
};
export const createLogtail = () => {
return createLogger({
sourceToken: process.env.LOGTAIL_SOURCE_TOKEN!,
ingestingHost: process.env.LOGTAIL_INGESTING_HOST!,
});
};
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!,
});
@@ -101,7 +127,5 @@ export const createLogtailAll = () => {
return logtail;
};
export const logtail = createLogtailAll();
// const logtail = createLogtailLogger();
// export default logtail;
export const logger = createLogtail();
export const logtailAll = createLogtailAll();

View File

@@ -2,11 +2,9 @@ import dotenv from "dotenv";
dotenv.config();
import { PostHog } from "posthog-node";
import { initLogger } from "@/errors/logger.js";
import { logger } from "../logtail/logtailUtils.js";
export const createPosthogCli = () => {
const logger = initLogger();
if (!process.env.POSTHOG_API_KEY) {
logger.warn("POSTHOG_API_KEY not set, skipping posthog");
return null;

View File

@@ -5,17 +5,19 @@ export const createResendCli = () => {
};
export const sendTextEmail = async ({
from,
to,
subject,
body,
}: {
from?: string;
to: string;
subject: string;
body: string;
}) => {
const resend = createResendCli();
await resend.emails.send({
from: `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
from: from || `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
to: to,
subject: subject,
text: body,
@@ -23,17 +25,19 @@ export const sendTextEmail = async ({
};
export const sendHtmlEmail = async ({
from,
to,
subject,
body,
}: {
from?: string;
to: string;
subject: string;
body: string;
}) => {
const resend = createResendCli();
await resend.emails.send({
from: `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
from: from || `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
to: to,
subject: subject,
html: body,

View File

@@ -0,0 +1,23 @@
import { logger } from "../logtail/logtailUtils.js";
export function safeResend<T extends (...args: any[]) => any>({
fn,
action,
}: {
fn: T;
action: string;
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
return async (...args: Parameters<T>) => {
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
logger.warn(
`RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
);
return;
}
try {
return await fn(...args);
} catch (error) {
logger.error(`Error ${action}: ${error}`);
}
};
}

23
server/src/external/supabase/safeSb.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import { logger } from "../logtail/logtailUtils.js";
export function safeSb<T extends (...args: any[]) => any>({
fn,
action,
}: {
fn: T;
action: string;
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
return async (...args: Parameters<T>) => {
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
logger.warn(
`SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, skipping ${action}`,
);
return;
}
try {
return await fn(...args);
} catch (error) {
logger.error(`Error ${action}: ${error}`);
}
};
}

View File

@@ -1,16 +1,17 @@
import { SupabaseClient } from "@supabase/supabase-js";
import { createSupabaseClient } from "../supabaseUtils.js";
export const uploadFile = async ({
sb,
path,
file,
contentType,
}: {
sb: SupabaseClient;
path: string;
file: Buffer;
contentType?: string;
}) => {
const sb = createSupabaseClient();
const { data, error } = await sb.storage.from("autumn").upload(path, file, {
upsert: true,
contentType,
@@ -23,13 +24,8 @@ export const uploadFile = async ({
return data;
};
export const getUploadUrl = async ({
sb,
path,
}: {
sb: SupabaseClient;
path: string;
}) => {
export const getUploadUrl = async ({ path }: { path: string }) => {
const sb = createSupabaseClient();
await sb.storage.from("autumn").remove([path]);
const { data, error } = await sb.storage

View File

@@ -1,29 +1,32 @@
import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js";
import { createSupabaseClient } from "../supabaseUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { safeSb } from "./safeSb.js";
export const subscribeToOrgUpdates = ({ db }: { db: DrizzleCli }) => {
try {
const sb = createSupabaseClient();
sb.channel("table-db-changes")
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "organizations",
},
async (payload) => {
// await clearOrgCache(payload.new.id);
try {
await clearOrgCache({ db, orgId: payload.new.id });
} catch (error) {
console.warn("Error clearing org cache:", error);
}
},
)
.subscribe();
} catch (error) {
console.warn("Error subscribing to org updates:", error);
}
};
export const subscribeToOrgUpdates = safeSb({
fn: ({ db }: { db: DrizzleCli }) => {
try {
const sb = createSupabaseClient();
sb.channel("table-db-changes")
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "organizations",
},
async (payload) => {
try {
await clearOrgCache({ db, orgId: payload.new.id });
} catch (error) {
console.warn("Error clearing org cache:", error);
}
},
)
.subscribe();
} catch (error) {
console.warn("Error subscribing to org updates:", error);
}
},
action: "subscribe to org updates",
});

68
server/src/external/svix/svixHelpers.ts vendored Normal file
View File

@@ -0,0 +1,68 @@
import { AppEnv, Organization } from "@autumn/shared";
import { createSvixCli, getSvixAppId, safeSvix } from "./svixUtils.js";
export const createSvixApp = safeSvix({
fn: async ({
name,
orgId,
env,
}: {
name: string;
orgId: string;
env: AppEnv;
}) => {
const svix = createSvixCli();
const app = await svix.application.create({
name,
metadata: {
org_id: orgId,
env,
},
});
return app;
},
action: "createSvixApp",
});
export const deleteSvixApp = safeSvix({
fn: async ({ appId }: { appId: string }) => {
const svix = createSvixCli();
await svix.application.delete(appId);
},
action: "deleteSvixApp",
});
export const sendSvixEvent = safeSvix({
fn: async ({
org,
env,
eventType,
data,
}: {
org: Organization;
env: AppEnv;
eventType: string;
data: any;
}) => {
const svix = createSvixCli();
return await svix.message.create(getSvixAppId({ org, env }), {
eventType,
payload: {
type: eventType,
data,
},
});
},
action: "sendSvixEvent",
});
export const getSvixDashboardUrl = safeSvix({
fn: async ({ org, env }: { org: Organization; env: AppEnv }) => {
const appId = getSvixAppId({ org, env });
const svix = createSvixCli();
const dashboard = await svix.authentication.appPortalAccess(appId, {});
return dashboard.url;
},
action: "getSvixDashboardUrl",
});

View File

@@ -1,11 +1,32 @@
import { AppEnv } from "@autumn/shared";
import { Organization } from "@autumn/shared";
import { Svix } from "svix";
import { logger } from "../logtail/logtailUtils.js";
export const createSvixCli = () => {
return new Svix(process.env.SVIX_API_KEY as string);
};
export function safeSvix<T extends (...args: any[]) => any>({
fn,
action,
}: {
fn: T;
action: string;
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
return async (...args: Parameters<T>) => {
if (!process.env.SVIX_API_KEY) {
logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
return;
}
try {
return await fn(...args);
} catch (error) {
logger.error(`Error ${action}: ${error}`);
}
};
}
export const getSvixAppId = ({
org,
env,
@@ -18,63 +39,3 @@ export const getSvixAppId = ({
? svixConfig.live_app_id
: svixConfig.sandbox_app_id;
};
export const createSvixApp = async ({
name,
orgId,
env,
}: {
name: string;
orgId: string;
env: AppEnv;
}) => {
const svix = createSvixCli();
const app = await svix.application.create({
name,
metadata: {
org_id: orgId,
env,
},
});
return app;
};
export const deleteSvixApp = async ({ appId }: { appId: string }) => {
const svix = createSvixCli();
await svix.application.delete(appId);
};
export const sendSvixEvent = async ({
org,
env,
eventType,
data,
}: {
org: Organization;
env: AppEnv;
eventType: string;
data: any;
}) => {
const svix = createSvixCli();
return await svix.message.create(getSvixAppId({ org, env }), {
eventType,
payload: {
type: eventType,
data,
},
});
};
export const getSvixDashboardUrl = async ({
org,
env,
}: {
org: Organization;
env: AppEnv;
}) => {
const appId = getSvixAppId({ org, env });
const svix = createSvixCli();
const dashboard = await svix.authentication.appPortalAccess(appId, {});
return dashboard.url;
};

View File

@@ -1,59 +0,0 @@
// import { AppEnv } from "@autumn/shared";
// import { Unkey } from "@unkey/api";
// const UNKEY_API_ID = "api_2fcMv43jiAbBySAgDubovfpVUABP";
// const createUnkeyCli = () => {
// return new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });
// };
// export const createKey = async ({
// env,
// name,
// ownerId,
// prefix,
// meta,
// }: {
// env: AppEnv;
// name: string;
// ownerId: string;
// prefix: string;
// meta: any;
// }) => {
// const unkey = createUnkeyCli();
// const key = await unkey.keys.create({
// apiId: UNKEY_API_ID,
// name,
// prefix,
// ownerId,
// meta,
// environment: env,
// });
// return key;
// };
// export const updateKey = async (keyId: string, meta: any) => {
// const unkey = createUnkeyCli();
// await unkey.keys.update({
// keyId,
// meta,
// });
// };
// export const deleteKey = async (keyId: string) => {
// const unkey = createUnkeyCli();
// await unkey.keys.delete({ keyId });
// };
// export const validateApiKey = async (apiKey: string) => {
// const unkey = createUnkeyCli();
// const { result, error } = await unkey.keys.verify({
// apiId: UNKEY_API_ID,
// key: apiKey,
// });
// if (error || !result.valid) {
// throw new Error("Invalid API key");
// }
// return result;
// };

View File

@@ -2,229 +2,96 @@ import { Request, Response } from "express";
import { Webhook } from "svix";
import { OrgService } from "@/internal/orgs/OrgService.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { SupabaseClient } from "@supabase/supabase-js";
import { createClerkCli } from "../clerkUtils.js";
import { sendOnboardingEmail } from "./sendOnboardingEmail.js";
import { AppEnv } from "autumn-js";
import { deleteSvixApp } from "../svix/svixUtils.js";
import {
deleteStripeWebhook,
initOrgSvixApps,
} from "@/internal/orgs/orgUtils.js";
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
import { deleteStripeWebhook } from "@/internal/orgs/orgUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { constructOrg } from "@/internal/orgs/orgUtils.js";
import { createOnboardingProducts } from "@/internal/orgs/onboarding/createOnboardingProducts.js";
import { eq } from "drizzle-orm";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { Organization, organizations } from "@autumn/shared";
import { generatePublishableKey } from "@/utils/encryptUtils.js";
const verifyClerkWebhook = async (req: Request, res: Response) => {
const wh = new Webhook(process.env.CLERK_SIGNING_SECRET!);
// const verifyClerkWebhook = async (req: Request, res: Response) => {
// const wh = new Webhook(process.env.CLERK_SIGNING_SECRET!);
const headers = req.headers;
const payload = req.body;
// const headers = req.headers;
// const payload = req.body;
const svix_id = headers["svix-id"];
const svix_timestamp = headers["svix-timestamp"];
const svix_signature = headers["svix-signature"];
// const svix_id = headers["svix-id"];
// const svix_timestamp = headers["svix-timestamp"];
// const svix_signature = headers["svix-signature"];
if (!svix_id || !svix_timestamp || !svix_signature) {
res.status(400).json({
success: false,
message: "Error: Missing svix headers",
});
return;
}
// if (!svix_id || !svix_timestamp || !svix_signature) {
// res.status(400).json({
// success: false,
// message: "Error: Missing svix headers",
// });
// return;
// }
let evt: any;
try {
evt = wh.verify(payload, {
"svix-id": svix_id as string,
"svix-timestamp": svix_timestamp as string,
"svix-signature": svix_signature as string,
});
} catch (err) {
console.log("Error: Could not verify webhook");
res.status(400).json({
success: false,
message: "Error: Could not verify webhook",
});
return;
}
// let evt: any;
// try {
// evt = wh.verify(payload, {
// "svix-id": svix_id as string,
// "svix-timestamp": svix_timestamp as string,
// "svix-signature": svix_signature as string,
// });
// } catch (err) {
// console.log("Error: Could not verify webhook");
// res.status(400).json({
// success: false,
// message: "Error: Could not verify webhook",
// });
// return;
// }
return evt;
};
// return evt;
// };
export const handleClerkWebhook = async (req: any, res: any) => {
let event = await verifyClerkWebhook(req, res);
// export const handleClerkWebhook = async (req: any, res: any) => {
// let event = await verifyClerkWebhook(req, res);
if (!event) {
return;
}
// if (!event) {
// return;
// }
const eventType = event.type;
const eventData = event.data;
// const eventType = event.type;
// const eventData = event.data;
try {
switch (eventType) {
case "organization.created":
await saveOrgToDB({
db: req.db,
id: eventData.id,
slug: eventData.slug,
createdAt: eventData.created_at,
});
break;
// try {
// switch (eventType) {
// case "organization.created":
// await saveOrgToDB({
// db: req.db,
// id: eventData.id,
// slug: eventData.slug,
// createdAt: eventData.created_at,
// });
// break;
case "organization.deleted":
await handleOrgDeleted({
db: req.db,
eventData,
});
break;
// case "organization.deleted":
// await handleOrgDeleted({
// db: req.db,
// eventData,
// });
// break;
default:
break;
}
} catch (error) {
handleRequestError({
req,
error,
res,
action: "Handle Clerk Webhook",
});
return;
}
// default:
// break;
// }
// } catch (error) {
// handleRequestError({
// req,
// error,
// res,
// action: "Handle Clerk Webhook",
// });
// return;
// }
return void res.status(200).json({
success: true,
message: "Webhook received",
});
};
export const saveOrgToDB = async ({
db,
id,
slug,
createdAt,
}: {
db: DrizzleCli;
id: string;
slug: string;
createdAt: Date;
}) => {
console.log(`Handling organization.created: ${slug} (${id})`);
try {
await OrgService.update({
db,
orgId: id,
updates: {
created_at: createdAt.getTime(),
},
});
// 1. Create svix webhoooks
const { sandboxApp, liveApp } = await initOrgSvixApps({
slug,
id,
});
await OrgService.update({
db,
orgId: id,
updates: {
svix_config: { sandbox_app_id: sandboxApp.id, live_app_id: liveApp.id },
test_pkey: generatePublishableKey(AppEnv.Sandbox),
live_pkey: generatePublishableKey(AppEnv.Live),
},
});
console.log(`Created svix webhooks for org ${id}`);
} catch (error: any) {
if (error?.data && error.data.code == "23505") {
console.error(
`Org ${id} already exists in Supabase -- skipping creationg`,
);
return;
}
console.error(
`Failed to insert org. Code: ${error.code}, message: ${error.message}`,
);
return;
}
};
const handleOrgDeleted = async ({
db,
eventData,
}: {
db: DrizzleCli;
eventData: any;
}) => {
// 1. Delete svix webhooks
try {
console.log(`Handling organization.deleted: (${eventData.id})`);
const org = (await db.query.organizations.findFirst({
where: eq(organizations.id, eventData.id),
})) as unknown as Organization;
if (!org) {
throw new RecaseError({
message: `Clerk webhook, tried deleting org ${eventData.slug} but not found`,
code: "org_not_found",
statusCode: 404,
});
}
console.log("1. Deleting svix webhooks");
const batch = [];
if (org.svix_config?.sandbox_app_id) {
batch.push(
deleteSvixApp({
appId: org.svix_config.sandbox_app_id,
}),
);
}
if (org.svix_config?.live_app_id) {
batch.push(
deleteSvixApp({
appId: org.svix_config.live_app_id,
}),
);
}
await Promise.all(batch);
// 2. Delete stripe webhooks
console.log("2. Deleting stripe webhooks");
if (org.stripe_config) {
await deleteStripeWebhook({
org: org,
env: AppEnv.Sandbox,
});
await deleteStripeWebhook({
org: org,
env: AppEnv.Live,
});
}
// 3. Delete org
console.log("3. Deleting org");
await OrgService.delete({
db,
orgId: eventData.id,
});
console.log(`Deleted org ${org.slug} (${org.id})`);
} catch (error) {
console.log("Failed to delete organization", error);
return;
}
};
// return void res.status(200).json({
// success: true,
// message: "Webhook received",
// });
// };

View File

@@ -1,6 +1,5 @@
import express from "express";
import bodyParser from "body-parser";
import { handleClerkWebhook } from "./clerkWebhooks.js";
import { stripeWebhookRouter } from "../stripe/stripeWebhooks.js";
import { autumnWebhookRouter } from "../autumn/autumnWebhookRouter.js";
@@ -10,10 +9,4 @@ webhooksRouter.use("/stripe", stripeWebhookRouter);
webhooksRouter.use("/autumn", autumnWebhookRouter);
webhooksRouter.post(
"/clerk",
bodyParser.raw({ type: "application/json" }),
handleClerkWebhook
);
export default webhooksRouter;

View File

@@ -9,32 +9,24 @@ import cors from "cors";
import chalk from "chalk";
import http from "http";
import { apiRouter } from "./internal/api/apiRouter.js";
import webhooksRouter from "./external/webhooks/webhooksRouter.js";
import { initLogger } from "./errors/logger.js";
import { apiRouter } from "./internal/api/apiRouter.js";
import { QueueManager } from "./queue/QueueManager.js";
import { AppEnv } from "@autumn/shared";
import { createSupabaseClient } from "./external/supabaseUtils.js";
import {
createLogtail,
createLogtailAll,
} from "./external/logtail/logtailUtils.js";
import { createLogtail } from "./external/logtail/logtailUtils.js";
import { CacheManager } from "./external/caching/CacheManager.js";
import { logtailAll, logger } from "./external/logtail/logtailUtils.js";
import { createPosthogCli } from "./external/posthog/createPosthogCli.js";
import { generateId } from "./utils/genUtils.js";
import { subscribeToOrgUpdates } from "./external/supabase/subscribeToOrgUpdates.js";
import { client, db } from "./db/initDrizzle.js";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./utils/auth.js";
import { logtail as logtailAll } from "./external/logtail/logtailUtils.js";
if (!process.env.DATABASE_URL) {
console.error(`DATABASE_URL is not set`);
process.exit(1);
}
import { checkEnvVars } from "./utils/initUtils.js";
checkEnvVars();
const init = async () => {
const app = express();
@@ -45,6 +37,7 @@ const init = async () => {
"http://localhost:3000",
"https://app.useautumn.com",
"https://*.useautumn.com",
process.env.CLIENT_URL || "",
],
credentials: true,
allowedHeaders: [
@@ -69,32 +62,21 @@ const init = async () => {
app.all("/api/auth/*", toNodeHandler(auth));
const logger = initLogger();
const server = http.createServer(app);
const posthog = createPosthogCli();
server.keepAliveTimeout = 120000; // 120 seconds
server.headersTimeout = 120000; // 120 seconds should be >= keepAliveTimeout
await QueueManager.getInstance(); // initialize the queue manager
await CacheManager.getInstance();
const supabaseClient = createSupabaseClient();
// const { db } = initDrizzle();
// Optional services
// const logtailAll = createLogtailAll();
const posthog = createPosthogCli();
subscribeToOrgUpdates({ db });
app.use((req: any, res: any, next: any) => {
req.sb = supabaseClient;
req.db = db;
req.logger = logger;
req.logtailAll = logtailAll;
req.env = req.env = req.headers["app_env"] || AppEnv.Sandbox;
req.db = db;
req.logtailAll = logtailAll;
req.posthog = posthog;
req.id = req.headers["rndr-id"] || generateId("local_req");
@@ -105,7 +87,7 @@ const init = async () => {
headersClone.authorization = undefined;
headersClone.Authorization = undefined;
logtailAll.info(`${req.method} ${req.originalUrl}`, {
logtailAll?.info(`${req.method} ${req.originalUrl}`, {
url: req.originalUrl,
method: req.method,
headers: headersClone,
@@ -113,9 +95,8 @@ const init = async () => {
});
req.logtail = createLogtail();
req.logger = req.logtail;
} catch (error) {
req.logtail = logtailAll; // fallback
req.logtail = logger; // fallback
console.error(`Error creating req.logtail`);
console.error(error);
}
@@ -174,14 +155,7 @@ if (process.env.NODE_ENV === "development") {
}
cluster.on("exit", (worker, code, signal) => {
try {
let logtail = createLogtail();
logtail.error(`WORKER DIED: ${worker.process.pid}`);
logtail.flush();
} catch (error) {
console.log("Error sending log to logtail", error);
}
// LOG in Render
logger.error(`WORKER DIED: ${worker.process.pid}`);
cluster.fork();
});
} else {

View File

@@ -8,7 +8,7 @@ import {
Organization,
} from "@autumn/shared";
import { sendSvixEvent } from "../../../external/svix/svixUtils.js";
import { sendSvixEvent } from "@/external/svix/svixHelpers.js";
import { CusService } from "@/internal/customers/CusService.js";
import { getCustomerDetails } from "@/internal/customers/cusUtils/getCustomerDetails.js";
@@ -205,7 +205,7 @@ export const handleProductsUpdated = async ({
}
// 2. Send Svix event
const res = await sendSvixEvent({
await sendSvixEvent({
org,
env,
eventType: "customer.products.updated",

View File

@@ -189,194 +189,6 @@ export class CusService {
return customer as Customer;
}
//search customers
static addPaginationAndSearch = ({
query,
search,
pageNumber,
pageSize,
lastItem,
customerPrefix = "",
}: {
query: any;
search: string;
pageNumber: number | null;
pageSize: number;
lastItem: any;
customerPrefix: string;
}) => {
if (search && search !== "") {
query.or(
`"name".ilike.%${search}%, ` +
`"email".ilike.%${search}%, ` +
`"id".ilike.%${search}%`,
customerPrefix && {
foreignTable: "customers",
referencedTable: "customers",
},
);
}
if (lastItem) {
query.or(
`"internal_id".lt.${lastItem.internal_id}`,
// `"created_at".lt.${lastItem.created_at},` +
// `and("created_at".eq.${lastItem.created_at},"internal_id".gt.${lastItem.internal_id})`,
customerPrefix && {
foreignTable: "customers",
referencedTable: "customers",
},
);
}
if (customerPrefix) {
query.order(`customer(internal_id)`, { ascending: false });
} else {
query.order("internal_id", { ascending: false });
// query
// .order("created_at", { ascending: false })
// .order("internal_id", { ascending: true });
}
query.limit(pageSize);
};
static async searchCustomersByProduct({
sb,
orgId,
env,
search,
filters,
pageSize,
lastItem,
pageNumber,
}: {
sb: SupabaseClient;
orgId: string;
env: AppEnv;
search: string;
filters: any;
pageSize: number;
lastItem: any;
pageNumber: number;
}) {
const query = sb
.from("customer_products")
.select(
`*,
customer:customers!inner(*), product:products!inner(id, name, version)`,
{
count: "exact",
},
)
.eq("customer.org_id", orgId)
.eq("customer.env", env)
.in("status", [CusProductStatus.Active, CusProductStatus.PastDue]);
if (filters.product_id) {
query.eq("product.id", filters.product_id);
}
if (filters?.status === "canceled") {
console.log("Adding canceled filter");
query
.eq("status", CusProductStatus.Active)
.not("canceled_at", "is", null);
} else if (filters?.status === "free_trial") {
console.log("Adding free trial filter");
query
.eq("status", CusProductStatus.Active)
.gt("trial_ends_at", Date.now());
}
this.addPaginationAndSearch({
query,
search,
pageNumber,
pageSize,
lastItem,
customerPrefix: "customers.",
});
const { data, count, error } = await query;
if (error) {
throw error;
}
// Flip
const customers = flipProductResults(data);
return { data: customers, count };
}
static async searchCustomers({
sb,
orgId,
env,
search,
pageSize = 50,
filters,
lastItem,
pageNumber,
}: {
sb: SupabaseClient;
orgId: string;
env: AppEnv;
search: string;
lastItem?: { created_at: string; name: string; internal_id: string } | null;
filters: any;
pageSize?: number;
pageNumber: number;
}) {
if (filters.product_id || filters.status) {
return await this.searchCustomersByProduct({
sb,
orgId,
env,
search,
filters,
pageSize,
lastItem,
pageNumber,
});
}
let select =
"*, customer_products:customer_products(*, product:products(*))";
let query = sb
.from("customers")
.select(select, {
count: "exact",
// count: "planned", // use for 1M rows...?
})
.eq("org_id", orgId)
.eq("env", env);
this.addPaginationAndSearch({
query,
search,
pageNumber: null,
pageSize,
lastItem,
customerPrefix: "",
});
const { data, count, error } = await query;
if (error) {
throw error;
}
const totalCount = count && count + pageSize * (pageNumber - 1);
return { data, count: totalCount };
}
// End of search customers
static async insert({ db, data }: { db: DrizzleCli; data: Customer }) {
try {
const results = await db
@@ -469,192 +281,3 @@ export class CusService {
return results;
}
}
// static async getWithProductsDrizzle({
// db,
// idOrInternalId,
// orgId,
// env,
// inStatuses = [
// CusProductStatus.Active,
// CusProductStatus.PastDue,
// CusProductStatus.Scheduled,
// ],
// withEntities = false,
// entityId,
// expand,
// withSubs = false,
// }: {
// db: DrizzleCli;
// idOrInternalId: string;
// orgId: string;
// env: AppEnv;
// inStatuses?: CusProductStatus[];
// withEntities?: boolean;
// entityId?: string;
// expand?: (CusExpand | EntityExpand)[];
// withSubs?: boolean;
// }) {
// // 1. Call RPC function
// let data: {
// customer: Customer | null;
// products: FullCusProduct[] | null;
// entities: Entity[] | null;
// entity: Entity | null;
// trials_used: any[] | null;
// subscriptions: any[] | null;
// invoices: any[] | null;
// };
// try {
// const result = await db.execute(sql`
// SELECT * FROM get_cus_with_products(
// p_cus_id => ${idOrInternalId}::text,
// p_org_id => ${orgId}::text,
// p_env => ${env}::text,
// p_statuses => ARRAY[${sql.join(
// inStatuses.map((status) => sql`${status}`),
// sql`, `,
// )}]::text[],
// p_with_entities => ${withEntities}::boolean,
// p_entity_id => ${entityId || null}::text,
// p_with_trials_used => ${expand?.includes(CusExpand.TrialsUsed) || false}::boolean,
// p_with_subs => ${withSubs}::boolean,
// p_with_invoices => ${expand?.includes(CusExpand.Invoices) || false}::boolean
// )
// `);
// if (!result || result.length == 0 || !result[0].get_cus_with_products) {
// throw new RecaseError({
// message: "Calling get_cus_with_products RPC returned wrong shape",
// code: ErrCode.GetCusWithProductsFailed,
// statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
// data: result,
// });
// }
// data = result[0].get_cus_with_products as any;
// } catch (error) {
// throw error;
// }
// if (!data || !data.customer) {
// return null;
// }
// let { customer, products, entities, entity } = data;
// if (!products) {
// products = [];
// }
// for (let product of products) {
// if (!product.customer_prices) {
// product.customer_prices = [];
// }
// if (!product.customer_entitlements) {
// product.customer_entitlements = [];
// }
// }
// let trialsUsed = data.trials_used;
// if (trialsUsed) {
// trialsUsed = trialsUsed.filter(
// (trial: any, index: number, self: any) =>
// index ===
// self.findIndex((t: any) => t.product_id === trial.product_id),
// );
// }
// return {
// ...customer,
// customer_products: products,
// entities: entities,
// entity: entity,
// trials_used: trialsUsed,
// subscriptions: data.subscriptions,
// invoices: data.invoices,
// } as FullCustomer;
// }
// static async getWithProducts({
// sb,
// idOrInternalId,
// orgId,
// env,
// inStatuses = [
// CusProductStatus.Active,
// CusProductStatus.PastDue,
// CusProductStatus.Scheduled,
// ],
// withEntities = false,
// entityId,
// expand,
// withSubs = false,
// }: {
// sb: SupabaseClient;
// idOrInternalId: string;
// orgId: string;
// env: AppEnv;
// inStatuses?: CusProductStatus[];
// withEntities?: boolean;
// entityId?: string;
// expand?: (CusExpand | EntityExpand)[];
// withSubs?: boolean;
// }) {
// const { data, error } = await sb.rpc("get_cus_with_products", {
// p_cus_id: idOrInternalId,
// p_org_id: orgId,
// p_env: env,
// p_statuses: inStatuses,
// p_with_entities: withEntities,
// p_entity_id: entityId,
// p_with_trials_used: expand?.includes(CusExpand.TrialsUsed),
// p_with_subs: withSubs,
// p_with_invoices: expand?.includes(CusExpand.Invoices),
// });
// if (error) {
// throw error;
// }
// if (!data || !data.customer) {
// return null;
// }
// let { customer, products, entities, entity } = data;
// if (!products) {
// products = [];
// }
// for (let product of products) {
// if (!product.customer_prices) {
// product.customer_prices = [];
// }
// if (!product.customer_entitlements) {
// product.customer_entitlements = [];
// }
// }
// let trialsUsed = data.trials_used;
// if (trialsUsed) {
// trialsUsed = trialsUsed.filter(
// (trial: any, index: number, self: any) =>
// index ===
// self.findIndex((t: any) => t.product_id === trial.product_id),
// );
// }
// return {
// ...customer,
// customer_products: products,
// entities: entities,
// entity: entity,
// trials_used: trialsUsed,
// subscriptions: data.subscriptions,
// invoices: data.invoices,
// };
// }

View File

@@ -115,32 +115,3 @@ export class CachedKeyService {
}
}
}
// const { data, error } = await sb.rpc("verify_api_key", {
// p_hashed_key: hashedKey,
// p_env: env,
// });
// if (error) {
// throw error;
// }
// if (!data.success || !data.organization) {
// console.warn(`(warning) failed to verify secret key: ${data.error}`);
// return null;
// }
// let org = structuredClone(data.organization);
// delete org.features;
// // Add org config and api version
// org.config = OrgConfigSchema.parse(org.config || {});
// org.api_version = getApiVersion({
// createdAt: org.created_at,
// });
// return {
// org,
// features: data.organization?.features || [],
// env,
// };

View File

@@ -4,7 +4,7 @@ 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/svixUtils.js";
import { getSvixDashboardUrl } from "@/external/svix/svixHelpers.js";
import { handleRequestError } from "@/utils/errorUtils.js";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { CacheType } from "@/external/caching/cacheActions.js";

View File

@@ -0,0 +1,40 @@
import { sendTextEmail } from "@/external/resend/resendUtils.js";
import { safeResend } from "@/external/resend/safeResend.js";
const getInvitationEmailBody = ({
orgName,
inviteLink,
}: {
orgName: string;
inviteLink: string;
}) => {
return `Hey there! You've been invited to join ${orgName} on Autumn.
Click the link below to create an account / sign in and you'll be automatically added to the organization.
${process.env.CLIENT_URL}/sign-in
`;
};
export const sendInvitationEmail = safeResend({
fn: async ({
email,
orgName,
inviteLink,
}: {
email: string;
orgName: string;
inviteLink: string;
}) => {
console.log("Sending invitation email to", email);
await sendTextEmail({
from: `Autumn <hey@${process.env.RESEND_DOMAIN}>`,
to: email,
subject: `Join ${orgName} on Autumn`,
body: getInvitationEmailBody({ orgName, inviteLink }),
});
},
action: "send org invitation email",
});

View File

@@ -0,0 +1,49 @@
import { MigrationService } from "../migrations/MigrationService.js";
import { sendTextEmail } from "@/external/resend/resendUtils.js";
import { MigrationJobStep, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { safeResend } from "@/external/resend/safeResend.js";
export const sendMigrationEmail = safeResend({
fn: async ({
db,
migrationJobId,
org,
}: {
db: DrizzleCli;
migrationJobId: string;
org: Organization;
}) => {
let migrationJob = await MigrationService.getJob({
db,
id: migrationJobId,
});
// Send email
let getCustomersStep =
migrationJob.step_details[MigrationJobStep.GetCustomers];
let migrateStep =
migrationJob.step_details[MigrationJobStep.MigrateCustomers];
console.log("Sending migration email");
await sendTextEmail({
to: "johnyeocx@gmail.com",
subject: `Migration Job Finished -- ${migrationJob.id}`,
body: `
ORG: ${org.id}, ${org.slug}
Step: Get migration customers
1. Total customers: ${getCustomersStep?.total_customers}
2. Canceled customers: ${getCustomersStep?.canceled_customers}
Step: Migrate customers
1. Number of errors: ${migrateStep?.num_errors}
2. Failed customers:
${migrateStep?.failed_customers}
`,
});
},
action: "send migration email",
});

View File

@@ -1,9 +1,14 @@
import { logger } from "@/external/logtail/logtailUtils.js";
import { createResendCli } from "@/external/resend/resendUtils.js";
import OTPEmail from "@emails/OTPEmail.js";
const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => {
const resend = createResendCli();
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
logger.warn(`RESEND NOT SET UP, SIGN IN OTP: ${otp}`);
return;
}
const resend = createResendCli();
await resend.emails.send({
from: `Autumn <hey@${process.env.RESEND_DOMAIN}>`,
to: email,

View File

@@ -1,5 +1,5 @@
import { ClerkClient } from "@clerk/express";
import { sendHtmlEmail, sendTextEmail } from "../resend/resendUtils.js";
import { sendHtmlEmail } from "@/external/resend/resendUtils.js";
import { safeResend } from "@/external/resend/safeResend.js";
const getWelcomeEmailBody = (userFirstName: string) => {
return `
@@ -18,20 +18,15 @@ Co-founder, Autumn</p>
`;
};
export const sendOnboardingEmail = async ({
name,
email,
}: {
name: string;
email: string;
}) => {
console.log("Sending onboarding email to", email);
export const sendOnboardingEmail = safeResend({
fn: async ({ name, email }: { name: string; email: string }) => {
const firstName = name.split(" ")[0];
const firstName = name.split(" ")[0];
await sendHtmlEmail({
to: email,
subject: "Anything I can help with?",
body: getWelcomeEmailBody(firstName),
});
};
await sendHtmlEmail({
to: email,
subject: "Anything I can help with?",
body: getWelcomeEmailBody(firstName),
});
},
action: "send onboarding email",
});

View File

@@ -159,6 +159,13 @@ export const runSaveFeatureDisplayTask = async ({
}) => {
let display;
try {
if (!process.env.ANTHROPIC_API_KEY) {
logger.warn(
"ANTHROPIC_API_KEY is not set, skipping feature display generation",
);
return;
}
logger.info(
`Generating feature display for ${feature.id} (org: ${org.slug})`,
);

View File

@@ -12,7 +12,6 @@ import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js";
import { handlePostOrg } from "./orgs/handlers/handlePostOrg.js";
import { Autumn } from "autumn-js";
import { autumnHandler } from "autumn-js/express";
import { parseAuthHeader } from "@/utils/authUtils.js";
import { withAdminAuth } from "./admin/withAdminAuth.js";
import { adminRouter } from "./admin/adminRouter.js";
@@ -23,10 +22,8 @@ mainRouter.get("", async (req: any, res) => {
});
mainRouter.post("/organization", withAuth, handlePostOrg);
mainRouter.use("/admin", withAdminAuth, adminRouter);
mainRouter.use("/users", withAuth, userRouter);
mainRouter.use("/onboarding", withOrgAuth, onboardingRouter);
mainRouter.use("/organization", withOrgAuth, orgRouter);
mainRouter.use("/features", withOrgAuth, featureRouter);
@@ -58,7 +55,6 @@ mainRouter.use(
withOrgAuth,
autumnHandler({
autumn: (req: any) => {
console.log("Instantiating Autumn...");
let client = new Autumn({
url: "http://localhost:8080/v1",
headers: req.headers,

View File

@@ -11,7 +11,7 @@ import { MigrationService } from "../MigrationService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { migrateCustomer } from "./migrateCustomer.js";
import { sendMigrationEmail } from "./sendMigrationEmail.js";
import { sendMigrationEmail } from "../../emails/sendMigrationEmail.js";
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
import { DrizzleCli } from "@/db/initDrizzle.js";

View File

@@ -1,45 +0,0 @@
import { MigrationService } from "../MigrationService.js";
import { sendTextEmail } from "@/external/resend/resendUtils.js";
import { MigrationJobStep, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const sendMigrationEmail = async ({
db,
migrationJobId,
org,
}: {
db: DrizzleCli;
migrationJobId: string;
org: Organization;
}) => {
let migrationJob = await MigrationService.getJob({
db,
id: migrationJobId,
});
// Send email
let getCustomersStep =
migrationJob.step_details[MigrationJobStep.GetCustomers];
let migrateStep =
migrationJob.step_details[MigrationJobStep.MigrateCustomers];
console.log("Sending migration email");
await sendTextEmail({
to: "johnyeocx@gmail.com",
subject: `Migration Job Finished -- ${migrationJob.id}`,
body: `
ORG: ${org.id}, ${org.slug}
Step: Get migration customers
1. Total customers: ${getCustomersStep?.total_customers}
2. Canceled customers: ${getCustomersStep?.canceled_customers}
Step: Migrate customers
1. Number of errors: ${migrateStep?.num_errors}
2. Failed customers:
${migrateStep?.failed_customers}
`,
});
};

View File

@@ -1,35 +0,0 @@
import { sendTextEmail } from "@/external/resend/resendUtils.js";
const getInvitationEmailBody = ({
orgName,
inviteLink,
}: {
orgName: string;
inviteLink: string;
}) => {
return `Hey there! You've been invited to join ${orgName} on Autumn.
Click the link below to create an account / sign in and you'll be automatically added to the organization.
${process.env.CLIENT_URL}/sign-in
`;
};
export const sendInvitationEmail = async ({
email,
orgName,
inviteLink,
}: {
email: string;
orgName: string;
inviteLink: string;
}) => {
console.log("Sending invitation email to", email);
await sendTextEmail({
to: email,
subject: `Join ${orgName} on Autumn`,
body: getInvitationEmailBody({ orgName, inviteLink }),
});
};

View File

@@ -1,12 +1,10 @@
import { deleteSvixApp } from "@/external/svix/svixUtils.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 { Request, Response } from "express";
import { Response } from "express";
import { deleteStripeWebhook } from "../orgUtils.js";
import { OrgService } from "../OrgService.js";
import { auth } from "@/utils/auth.js";
const deleteSvixWebhooks = async ({
org,

View File

@@ -1,22 +1,24 @@
import { logger } from "@/external/logtail/logtailUtils.js";
import { getUploadUrl } from "@/external/supabase/storageUtils.js";
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
import { ErrCode } from "@autumn/shared";
export const handleGetUploadUrl = async (req: any, res: any) => {
try {
const { org, db, sb } = req;
const { org } = req;
if (!sb) {
throw new RecaseError({
message: "Supabase not initialized, can't get signed URL",
code: ErrCode.SupabaseNotFound,
});
}
// Get signed URL
let path = `logo/${org.id}`;
const data = await getUploadUrl({ sb, path });
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
logger.warn("Supabase storage not set up");
res.status(400).json({
message: "Supabase storage not set up",
code: ErrCode.SupabaseNotFound,
});
return;
}
const data = await getUploadUrl({ path });
res.status(200).json(data);
} catch (error) {

View File

@@ -1,16 +1,16 @@
import express from "express";
import Stripe from "stripe";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { encryptData } from "@/utils/encryptUtils.js";
import { ErrCode } from "@/errors/errCodes.js";
import { createClerkCli } from "@/external/clerkUtils.js";
import {
checkKeyValid,
createWebhookEndpoint,
} from "@/external/stripe/stripeOnboardingUtils.js";
import { encryptData } from "@/utils/encryptUtils.js";
import RecaseError, {
handleFrontendReqError,
handleRequestError,
} from "@/utils/errorUtils.js";
import express from "express";
import Stripe from "stripe";
import { OrgService } from "./OrgService.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { AppEnv } from "@autumn/shared";

View File

@@ -1,10 +1,10 @@
import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { AppEnv, ErrCode, FrontendOrg, Organization } from "@autumn/shared";
import { createSvixApp } from "@/external/svix/svixUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { OrgService } from "./OrgService.js";
import { FeatureService } from "../features/FeatureService.js";
import { createSvixApp } from "@/external/svix/svixHelpers.js";
export const constructOrg = ({ id, slug }: { id: string; slug: string }) => {
return {
@@ -23,33 +23,6 @@ export const constructOrg = ({ id, slug }: { id: string; slug: string }) => {
config: {} as any,
};
};
export const initOrgSvixApps = async ({
id,
slug,
}: {
id: string;
slug: string;
}) => {
const batchCreate = [];
batchCreate.push(
createSvixApp({
name: `${slug}_${AppEnv.Sandbox}`,
orgId: id,
env: AppEnv.Sandbox,
}),
);
batchCreate.push(
createSvixApp({
name: `${slug}_${AppEnv.Live}`,
orgId: id,
env: AppEnv.Live,
}),
);
const [sandboxApp, liveApp] = await Promise.all(batchCreate);
return { sandboxApp, liveApp };
};
export const deleteStripeWebhook = async ({
org,
@@ -115,7 +88,6 @@ export const createOrgResponse = (org: Organization): FrontendOrg => {
created_at: new Date(org.createdAt).getTime(),
test_pkey: org.test_pkey,
live_pkey: org.live_pkey,
onboarded: org.onboarded || false,
};
};

View File

@@ -252,17 +252,5 @@ export class RewardProgramService {
);
return result[0].count;
// const { data, error, count } = await sb
// .from("reward_redemptions")
// .select("*, reward_program:reward_programs!inner(*)", { count: "exact" })
// .eq("referral_code_id", referralCodeId)
// .eq("triggered", true);
// if (error) {
// throw error;
// }
// return count;
}
}

View File

@@ -72,41 +72,6 @@ export class RewardRedemptionService {
});
return data as any;
// let query = sb
// .from("reward_redemptions")
// .select(
// `
// *
// ${
// withRewardProgram
// ? ", reward_program:reward_programs!inner(*, reward:rewards!inner(*))"
// : ""
// }
// ${withReferralCode ? ", referral_code:referral_codes!inner(*)" : ""}
// `,
// )
// .eq("internal_customer_id", internalCustomerId);
// if (notNullish(internalRewardProgramId)) {
// query = query.eq("internal_reward_program_id", internalRewardProgramId);
// }
// if (notNullish(triggered)) {
// query = query.eq("triggered", triggered);
// }
// if (notNullish(limit)) {
// query = query.limit(limit);
// }
// const { data, error } = await query;
// if (error) {
// throw error;
// }
// return data;
}
static async getByReferrer({
@@ -142,23 +107,6 @@ export class RewardRedemptionService {
}));
return processed;
// const { data, error } = await sb
// .from("reward_redemptions")
// .select(
// `
// *, referral_code:referral_codes!inner(*)
// ${withCustomer ? ", customer:customers!inner(*)" : ""}
// `,
// )
// .eq("referral_code.internal_customer_id", internalCustomerId)
// .limit(limit);
// if (error) {
// throw error;
// }
// return data;
}
static async insert({

View File

@@ -8,40 +8,44 @@ const handleResFinish = (req: any, res: any, logtailContext: any) => {
if (skipUrls.includes(req.originalUrl)) {
return;
}
req.logtailAll.info(
`[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.org?.slug})`,
{
req: {
...logtailContext,
// Only log to logtailAll if it exists
if (req.logtailAll) {
req.logtailAll.info(
`[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.org?.slug})`,
{
req: {
...logtailContext,
},
statusCode: res.statusCode,
res: res.locals.responseBody,
},
statusCode: res.statusCode,
res: res.locals.responseBody,
},
);
req.logtailAll.flush();
);
req.logtailAll.flush();
}
} catch (error) {
console.error("Failed to log response to logtailAll");
console.error(error);
}
// Post hog
let posthogUrls = ["/v1/attach"];
if (req.posthog && posthogUrls.includes(req.originalUrl)) {
posthogCapture({
posthog: req.posthog,
params: {
distinctId: req.org?.id,
event: `${req.method} ${req.originalUrl}`,
properties: {
authType: req.auth,
orgSlug: req.org?.slug,
statusCode: res.statusCode,
res: res.locals.responseBody,
req: req.body,
},
},
});
}
// Save to PostHog
// let posthogUrls = ["/v1/attach"];
// if (req.posthog && posthogUrls.includes(req.originalUrl)) {
// posthogCapture({
// posthog: req.posthog,
// params: {
// distinctId: req.org?.id,
// event: `${req.method} ${req.originalUrl}`,
// properties: {
// authType: req.auth,
// orgSlug: req.org?.slug,
// statusCode: res.statusCode,
// res: res.locals.responseBody,
// req: req.body,
// },
// },
// });
// }
};
export const analyticsMiddleware = async (req: any, res: any, next: any) => {

View File

@@ -8,6 +8,7 @@ export const verifySecretKey = async (req: any, res: any, next: any) => {
const authHeader =
req.headers["authorization"] || req.headers["Authorization"];
const logger = req.logtail;
const version = req.headers["x-api-version"];
if (version) {
@@ -56,7 +57,6 @@ export const verifySecretKey = async (req: any, res: any, next: any) => {
// Try verify via Autumn
let logger = req.logtail;
try {
const { valid, data } = await verifyKey({
db: req.db,

View File

@@ -92,8 +92,6 @@ export const withOrgAuth = async (req: any, res: any, next: NextFunction) => {
};
export const withAuth = async (req: any, res: any, next: NextFunction) => {
// const tokenData = await getTokenData(req, res);
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});

View File

@@ -1,14 +1,15 @@
import { db } from "@/db/initDrizzle.js";
import { saveOrgToDB } from "@/external/webhooks/clerkWebhooks.js";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { sendInvitationEmail } from "@/internal/orgs/emails/sendInvitationEmail.js";
import { sendInvitationEmail } from "@/internal/emails/sendInvitationEmail.js";
import { beforeSessionCreated } from "./authUtils/beforeSessionCreated.js";
import { betterAuth } from "better-auth";
import { emailOTP, admin, organization } from "better-auth/plugins";
import sendOTPEmail from "@/internal/orgs/emails/sendOTPEmail.js";
import { sendOnboardingEmail } from "@/external/webhooks/sendOnboardingEmail.js";
import sendOTPEmail from "@/internal/emails/sendOTPEmail.js";
import { sendOnboardingEmail } from "@/internal/emails/sendOnboardingEmail.js";
import { ADMIN_USER_IDs } from "./constants.js";
import { afterOrgCreated } from "./authUtils/afterOrgCreated.js";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // or "mysql", "sqlite"
@@ -55,6 +56,7 @@ export const auth = betterAuth({
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
// Implement the sendVerificationOTP method to send the OTP to the user's email address
await sendOTPEmail({
email,
otp,
@@ -86,13 +88,8 @@ export const auth = betterAuth({
organizationCreation: {
disabled: false,
afterCreate: async ({ organization, user }) => {
await saveOrgToDB({
db,
id: organization.id,
slug: organization.slug,
createdAt: organization.createdAt,
});
afterCreate: async ({ organization }) => {
await afterOrgCreated({ org: organization as any });
},
},
}),

View File

@@ -0,0 +1,83 @@
import { db } from "@/db/initDrizzle.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { AppEnv } from "@autumn/shared";
import { generatePublishableKey } from "../encryptUtils.js";
import { createSvixApp } from "@/external/svix/svixHelpers.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import { Organization } from "better-auth/plugins";
export const initOrgSvixApps = async ({
id,
slug,
}: {
id: string;
slug: string;
}) => {
const batchCreate = [];
batchCreate.push(
createSvixApp({
name: `${slug}_${AppEnv.Sandbox}`,
orgId: id,
env: AppEnv.Sandbox,
}),
);
batchCreate.push(
createSvixApp({
name: `${slug}_${AppEnv.Live}`,
orgId: id,
env: AppEnv.Live,
}),
);
const [sandboxApp, liveApp] = await Promise.all(batchCreate);
return { sandboxApp, liveApp };
};
export const afterOrgCreated = async ({ org }: { org: Organization }) => {
logger.info(`Org created: ${org.id} (${org.slug})`);
const { id, slug, createdAt } = org;
try {
await OrgService.update({
db,
orgId: id,
updates: {
created_at: createdAt.getTime(),
},
});
// 1. Create svix webhoooks
const { sandboxApp, liveApp } = await initOrgSvixApps({
slug,
id,
});
await OrgService.update({
db,
orgId: id,
updates: {
svix_config: {
sandbox_app_id: sandboxApp?.id,
live_app_id: liveApp?.id,
},
test_pkey: generatePublishableKey(AppEnv.Sandbox),
live_pkey: generatePublishableKey(AppEnv.Live),
},
});
logger.info(`Initialized resources for org ${id} (${slug})`);
} catch (error: any) {
if (error?.data && error.data.code == "23505") {
logger.error(
`Org ${id} already exists in Supabase -- skipping creationg`,
);
return;
}
logger.error(
`Failed to insert org. Code: ${error.code}, message: ${error.message}`,
);
return;
}
};

View File

@@ -0,0 +1,39 @@
import { logger } from "@/external/logtail/logtailUtils.js";
import "dotenv/config";
export const checkEnvVars = () => {
if (!process.env.DATABASE_URL) {
console.error(`DATABASE_URL is not set`);
process.exit(1);
}
if (!process.env.ENCRYPTION_IV || !process.env.ENCRYPTION_PASSWORD) {
console.error(
`ENCRYPTION_IV or ENCRYPTION_PASSWORD is not set (used for Stripe key encryption)`,
);
process.exit(1);
}
if (!process.env.REDIS_URL) {
console.error(`REDIS_URL is not set`);
process.exit(1);
}
if (!process.env.BETTER_AUTH_SECRET || !process.env.BETTER_AUTH_URL) {
console.error(`BETTER_AUTH_SECRET or BETTER_AUTH_URL is not set`);
process.exit(1);
}
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
logger.warn(
"RESEND_API_KEY or RESEND_DOMAIN is not set (use terminal for sign in OTP)",
);
}
if (
!process.env.LOGTAIL_SOURCE_TOKEN ||
!process.env.LOGTAIL_INGESTING_HOST
) {
logger.warn("LOGTAIL ENV VARs not found, skipping logtail");
}
};

View File

@@ -144,7 +144,6 @@ describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => {
// let usagePrice = await getUsageInArrearPrice({
// org: this.org,
// sb: this.sb,
// env: this.env,
// productId: advanceProducts.gpuStarterAnnual.id,
// });

View File

@@ -12,7 +12,6 @@ describe("Initialize org for tests", () => {
this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
this.env = DEFAULT_ENV;
this.sb = createSupabaseClient();
await setupOrg({
orgId: this.org.id,
env: DEFAULT_ENV,

View File

@@ -422,7 +422,6 @@ let orgSlug = "unit-test-org";
before(async function () {
try {
this.env = AppEnv.Sandbox;
this.sb = createSupabaseClient();
const { db, client } = initDrizzle();
this.db = db;
this.client = client;

View File

@@ -1,298 +1,298 @@
import chalk from "chalk";
import Stripe from "stripe";
// import chalk from "chalk";
// import Stripe from "stripe";
import { expect } from "chai";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { advanceProducts, features } from "tests/global.js";
import { compareMainProduct } from "tests/utils/compare.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { timeout } from "tests/utils/genUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { addDays, addMonths, format } from "date-fns";
// import { expect } from "chai";
// import { AutumnCli } from "tests/cli/AutumnCli.js";
// import { advanceProducts, features } from "tests/global.js";
// import { compareMainProduct } from "tests/utils/compare.js";
// import { advanceTestClock } from "tests/utils/stripeUtils.js";
// import { timeout } from "tests/utils/genUtils.js";
// import { createStripeCli } from "@/external/stripe/utils.js";
// import { addDays, addMonths, format } from "date-fns";
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { CacheType } from "@/external/caching/cacheActions.js";
import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
// import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
// import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js";
// import { CacheManager } from "@/external/caching/CacheManager.js";
// import { CacheType } from "@/external/caching/cacheActions.js";
// import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
const advanceAPThroughBalances = async ({
stripeSub,
stripeCli,
testClockId,
customerId,
billingUnits,
startingFrom,
startingBalance,
}: {
stripeSub: Stripe.Subscription;
stripeCli: Stripe;
testClockId: string;
customerId: string;
billingUnits: number;
startingFrom?: number;
startingBalance?: number;
}) => {
// 1. Get total period
let totalPeriod =
(stripeSub.current_period_end - stripeSub.current_period_start) * 1000;
// const advanceAPThroughBalances = async ({
// stripeSub,
// stripeCli,
// testClockId,
// customerId,
// billingUnits,
// startingFrom,
// startingBalance,
// }: {
// stripeSub: Stripe.Subscription;
// stripeCli: Stripe;
// testClockId: string;
// customerId: string;
// billingUnits: number;
// startingFrom?: number;
// startingBalance?: number;
// }) => {
// // 1. Get total period
// let totalPeriod =
// (stripeSub.current_period_end - stripeSub.current_period_start) * 1000;
// 2. Get allowance
let allowance =
advanceProducts.proratedArrearSeats.entitlements.seats.allowance!;
// // 2. Get allowance
// let allowance =
// advanceProducts.proratedArrearSeats.entitlements.seats.allowance!;
// 3. Get starting balance
let balance = startingBalance || allowance;
// // 3. Get starting balance
// let balance = startingBalance || allowance;
// 4. Get price per seat
let pricePerSeat =
advanceProducts.proratedArrearSeats.prices[1].config.usage_tiers[0].amount;
// // 4. Get price per seat
// let pricePerSeat =
// advanceProducts.proratedArrearSeats.prices[1].config.usage_tiers[0].amount;
let skipDays = 2;
// 5. Get accrued price
let accruedPrice = 0;
if (startingBalance) {
let proratedPrice =
(-startingBalance *
pricePerSeat *
(startingFrom! - stripeSub.current_period_start * 1000)) /
totalPeriod;
// let skipDays = 2;
// // 5. Get accrued price
// let accruedPrice = 0;
// if (startingBalance) {
// let proratedPrice =
// (-startingBalance *
// pricePerSeat *
// (startingFrom! - stripeSub.current_period_start * 1000)) /
// totalPeriod;
let previouslyPaid = pricePerSeat * -startingBalance;
let priceToPay = proratedPrice - previouslyPaid;
// let previouslyPaid = pricePerSeat * -startingBalance;
// let priceToPay = proratedPrice - previouslyPaid;
accruedPrice = priceToPay;
// accruedPrice = Math.max(accruedPrice, 0);
console.log(" 🔍 Starting balance: ", startingBalance);
console.log(" 🔍 Starting price: ", accruedPrice);
}
// accruedPrice = priceToPay;
// // accruedPrice = Math.max(accruedPrice, 0);
// console.log(" 🔍 Starting balance: ", startingBalance);
// console.log(" 🔍 Starting price: ", accruedPrice);
// }
let curTime = startingFrom || stripeSub.current_period_start * 1000;
let numberOfEvents = 2;
// let curTime = startingFrom || stripeSub.current_period_start * 1000;
// let numberOfEvents = 2;
console.group();
console.group();
for (let i = 0; i < numberOfEvents; i++) {
let sign = balance > 0 ? 1 : Math.random() > 0.7 ? 1 : -1;
// console.group();
// console.group();
// for (let i = 0; i < numberOfEvents; i++) {
// let sign = balance > 0 ? 1 : Math.random() > 0.7 ? 1 : -1;
let currentUsage = allowance - balance;
// let currentUsage = allowance - balance;
let nextBoundary =
Math.ceil((currentUsage + 1) / billingUnits) * billingUnits;
// let nextBoundary =
// Math.ceil((currentUsage + 1) / billingUnits) * billingUnits;
let prevBoundary = nextBoundary - billingUnits;
// let prevBoundary = nextBoundary - billingUnits;
let valueNeeded = 0;
if (sign > 0) {
// Add random amount to push above next boundary
const valueToGetToNegative = balance + 1;
valueNeeded =
Math.floor(Math.random() * 10) + (nextBoundary - currentUsage + 1);
// let valueNeeded = 0;
// if (sign > 0) {
// // Add random amount to push above next boundary
// const valueToGetToNegative = balance + 1;
// valueNeeded =
// Math.floor(Math.random() * 10) + (nextBoundary - currentUsage + 1);
valueNeeded = Math.max(valueNeeded, valueToGetToNegative);
} else {
valueNeeded = -(
Math.floor(Math.random() * 10) +
(currentUsage - prevBoundary + 1)
);
}
// valueNeeded = Math.max(valueNeeded, valueToGetToNegative);
// } else {
// valueNeeded = -(
// Math.floor(Math.random() * 10) +
// (currentUsage - prevBoundary + 1)
// );
// }
let newBalance = balance - valueNeeded;
// let newBalance = balance - valueNeeded;
await AutumnCli.updateBalances({
customerId,
balances: [
{
feature_id: features.seats.id,
balance: newBalance,
},
],
});
// await AutumnCli.updateBalances({
// customerId,
// balances: [
// {
// feature_id: features.seats.id,
// balance: newBalance,
// },
// ],
// });
await timeout(2000);
// await timeout(2000);
let prevBalance = balance;
balance = newBalance;
// let prevBalance = balance;
// balance = newBalance;
// Calculate prorated price only when crossing boundary
let newPrice = Math.max(0, -balance * pricePerSeat);
let prevCurTime = curTime;
curTime = addDays(curTime, 2).getTime();
// // Calculate prorated price only when crossing boundary
// let newPrice = Math.max(0, -balance * pricePerSeat);
// let prevCurTime = curTime;
// curTime = addDays(curTime, 2).getTime();
if (i === numberOfEvents - 1) {
curTime = stripeSub.current_period_end * 1000;
}
// if (i === numberOfEvents - 1) {
// curTime = stripeSub.current_period_end * 1000;
// }
let proratedPrice = (newPrice * (curTime - prevCurTime)) / totalPeriod;
accruedPrice += Number(proratedPrice.toFixed(2));
// let proratedPrice = (newPrice * (curTime - prevCurTime)) / totalPeriod;
// accruedPrice += Number(proratedPrice.toFixed(2));
console.log(`Event ${i + 1}:`);
console.log(` - Value added: ${valueNeeded}`);
console.log(` - Balance: ${prevBalance} -> ${balance}`);
console.log(` - Prorated price: ${proratedPrice.toFixed(2)}`);
console.log(` - Accrued price: ${accruedPrice.toFixed(2)}`);
// console.log(`Event ${i + 1}:`);
// console.log(` - Value added: ${valueNeeded}`);
// console.log(` - Balance: ${prevBalance} -> ${balance}`);
// console.log(` - Prorated price: ${proratedPrice.toFixed(2)}`);
// console.log(` - Accrued price: ${accruedPrice.toFixed(2)}`);
await advanceTestClock({
stripeCli,
testClockId,
numberOfDays: 2,
startingFrom: new Date(prevCurTime),
});
}
// await advanceTestClock({
// stripeCli,
// testClockId,
// numberOfDays: 2,
// startingFrom: new Date(prevCurTime),
// });
// }
console.groupEnd();
console.groupEnd();
// console.groupEnd();
// console.groupEnd();
// Advance test clock to end of period
// // Advance test clock to end of period
let advanceTo = addDays(addMonths(new Date(), 1), 2);
let advanceToStart = startingFrom ? new Date(startingFrom) : new Date();
await advanceTestClock({
stripeCli,
testClockId,
numberOfDays: 2,
startingFrom: addMonths(advanceToStart, 1),
});
// let advanceTo = addDays(addMonths(new Date(), 1), 2);
// let advanceToStart = startingFrom ? new Date(startingFrom) : new Date();
// await advanceTestClock({
// stripeCli,
// testClockId,
// numberOfDays: 2,
// startingFrom: addMonths(advanceToStart, 1),
// });
// Check invoice amount
const res = await AutumnCli.getCustomer(customerId);
let invoice = res.invoices[0];
// // Check invoice amount
// const res = await AutumnCli.getCustomer(customerId);
// let invoice = res.invoices[0];
let basePrice = advanceProducts.proratedArrearSeats.prices[0].config.amount;
let nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0);
console.log(" 🔍 Next month usage price: ", nextMonthUsagePrice);
// let basePrice = advanceProducts.proratedArrearSeats.prices[0].config.amount;
// let nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0);
// console.log(" 🔍 Next month usage price: ", nextMonthUsagePrice);
let expectedTotal = invoice.total;
expect(Number(expectedTotal.toFixed(2))).to.lessThan(invoice.total + 0.1);
expect(Number(expectedTotal.toFixed(2))).to.greaterThan(invoice.total - 0.1);
// let expectedTotal = invoice.total;
// expect(Number(expectedTotal.toFixed(2))).to.lessThan(invoice.total + 0.1);
// expect(Number(expectedTotal.toFixed(2))).to.greaterThan(invoice.total - 0.1);
return {
balance,
advancedTo: advanceTo.getTime(),
};
};
// return {
// balance,
// advancedTo: advanceTo.getTime(),
// };
// };
describe(`${chalk.yellowBright(
"arrear_prorated2: testing update in arrear prorated through /balances",
)}`, () => {
const customerId = "arrear-prorated-balances";
// describe(`${chalk.yellowBright(
// "arrear_prorated2: testing update in arrear prorated through /balances",
// )}`, () => {
// const customerId = "arrear-prorated-balances";
let testClockId = "";
let stripeCli: Stripe;
let subId = "";
let stripeSub: Stripe.Subscription;
let billingUnits =
advanceProducts.proratedArrearSeats.prices[1].config.billing_units || 1;
// let testClockId = "";
// let stripeCli: Stripe;
// let subId = "";
// let stripeSub: Stripe.Subscription;
// let billingUnits =
// advanceProducts.proratedArrearSeats.prices[1].config.billing_units || 1;
before(async function () {
const { testClockId: createdTestClockId } = await initCustomerWithTestClock(
{
customerId,
org: this.org,
env: this.env,
db: this.db,
},
);
// before(async function () {
// const { testClockId: createdTestClockId } = await initCustomerWithTestClock(
// {
// customerId,
// org: this.org,
// env: this.env,
// db: this.db,
// },
// );
stripeCli = createStripeCli({
org: this.org,
env: this.env,
});
// stripeCli = createStripeCli({
// org: this.org,
// env: this.env,
// });
testClockId = createdTestClockId;
// testClockId = createdTestClockId;
await this.sb
.from("organizations")
.update({
config: {
...this.org.config,
bill_upgrade_immediately: false,
},
})
.eq("id", this.org.id);
// await this.sb
// .from("organizations")
// .update({
// config: {
// ...this.org.config,
// bill_upgrade_immediately: false,
// },
// })
// .eq("id", this.org.id);
await CacheManager.invalidate({
action: CacheType.SecretKey,
value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!),
});
await CacheManager.disconnect();
});
// await CacheManager.invalidate({
// action: CacheType.SecretKey,
// value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!),
// });
// await CacheManager.disconnect();
// });
it("should attach in arrear prorated seats", async () => {
await timeout(5000);
await AutumnCli.attach({
customerId,
productId: advanceProducts.proratedArrearSeats.id,
});
});
// it("should attach in arrear prorated seats", async () => {
// await timeout(5000);
// await AutumnCli.attach({
// customerId,
// productId: advanceProducts.proratedArrearSeats.id,
// });
// });
it("should have correct product", async function () {
const res = await AutumnCli.getCustomer(customerId);
compareMainProduct({
sent: advanceProducts.proratedArrearSeats,
cusRes: res,
});
// it("should have correct product", async function () {
// const res = await AutumnCli.getCustomer(customerId);
// compareMainProduct({
// sent: advanceProducts.proratedArrearSeats,
// cusRes: res,
// });
// 2. Get subscription period start and period end
subId = res.products[0].subscription_ids[0];
stripeSub = await stripeCli.subscriptions.retrieve(subId);
// // 2. Get subscription period start and period end
// subId = res.products[0].subscription_ids[0];
// stripeSub = await stripeCli.subscriptions.retrieve(subId);
await checkSubscriptionContainsProducts({
db: this.db,
org: this.org,
env: this.env,
subscriptionId: subId,
productIds: [advanceProducts.proratedArrearSeats.id],
});
});
// await checkSubscriptionContainsProducts({
// db: this.db,
// org: this.org,
// env: this.env,
// subscriptionId: subId,
// productIds: [advanceProducts.proratedArrearSeats.id],
// });
// });
let advancedTo: number;
let balance: number;
it("should run first cycles and have correct invoice / balance", async () => {
// Do it again
let { advancedTo: advancedTo1, balance: balance1 } =
await advanceAPThroughBalances({
stripeSub,
stripeCli,
testClockId,
customerId,
billingUnits,
});
// let advancedTo: number;
// let balance: number;
// it("should run first cycles and have correct invoice / balance", async () => {
// // Do it again
// let { advancedTo: advancedTo1, balance: balance1 } =
// await advanceAPThroughBalances({
// stripeSub,
// stripeCli,
// testClockId,
// customerId,
// billingUnits,
// });
advancedTo = advancedTo1;
balance = balance1;
});
// advancedTo = advancedTo1;
// balance = balance1;
// });
it("should run second cycle and have correct invoice / balance", async () => {
console.log(` Advanced to ${format(new Date(advancedTo), "yyyy-MM-dd")}`);
// it("should run second cycle and have correct invoice / balance", async () => {
// console.log(` Advanced to ${format(new Date(advancedTo), "yyyy-MM-dd")}`);
let newStripeSub = await stripeCli.subscriptions.retrieve(subId);
await advanceAPThroughBalances({
stripeSub: newStripeSub,
stripeCli,
testClockId,
customerId,
billingUnits,
startingFrom: advancedTo,
startingBalance: balance,
});
});
// let newStripeSub = await stripeCli.subscriptions.retrieve(subId);
// await advanceAPThroughBalances({
// stripeSub: newStripeSub,
// stripeCli,
// testClockId,
// customerId,
// billingUnits,
// startingFrom: advancedTo,
// startingBalance: balance,
// });
// });
after(async function () {
await this.sb
.from("organizations")
.update({
config: {
...this.org.config,
bill_upgrade_immediately: true,
},
})
.eq("id", this.org.id);
void CacheManager.invalidate({
action: CacheType.SecretKey,
value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!),
});
});
// TODO: Test reset at for in arrear prorated with Ent Interval = Lifetime
// TODO: Test in arrear prorated for entitlements with billing units > 1
});
// after(async function () {
// await this.sb
// .from("organizations")
// .update({
// config: {
// ...this.org.config,
// bill_upgrade_immediately: true,
// },
// })
// .eq("id", this.org.id);
// void CacheManager.invalidate({
// action: CacheType.SecretKey,
// value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!),
// });
// });
// // TODO: Test reset at for in arrear prorated with Ent Interval = Lifetime
// // TODO: Test in arrear prorated for entitlements with billing units > 1
// });

View File

@@ -1,199 +0,0 @@
// THIS TEST CASE IS COVERED UNDER UPGRADE2.TS
// import { Customer } from "@autumn/shared";
// import chalk from "chalk";
// import { compareMainProduct } from "../../utils/compare.js";
// import { AutumnCli } from "../../cli/AutumnCli.js";
// import { advanceProducts, creditSystems } from "../../global.js";
// import { timeout } from "../../utils/genUtils.js";
// import { assert, expect } from "chai";
// import { createStripeCli } from "@/external/stripe/utils.js";
// import {
// advanceClockForInvoice,
// advanceMonths,
// advanceTestClock,
// checkBillingMeterEventSummary,
// getUsageInArrearPrice,
// } from "../../utils/stripeUtils.js";
// import { addMonths } from "date-fns";
// import {
// sendGPUEvents,
// checkUsageInvoiceAmount,
// } from "../../utils/advancedUsageUtils.js";
// import { Decimal } from "decimal.js";
// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
// import { setupBefore } from "tests/before.js";
// import Stripe from "stripe";
// // FOURTH, TEST GPU STARTER ANNUAL UPGRADE TO GPU PRO
// const testCase = "usage5";
// describe(`${chalk.yellowBright("usage5: multi interval upgrade, GPU starter annual -> GPU pro annual")}`, () => {
// const customerId = testCase;
// let testClockId = "";
// let totalCreditsUsed = 0;
// let customer: Customer;
// let stripeCli: Stripe;
// let curTime = new Date();
// before(async function () {
// await setupBefore(this);
// let res = await initCustomer({
// customerId,
// org: this.org,
// env: this.env,
// db: this.db,
// autumn: this.autumnJs,
// attachPm: "success",
// });
// testClockId = res.testClockId;
// customer = res.customer;
// stripeCli = this.stripeCli;
// });
// it("should attach GPU starter annual", async function () {
// await AutumnCli.attach({
// customerId: customerId,
// productId: advanceProducts.gpuStarterAnnual.id,
// });
// const res = await AutumnCli.getCustomer(customerId);
// compareMainProduct({
// sent: advanceProducts.gpuStarterAnnual,
// cusRes: res,
// });
// });
// it(`should advance 1 month and upgrade to GPU pro monthly`, async function () {
// let numberOfMonths = 1;
// await advanceMonths({
// stripeCli,
// testClockId,
// numberOfMonths,
// });
// curTime = addMonths(curTime, numberOfMonths);
// // Send 20 events
// let eventCount = 20;
// const { creditsUsed } = await sendGPUEvents({
// customerId,
// eventCount,
// });
// totalCreditsUsed = creditsUsed;
// await AutumnCli.attach({
// customerId: customerId,
// productId: advanceProducts.gpuProAnnual.id,
// });
// await advanceTestClock({
// stripeCli,
// testClockId,
// numberOfDays: 10,
// startingFrom: curTime,
// });
// });
// it("should have GPU pro annual product and 2 Stripe subscriptions", async function () {
// const res = await AutumnCli.getCustomer(customerId);
// compareMainProduct({
// sent: advanceProducts.gpuProAnnual,
// cusRes: res,
// });
// // Should have 2 subscriptions
// const subs = await stripeCli.subscriptions.list({
// customer: customer.processor.id,
// });
// expect(subs.data.length).to.equal(2);
// });
// it("should have correct invoice for GPU starter annual (bill for remaining usages)", async function () {
// const res = await AutumnCli.getCustomer(customerId);
// const invoices = res!.invoices;
// let invoiceIndex = invoices.findIndex((invoice: any) =>
// invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id),
// );
// await checkUsageInvoiceAmount({
// invoices,
// totalUsage: totalCreditsUsed,
// product: advanceProducts.gpuStarterAnnual,
// featureId: creditSystems.gpuCredits.id,
// invoiceIndex,
// includeBase: false,
// });
// });
// it("should send 20 events (on GPU pro annual)", async function () {
// const stripeCli = createStripeCli({ org: this.org, env: this.env });
// // Send 20 events
// let eventCount = 20;
// const { creditsUsed } = await sendGPUEvents({
// customerId,
// eventCount,
// });
// totalCreditsUsed = creditsUsed;
// await advanceClockForInvoice({
// stripeCli,
// testClockId,
// waitForMeterUpdate: true,
// startingFrom: curTime,
// });
// });
// it("should have correct billing meter event summary for GPU pro annual", async function () {
// const res = await AutumnCli.getCustomer(customerId);
// const invoices = res!.invoices;
// // Think I have to use Stripe metered event summary to check this
// let usagePrice = await getUsageInArrearPrice({
// org: this.org,
// sb: this.sb,
// env: this.env,
// productId: advanceProducts.gpuProAnnual.id,
// });
// let eventSummary = await checkBillingMeterEventSummary({
// stripeCli,
// startTime: curTime, // Wrong date?
// stripeMeterId: usagePrice?.config?.stripe_meter_id,
// stripeCustomerId: customer.processor.id,
// });
// let roundedFirst = Math.ceil(
// new Decimal(totalCreditsUsed)
// .div(usagePrice?.config?.billing_units!)
// .toNumber(),
// );
// let roundedTotalCreditsUsed = new Decimal(roundedFirst)
// .mul(usagePrice?.config?.billing_units!)
// .toNumber();
// try {
// assert.exists(eventSummary);
// assert.equal(eventSummary?.aggregated_value, roundedTotalCreditsUsed);
// } catch (error) {
// console.group();
// console.log(" - Event summary: ", eventSummary);
// console.log(" - Total credits used: ", totalCreditsUsed);
// console.groupEnd();
// throw error;
// }
// // await checkUsageInvoiceAmount({
// // invoices,
// // totalUsage: totalCreditsUsed,
// // product: advanceProducts.gpuProAnnual,
// // featureId: creditSystems.gpuCredits.id,
// // invoiceIndex: 0,
// // includeBase: false,
// // });
// });
// });

View File

@@ -14,7 +14,6 @@ const ORG_SLUG = "unit-test-org";
const DEFAULT_ENV = AppEnv.Sandbox;
export const setupBefore = async (instance: any) => {
const sb = createSupabaseClient();
const { db, client } = initDrizzle();
const org = await OrgService.getBySlug({ db, slug: ORG_SLUG });
@@ -31,7 +30,6 @@ export const setupBefore = async (instance: any) => {
});
const stripeCli = createStripeCli({ org, env });
instance.sb = sb;
instance.org = org;
instance.env = env;
instance.autumn = autumn;

View File

@@ -854,7 +854,6 @@ const DEFAULT_ENV = AppEnv.Sandbox;
before(async function () {
try {
this.env = AppEnv.Sandbox;
this.sb = createSupabaseClient();
let { db, client } = initDrizzle();
this.db = db;
this.client = client;

View File

@@ -391,37 +391,6 @@ export const checkBillingMeterEventSummary = async ({
}
};
export const getUsageInArrearPrice = async ({
org,
sb,
env,
productId,
}: {
org: Organization;
sb: SupabaseClient;
env: AppEnv;
productId: string;
}) => {
const { data, error } = await sb
.from("prices")
.select("*, product:products!inner(*)")
.eq("product.org_id", org.id)
.eq("product.env", env)
.eq("product.id", productId);
if (error) {
throw new Error(error.message);
}
for (const price of data) {
if (getBillingType(price.config as any) === BillingType.UsageInArrear) {
return price;
}
}
return null;
};
export const getDiscount = async ({
stripeCli,
customer,

View File

@@ -12,15 +12,11 @@ import { usePostHog } from "posthog-js/react";
import { Button } from "@/components/ui/button";
import { ArrowUpRightFromSquare } from "lucide-react";
import { AutumnProvider } from "autumn-js/react";
import { useAuth } from "@clerk/clerk-react";
import { useSession } from "@/lib/auth-client";
import { CustomToaster } from "@/components/general/CustomToaster";
export function MainLayout() {
const env = useEnv();
const { getToken } = useAuth();
const { pathname } = useLocation();
const { data, isPending } = useSession();
const navigate = useNavigate();
@@ -88,16 +84,7 @@ export function MainLayout() {
// }
return (
<AutumnProvider
includeCredentials={false}
backendUrl={import.meta.env.VITE_BACKEND_URL}
getBearerToken={async () => {
const token = await getToken({
template: "custom_template",
});
return token;
}}
>
<AutumnProvider backendUrl={import.meta.env.VITE_BACKEND_URL}>
<main className="w-screen h-screen flex bg-stone-100">
<CustomToaster />
<MainSidebar />

View File

@@ -3,33 +3,25 @@ import App from "./App";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ClerkProvider } from "@clerk/clerk-react";
import { PostHogProvider } from "posthog-js/react";
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
if (!PUBLISHABLE_KEY) {
throw new Error("Add your Clerk Publishable Key to the .env file");
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
{process.env.NODE_ENV === "development" ? (
{process.env.NODE_ENV === "development" ? (
<App />
) : (
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{
// autocapture: false,
// capture_pageview: false,
// capture_pageleave: false,
// session_recording: {}
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
}}
>
<App />
) : (
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{
// autocapture: false,
// capture_pageview: false,
// capture_pageleave: false,
// session_recording: {}
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
}}
>
<App />
</PostHogProvider>
)}
</ClerkProvider>
</PostHogProvider>
)}
</StrictMode>,
);

View File

@@ -9,9 +9,9 @@ import { OTPSignIn } from "./components/OTPSignIn";
import { Mail } from "lucide-react";
import { CustomToaster } from "@/components/general/CustomToaster";
import { toast } from "sonner";
import { getBackendErr } from "@/utils/genUtils";
export const SignIn = () => {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [googleLoading, setGoogleLoading] = useState(false);
const [sendOtpLoading, setSendOtpLoading] = useState(false);
@@ -60,13 +60,16 @@ export const SignIn = () => {
setGoogleLoading(true);
try {
const frontendUrl = import.meta.env.VITE_FRONTEND_URL;
await signIn.social({
const { data, error } = await signIn.social({
provider: "google",
callbackURL: `${frontendUrl}${callbackPath}`,
newUserCallbackURL: `${frontendUrl}${newPath}`,
});
if (error) {
toast.error(error.message || "Failed to sign in with Google");
}
} catch (error) {
console.error("Error signing in with Google:", error);
toast.error(getBackendErr(error, "Failed to sign in with Google"));
} finally {
setTimeout(() => {
setGoogleLoading(false);

View File

@@ -34,7 +34,6 @@ export const OTPSignIn = ({
}, [resendCountdown]);
const handleSubmit = async (otp: string) => {
console.log(otp);
setVerifying(true);
try {
const { data, error } = await authClient.signIn.emailOtp({
@@ -42,6 +41,9 @@ export const OTPSignIn = ({
otp: otp,
});
console.log("Data", data);
console.log("Error", error);
if (error) {
toast.error(error.message || "Failed to verify code");
setVerifying(false);

View File

@@ -3,13 +3,7 @@
import { useAxiosSWR } from "@/services/useAxiosSwr";
import LoadingScreen from "@/views/general/LoadingScreen";
import { AppEnv } from "@autumn/shared";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { CustomerContext } from "./CustomerContext";
import { Link, useNavigate, useParams, useSearchParams } from "react-router";
import { CustomerProductList } from "./CustomerProductList";

View File

@@ -54,15 +54,14 @@ function AddProduct() {
const navigate = useNavigate();
const handleAddProduct = async (productId: string, setLoading: any) => {
const { data } = await OrgService.get(axiosInstance);
let stripeConnected = org?.stripe_connected;
if (!data.org) {
toast.error("Something went wrong...please try again later");
setLoading(false);
return;
if (!stripeConnected) {
const { data: org } = await OrgService.get(axiosInstance);
stripeConnected = org?.stripe_connected;
}
if (!data.org.stripe_connected) {
if (!stripeConnected) {
toast.error("Connect to Stripe to add products to customers");
const redirectUrl = getRedirectUrl(`/customers/${customer.id}`, env);
navigateTo(`/integrations/stripe?redirect=${redirectUrl}`, navigate, env);

View File

@@ -7,6 +7,7 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
import axios from "axios";
import { useOrg } from "@/hooks/useOrg";
import { getOrgLogoUrl } from "@/utils/orgUtils";
import { getBackendErr } from "@/utils/genUtils";
const MAX_SIZE_MB = 10;
const MAX_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024;
@@ -89,7 +90,7 @@ const OrgLogoUploader: React.FC<OrgLogoUploaderProps> = ({
setLogoVersion(logoVersion + 1);
toast.success("Successfully uploaded logo");
} catch (error) {
toast.error("Failed to upload logo");
toast.error(getBackendErr(error, "Failed to upload logo"));
} finally {
setUploading(false);
}

View File

@@ -37,6 +37,7 @@ import SmallSpinner from "@/components/general/SmallSpinner";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faStripeS } from "@fortawesome/free-brands-svg-icons";
import { useNavigate } from "react-router";
import { useOrg } from "@/hooks/useOrg";
function ConnectStripe({
className,
@@ -45,29 +46,19 @@ function ConnectStripe({
className?: string;
onboarding?: boolean;
}) {
const axiosInstance = useAxiosInstance({ env: AppEnv.Live });
const navigate = useNavigate();
const { org, mutate, isLoading: isOrgLoading } = useOrg();
const [searchParams] = useSearchParams();
const redirect = searchParams.get("redirect");
const axiosInstance = useAxiosInstance({ env: AppEnv.Live });
const [testApiKey, setTestApiKey] = useState("");
const [liveApiKey, setLiveApiKey] = useState("");
const [successUrl, setSuccessUrl] = useState("https://useautumn.com");
const [defaultCurrency, setDefaultCurrency] = useState("USD");
const [isLoading, setIsLoading] = useState(false);
const {
data: orgData,
mutate: mutateOrg,
isLoading: isOrgLoading,
} = useAxiosSWR({
url: `/organization`,
env: AppEnv.Live,
});
const org = orgData?.org;
const handleConnectStripe = async () => {
if (!testApiKey || !successUrl || !defaultCurrency) {
toast.error("Please fill in all fields");
@@ -90,7 +81,7 @@ function ConnectStripe({
});
toast.success("Successfully connected to Stripe");
await mutateOrg();
await mutate();
if (redirect && !onboarding) {
navigate(redirect);
}
@@ -107,7 +98,7 @@ function ConnectStripe({
try {
setIsDisconnecting(true);
await OrgService.disconnectStripe(axiosInstance);
await mutateOrg();
await mutate();
toast.success("Successfully disconnected from Stripe");
} catch (error) {
toast.error(getBackendErr(error, "Failed to disconnect Stripe"));
@@ -125,7 +116,7 @@ function ConnectStripe({
className={cn(
"flex flex-col gap-4",
className,
onboarding && "flex-row justify-between items-center"
onboarding && "flex-row justify-between items-center",
)}
>
<p className="text-t3 text-sm">Stripe Connected &nbsp; </p>
@@ -222,7 +213,7 @@ export const CurrencySelect = ({
"w-full justify-between transition-colors duration-100",
open &&
"border-[rgb(139,92,246)] shadow-[0_0_2px_1px_rgba(139,92,246,0.25)]",
className
className,
)}
disabled={disabled}
>