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} >