diff --git a/package-lock.json b/package-lock.json
index 207fe9fb8..6da3ca5b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/server/auth-schema.ts b/server/auth-schema.ts
index f7ab9c012..52f1c8dbf 100644
--- a/server/auth-schema.ts
+++ b/server/auth-schema.ts
@@ -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" }),
diff --git a/server/package.json b/server/package.json
index c6d082c04..94557e58c 100644
--- a/server/package.json
+++ b/server/package.json
@@ -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",
diff --git a/server/src/external/caching/CacheManager.ts b/server/src/external/caching/CacheManager.ts
index 7e6df2e15..26c06ac38 100644
--- a/server/src/external/caching/CacheManager.ts
+++ b/server/src/external/caching/CacheManager.ts
@@ -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, {
diff --git a/server/src/external/logtail/logtailUtils.ts b/server/src/external/logtail/logtailUtils.ts
index ad7c530f6..aa2f2c727 100644
--- a/server/src/external/logtail/logtailUtils.ts
+++ b/server/src/external/logtail/logtailUtils.ts
@@ -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();
diff --git a/server/src/external/posthog/createPosthogCli.ts b/server/src/external/posthog/createPosthogCli.ts
index 471fa7669..c4321a35f 100644
--- a/server/src/external/posthog/createPosthogCli.ts
+++ b/server/src/external/posthog/createPosthogCli.ts
@@ -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;
diff --git a/server/src/external/resend/resendUtils.ts b/server/src/external/resend/resendUtils.ts
index 6737f2346..fcf202940 100644
--- a/server/src/external/resend/resendUtils.ts
+++ b/server/src/external/resend/resendUtils.ts
@@ -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 `,
+ from: from || `Ayush `,
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 `,
+ from: from || `Ayush `,
to: to,
subject: subject,
html: body,
diff --git a/server/src/external/resend/safeResend.ts b/server/src/external/resend/safeResend.ts
new file mode 100644
index 000000000..d48e626d3
--- /dev/null
+++ b/server/src/external/resend/safeResend.ts
@@ -0,0 +1,23 @@
+import { logger } from "../logtail/logtailUtils.js";
+
+export function safeResend any>({
+ fn,
+ action,
+}: {
+ fn: T;
+ action: string;
+}): (...args: Parameters) => Promise | undefined> {
+ return async (...args: Parameters) => {
+ 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}`);
+ }
+ };
+}
diff --git a/server/src/external/supabase/safeSb.ts b/server/src/external/supabase/safeSb.ts
new file mode 100644
index 000000000..682fadec5
--- /dev/null
+++ b/server/src/external/supabase/safeSb.ts
@@ -0,0 +1,23 @@
+import { logger } from "../logtail/logtailUtils.js";
+
+export function safeSb any>({
+ fn,
+ action,
+}: {
+ fn: T;
+ action: string;
+}): (...args: Parameters) => Promise | undefined> {
+ return async (...args: Parameters) => {
+ 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}`);
+ }
+ };
+}
diff --git a/server/src/external/supabase/storageUtils.ts b/server/src/external/supabase/storageUtils.ts
index 14881c79e..f49c88865 100644
--- a/server/src/external/supabase/storageUtils.ts
+++ b/server/src/external/supabase/storageUtils.ts
@@ -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
diff --git a/server/src/external/supabase/subscribeToOrgUpdates.ts b/server/src/external/supabase/subscribeToOrgUpdates.ts
index d6fe114e8..8170e4264 100644
--- a/server/src/external/supabase/subscribeToOrgUpdates.ts
+++ b/server/src/external/supabase/subscribeToOrgUpdates.ts
@@ -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",
+});
diff --git a/server/src/external/svix/svixHelpers.ts b/server/src/external/svix/svixHelpers.ts
new file mode 100644
index 000000000..45c91c37d
--- /dev/null
+++ b/server/src/external/svix/svixHelpers.ts
@@ -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",
+});
diff --git a/server/src/external/svix/svixUtils.ts b/server/src/external/svix/svixUtils.ts
index 55f5476d0..2d03d6fb2 100644
--- a/server/src/external/svix/svixUtils.ts
+++ b/server/src/external/svix/svixUtils.ts
@@ -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 any>({
+ fn,
+ action,
+}: {
+ fn: T;
+ action: string;
+}): (...args: Parameters) => Promise | undefined> {
+ return async (...args: Parameters) => {
+ 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;
-};
diff --git a/server/src/external/unkeyUtils.ts b/server/src/external/unkeyUtils.ts
deleted file mode 100644
index 87d5f4a86..000000000
--- a/server/src/external/unkeyUtils.ts
+++ /dev/null
@@ -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;
-// };
diff --git a/server/src/external/webhooks/clerkWebhooks.ts b/server/src/external/webhooks/clerkWebhooks.ts
index 20cf40440..8cf6a47f8 100644
--- a/server/src/external/webhooks/clerkWebhooks.ts
+++ b/server/src/external/webhooks/clerkWebhooks.ts
@@ -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",
+// });
+// };
diff --git a/server/src/external/webhooks/webhooksRouter.ts b/server/src/external/webhooks/webhooksRouter.ts
index a6ec2db4e..82f123ac8 100644
--- a/server/src/external/webhooks/webhooksRouter.ts
+++ b/server/src/external/webhooks/webhooksRouter.ts
@@ -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;
diff --git a/server/src/index.ts b/server/src/index.ts
index 832150d54..d457bd612 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -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 {
diff --git a/server/src/internal/analytics/handlers/handleProductsUpdated.ts b/server/src/internal/analytics/handlers/handleProductsUpdated.ts
index 6234c6aac..b9a6b0c73 100644
--- a/server/src/internal/analytics/handlers/handleProductsUpdated.ts
+++ b/server/src/internal/analytics/handlers/handleProductsUpdated.ts
@@ -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",
diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts
index 11a5d3294..66bd9c41d 100644
--- a/server/src/internal/customers/CusService.ts
+++ b/server/src/internal/customers/CusService.ts
@@ -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,
-// };
-// }
diff --git a/server/src/internal/dev/ApiKeyService.ts b/server/src/internal/dev/ApiKeyService.ts
index 437fd4e9d..4c3042106 100644
--- a/server/src/internal/dev/ApiKeyService.ts
+++ b/server/src/internal/dev/ApiKeyService.ts
@@ -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,
-// };
diff --git a/server/src/internal/dev/devRouter.ts b/server/src/internal/dev/devRouter.ts
index fd70cf248..25c5c77d2 100644
--- a/server/src/internal/dev/devRouter.ts
+++ b/server/src/internal/dev/devRouter.ts
@@ -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";
diff --git a/server/src/internal/emails/sendInvitationEmail.ts b/server/src/internal/emails/sendInvitationEmail.ts
new file mode 100644
index 000000000..c4a4dfa17
--- /dev/null
+++ b/server/src/internal/emails/sendInvitationEmail.ts
@@ -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 `,
+ to: email,
+ subject: `Join ${orgName} on Autumn`,
+ body: getInvitationEmailBody({ orgName, inviteLink }),
+ });
+ },
+ action: "send org invitation email",
+});
diff --git a/server/src/internal/emails/sendMigrationEmail.ts b/server/src/internal/emails/sendMigrationEmail.ts
new file mode 100644
index 000000000..8d2354306
--- /dev/null
+++ b/server/src/internal/emails/sendMigrationEmail.ts
@@ -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",
+});
diff --git a/server/src/internal/orgs/emails/sendOTPEmail.ts b/server/src/internal/emails/sendOTPEmail.ts
similarity index 69%
rename from server/src/internal/orgs/emails/sendOTPEmail.ts
rename to server/src/internal/emails/sendOTPEmail.ts
index 85905b429..2b767a4cc 100644
--- a/server/src/internal/orgs/emails/sendOTPEmail.ts
+++ b/server/src/internal/emails/sendOTPEmail.ts
@@ -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 `,
to: email,
diff --git a/server/src/external/webhooks/sendOnboardingEmail.ts b/server/src/internal/emails/sendOnboardingEmail.ts
similarity index 53%
rename from server/src/external/webhooks/sendOnboardingEmail.ts
rename to server/src/internal/emails/sendOnboardingEmail.ts
index 1fcdbffd7..73a8585f4 100644
--- a/server/src/external/webhooks/sendOnboardingEmail.ts
+++ b/server/src/internal/emails/sendOnboardingEmail.ts
@@ -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
`;
};
-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",
+});
diff --git a/server/src/internal/features/featureUtils.ts b/server/src/internal/features/featureUtils.ts
index b7dc53fcb..5c30b42b0 100644
--- a/server/src/internal/features/featureUtils.ts
+++ b/server/src/internal/features/featureUtils.ts
@@ -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})`,
);
diff --git a/server/src/internal/mainRouter.ts b/server/src/internal/mainRouter.ts
index 2ef344676..4c720ca8d 100644
--- a/server/src/internal/mainRouter.ts
+++ b/server/src/internal/mainRouter.ts
@@ -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,
diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts
index cb25297ba..36e0460ba 100644
--- a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts
+++ b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts
@@ -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";
diff --git a/server/src/internal/migrations/migrationSteps/sendMigrationEmail.ts b/server/src/internal/migrations/migrationSteps/sendMigrationEmail.ts
deleted file mode 100644
index 9495c9c59..000000000
--- a/server/src/internal/migrations/migrationSteps/sendMigrationEmail.ts
+++ /dev/null
@@ -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}
-`,
- });
-};
diff --git a/server/src/internal/orgs/emails/sendInvitationEmail.ts b/server/src/internal/orgs/emails/sendInvitationEmail.ts
deleted file mode 100644
index 21a95d5f7..000000000
--- a/server/src/internal/orgs/emails/sendInvitationEmail.ts
+++ /dev/null
@@ -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 }),
- });
-};
diff --git a/server/src/internal/orgs/handlers/handleDeleteOrg.ts b/server/src/internal/orgs/handlers/handleDeleteOrg.ts
index f5ab1b34b..f7ef86112 100644
--- a/server/src/internal/orgs/handlers/handleDeleteOrg.ts
+++ b/server/src/internal/orgs/handlers/handleDeleteOrg.ts
@@ -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,
diff --git a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts
index dadf2664a..ad24855ef 100644
--- a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts
+++ b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts
@@ -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) {
diff --git a/server/src/internal/orgs/orgRouter.ts b/server/src/internal/orgs/orgRouter.ts
index 9cc54287d..244571b97 100644
--- a/server/src/internal/orgs/orgRouter.ts
+++ b/server/src/internal/orgs/orgRouter.ts
@@ -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";
diff --git a/server/src/internal/orgs/orgUtils.ts b/server/src/internal/orgs/orgUtils.ts
index b53b5baab..6baba03c1 100644
--- a/server/src/internal/orgs/orgUtils.ts
+++ b/server/src/internal/orgs/orgUtils.ts
@@ -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,
};
};
diff --git a/server/src/internal/rewards/RewardProgramService.ts b/server/src/internal/rewards/RewardProgramService.ts
index 6d80efdae..67ed56410 100644
--- a/server/src/internal/rewards/RewardProgramService.ts
+++ b/server/src/internal/rewards/RewardProgramService.ts
@@ -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;
}
}
diff --git a/server/src/internal/rewards/RewardRedemptionService.ts b/server/src/internal/rewards/RewardRedemptionService.ts
index fcb34aba0..184832c9c 100644
--- a/server/src/internal/rewards/RewardRedemptionService.ts
+++ b/server/src/internal/rewards/RewardRedemptionService.ts
@@ -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({
diff --git a/server/src/middleware/analyticsMiddleware.ts b/server/src/middleware/analyticsMiddleware.ts
index c02e9dd63..0c72b8910 100644
--- a/server/src/middleware/analyticsMiddleware.ts
+++ b/server/src/middleware/analyticsMiddleware.ts
@@ -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) => {
diff --git a/server/src/middleware/apiMiddleware.ts b/server/src/middleware/apiMiddleware.ts
index ed8ac6acd..efc890580 100644
--- a/server/src/middleware/apiMiddleware.ts
+++ b/server/src/middleware/apiMiddleware.ts
@@ -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,
diff --git a/server/src/middleware/authMiddleware.ts b/server/src/middleware/authMiddleware.ts
index 51e75b3cd..a0b903851 100644
--- a/server/src/middleware/authMiddleware.ts
+++ b/server/src/middleware/authMiddleware.ts
@@ -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),
});
diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts
index f347472d6..9753a23a8 100644
--- a/server/src/utils/auth.ts
+++ b/server/src/utils/auth.ts
@@ -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 });
},
},
}),
diff --git a/server/src/utils/authUtils/afterOrgCreated.ts b/server/src/utils/authUtils/afterOrgCreated.ts
new file mode 100644
index 000000000..bcc251446
--- /dev/null
+++ b/server/src/utils/authUtils/afterOrgCreated.ts
@@ -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;
+ }
+};
diff --git a/server/src/utils/initUtils.ts b/server/src/utils/initUtils.ts
new file mode 100644
index 000000000..5b9d38ddb
--- /dev/null
+++ b/server/src/utils/initUtils.ts
@@ -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");
+ }
+};
diff --git a/server/tests/advanced/usage/usage4.ts b/server/tests/advanced/usage/usage4.ts
index 06dd0e215..e1ac734e3 100644
--- a/server/tests/advanced/usage/usage4.ts
+++ b/server/tests/advanced/usage/usage4.ts
@@ -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,
// });
diff --git a/server/tests/alex/00_setup.ts b/server/tests/alex/00_setup.ts
index 246cfb766..73cd737ac 100644
--- a/server/tests/alex/00_setup.ts
+++ b/server/tests/alex/00_setup.ts
@@ -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,
diff --git a/server/tests/alex/init.ts b/server/tests/alex/init.ts
index 9a1bd22b2..d3338a1d9 100644
--- a/server/tests/alex/init.ts
+++ b/server/tests/alex/init.ts
@@ -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;
diff --git a/server/tests/archives/arrear_prorated/arrear_prorated2.ts b/server/tests/archives/arrear_prorated/arrear_prorated2.ts
index c4b1d23a5..3aae3b373 100644
--- a/server/tests/archives/arrear_prorated/arrear_prorated2.ts
+++ b/server/tests/archives/arrear_prorated/arrear_prorated2.ts
@@ -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
+// });
diff --git a/server/tests/archives/multi_interval2.ts b/server/tests/archives/multi_interval2.ts
deleted file mode 100644
index 4243588c4..000000000
--- a/server/tests/archives/multi_interval2.ts
+++ /dev/null
@@ -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,
-// // });
-// });
-// });
diff --git a/server/tests/before.ts b/server/tests/before.ts
index 36d2897d0..2250ebab8 100644
--- a/server/tests/before.ts
+++ b/server/tests/before.ts
@@ -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;
diff --git a/server/tests/global.ts b/server/tests/global.ts
index 809840853..2aca93eb3 100644
--- a/server/tests/global.ts
+++ b/server/tests/global.ts
@@ -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;
diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts
index 9bf901f0f..caef14d56 100644
--- a/server/tests/utils/stripeUtils.ts
+++ b/server/tests/utils/stripeUtils.ts
@@ -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,
diff --git a/vite/src/app/layout.tsx b/vite/src/app/layout.tsx
index 1e8998955..d1aa69a0f 100644
--- a/vite/src/app/layout.tsx
+++ b/vite/src/app/layout.tsx
@@ -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 (
- {
- const token = await getToken({
- template: "custom_template",
- });
- return token;
- }}
- >
+
diff --git a/vite/src/main.tsx b/vite/src/main.tsx
index 23eff54f0..be7032f0f 100644
--- a/vite/src/main.tsx
+++ b/vite/src/main.tsx
@@ -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(
-
- {process.env.NODE_ENV === "development" ? (
+ {process.env.NODE_ENV === "development" ? (
+
+ ) : (
+
- ) : (
-
-
-
- )}
-
+
+ )}
,
);
diff --git a/vite/src/views/auth/SignIn.tsx b/vite/src/views/auth/SignIn.tsx
index 203cd9998..452300f59 100644
--- a/vite/src/views/auth/SignIn.tsx
+++ b/vite/src/views/auth/SignIn.tsx
@@ -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);
diff --git a/vite/src/views/auth/components/OTPSignIn.tsx b/vite/src/views/auth/components/OTPSignIn.tsx
index 0c6821581..bfa88383c 100644
--- a/vite/src/views/auth/components/OTPSignIn.tsx
+++ b/vite/src/views/auth/components/OTPSignIn.tsx
@@ -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);
diff --git a/vite/src/views/customers/customer/CustomerView.tsx b/vite/src/views/customers/customer/CustomerView.tsx
index 869796fe0..a0c2336f5 100644
--- a/vite/src/views/customers/customer/CustomerView.tsx
+++ b/vite/src/views/customers/customer/CustomerView.tsx
@@ -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";
diff --git a/vite/src/views/customers/customer/add-product/NewProductDropdown.tsx b/vite/src/views/customers/customer/add-product/NewProductDropdown.tsx
index 23003c0fe..86ca60789 100644
--- a/vite/src/views/customers/customer/add-product/NewProductDropdown.tsx
+++ b/vite/src/views/customers/customer/add-product/NewProductDropdown.tsx
@@ -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);
diff --git a/vite/src/views/main-sidebar/org-dropdown/manage-org/OrgLogoUploader.tsx b/vite/src/views/main-sidebar/org-dropdown/manage-org/OrgLogoUploader.tsx
index ffd5dbd57..ebfda3f4b 100644
--- a/vite/src/views/main-sidebar/org-dropdown/manage-org/OrgLogoUploader.tsx
+++ b/vite/src/views/main-sidebar/org-dropdown/manage-org/OrgLogoUploader.tsx
@@ -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 = ({
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);
}
diff --git a/vite/src/views/onboarding/ConnectStripe.tsx b/vite/src/views/onboarding/ConnectStripe.tsx
index 78a820af9..83100fc02 100644
--- a/vite/src/views/onboarding/ConnectStripe.tsx
+++ b/vite/src/views/onboarding/ConnectStripe.tsx
@@ -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",
)}
>
Stripe Connected ✅
@@ -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}
>