chore: made env variables optional
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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" }),
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
server/src/external/caching/CacheManager.ts
vendored
4
server/src/external/caching/CacheManager.ts
vendored
@@ -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, {
|
||||
|
||||
54
server/src/external/logtail/logtailUtils.ts
vendored
54
server/src/external/logtail/logtailUtils.ts
vendored
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
8
server/src/external/resend/resendUtils.ts
vendored
8
server/src/external/resend/resendUtils.ts
vendored
@@ -5,17 +5,19 @@ export const createResendCli = () => {
|
||||
};
|
||||
|
||||
export const sendTextEmail = async ({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
body,
|
||||
}: {
|
||||
from?: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) => {
|
||||
const resend = createResendCli();
|
||||
await resend.emails.send({
|
||||
from: `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
|
||||
from: from || `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: body,
|
||||
@@ -23,17 +25,19 @@ export const sendTextEmail = async ({
|
||||
};
|
||||
|
||||
export const sendHtmlEmail = async ({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
body,
|
||||
}: {
|
||||
from?: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) => {
|
||||
const resend = createResendCli();
|
||||
await resend.emails.send({
|
||||
from: `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
|
||||
from: from || `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
|
||||
to: to,
|
||||
subject: subject,
|
||||
html: body,
|
||||
|
||||
23
server/src/external/resend/safeResend.ts
vendored
Normal file
23
server/src/external/resend/safeResend.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import { logger } from "../logtail/logtailUtils.js";
|
||||
|
||||
export function safeResend<T extends (...args: any[]) => any>({
|
||||
fn,
|
||||
action,
|
||||
}: {
|
||||
fn: T;
|
||||
action: string;
|
||||
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
||||
return async (...args: Parameters<T>) => {
|
||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
|
||||
logger.warn(
|
||||
`RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
logger.error(`Error ${action}: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
23
server/src/external/supabase/safeSb.ts
vendored
Normal file
23
server/src/external/supabase/safeSb.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import { logger } from "../logtail/logtailUtils.js";
|
||||
|
||||
export function safeSb<T extends (...args: any[]) => any>({
|
||||
fn,
|
||||
action,
|
||||
}: {
|
||||
fn: T;
|
||||
action: string;
|
||||
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
||||
return async (...args: Parameters<T>) => {
|
||||
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
|
||||
logger.warn(
|
||||
`SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, skipping ${action}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
logger.error(`Error ${action}: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
14
server/src/external/supabase/storageUtils.ts
vendored
14
server/src/external/supabase/storageUtils.ts
vendored
@@ -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
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js";
|
||||
import { createSupabaseClient } from "../supabaseUtils.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { safeSb } from "./safeSb.js";
|
||||
|
||||
export const subscribeToOrgUpdates = ({ db }: { db: DrizzleCli }) => {
|
||||
try {
|
||||
const sb = createSupabaseClient();
|
||||
sb.channel("table-db-changes")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{
|
||||
event: "UPDATE",
|
||||
schema: "public",
|
||||
table: "organizations",
|
||||
},
|
||||
async (payload) => {
|
||||
// await clearOrgCache(payload.new.id);
|
||||
try {
|
||||
await clearOrgCache({ db, orgId: payload.new.id });
|
||||
} catch (error) {
|
||||
console.warn("Error clearing org cache:", error);
|
||||
}
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
} catch (error) {
|
||||
console.warn("Error subscribing to org updates:", error);
|
||||
}
|
||||
};
|
||||
export const subscribeToOrgUpdates = safeSb({
|
||||
fn: ({ db }: { db: DrizzleCli }) => {
|
||||
try {
|
||||
const sb = createSupabaseClient();
|
||||
sb.channel("table-db-changes")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{
|
||||
event: "UPDATE",
|
||||
schema: "public",
|
||||
table: "organizations",
|
||||
},
|
||||
async (payload) => {
|
||||
try {
|
||||
await clearOrgCache({ db, orgId: payload.new.id });
|
||||
} catch (error) {
|
||||
console.warn("Error clearing org cache:", error);
|
||||
}
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
} catch (error) {
|
||||
console.warn("Error subscribing to org updates:", error);
|
||||
}
|
||||
},
|
||||
action: "subscribe to org updates",
|
||||
});
|
||||
|
||||
68
server/src/external/svix/svixHelpers.ts
vendored
Normal file
68
server/src/external/svix/svixHelpers.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
import { AppEnv, Organization } from "@autumn/shared";
|
||||
import { createSvixCli, getSvixAppId, safeSvix } from "./svixUtils.js";
|
||||
|
||||
export const createSvixApp = safeSvix({
|
||||
fn: async ({
|
||||
name,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
name: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
const app = await svix.application.create({
|
||||
name,
|
||||
metadata: {
|
||||
org_id: orgId,
|
||||
env,
|
||||
},
|
||||
});
|
||||
return app;
|
||||
},
|
||||
action: "createSvixApp",
|
||||
});
|
||||
|
||||
export const deleteSvixApp = safeSvix({
|
||||
fn: async ({ appId }: { appId: string }) => {
|
||||
const svix = createSvixCli();
|
||||
await svix.application.delete(appId);
|
||||
},
|
||||
action: "deleteSvixApp",
|
||||
});
|
||||
|
||||
export const sendSvixEvent = safeSvix({
|
||||
fn: async ({
|
||||
org,
|
||||
env,
|
||||
eventType,
|
||||
data,
|
||||
}: {
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
eventType: string;
|
||||
data: any;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
return await svix.message.create(getSvixAppId({ org, env }), {
|
||||
eventType,
|
||||
payload: {
|
||||
type: eventType,
|
||||
data,
|
||||
},
|
||||
});
|
||||
},
|
||||
action: "sendSvixEvent",
|
||||
});
|
||||
|
||||
export const getSvixDashboardUrl = safeSvix({
|
||||
fn: async ({ org, env }: { org: Organization; env: AppEnv }) => {
|
||||
const appId = getSvixAppId({ org, env });
|
||||
|
||||
const svix = createSvixCli();
|
||||
const dashboard = await svix.authentication.appPortalAccess(appId, {});
|
||||
return dashboard.url;
|
||||
},
|
||||
action: "getSvixDashboardUrl",
|
||||
});
|
||||
81
server/src/external/svix/svixUtils.ts
vendored
81
server/src/external/svix/svixUtils.ts
vendored
@@ -1,11 +1,32 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { Organization } from "@autumn/shared";
|
||||
import { Svix } from "svix";
|
||||
import { logger } from "../logtail/logtailUtils.js";
|
||||
|
||||
export const createSvixCli = () => {
|
||||
return new Svix(process.env.SVIX_API_KEY as string);
|
||||
};
|
||||
|
||||
export function safeSvix<T extends (...args: any[]) => any>({
|
||||
fn,
|
||||
action,
|
||||
}: {
|
||||
fn: T;
|
||||
action: string;
|
||||
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
||||
return async (...args: Parameters<T>) => {
|
||||
if (!process.env.SVIX_API_KEY) {
|
||||
logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
logger.error(`Error ${action}: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getSvixAppId = ({
|
||||
org,
|
||||
env,
|
||||
@@ -18,63 +39,3 @@ export const getSvixAppId = ({
|
||||
? svixConfig.live_app_id
|
||||
: svixConfig.sandbox_app_id;
|
||||
};
|
||||
|
||||
export const createSvixApp = async ({
|
||||
name,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
name: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
const app = await svix.application.create({
|
||||
name,
|
||||
metadata: {
|
||||
org_id: orgId,
|
||||
env,
|
||||
},
|
||||
});
|
||||
return app;
|
||||
};
|
||||
|
||||
export const deleteSvixApp = async ({ appId }: { appId: string }) => {
|
||||
const svix = createSvixCli();
|
||||
await svix.application.delete(appId);
|
||||
};
|
||||
|
||||
export const sendSvixEvent = async ({
|
||||
org,
|
||||
env,
|
||||
eventType,
|
||||
data,
|
||||
}: {
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
eventType: string;
|
||||
data: any;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
return await svix.message.create(getSvixAppId({ org, env }), {
|
||||
eventType,
|
||||
payload: {
|
||||
type: eventType,
|
||||
data,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getSvixDashboardUrl = async ({
|
||||
org,
|
||||
env,
|
||||
}: {
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const appId = getSvixAppId({ org, env });
|
||||
|
||||
const svix = createSvixCli();
|
||||
const dashboard = await svix.authentication.appPortalAccess(appId, {});
|
||||
return dashboard.url;
|
||||
};
|
||||
|
||||
59
server/src/external/unkeyUtils.ts
vendored
59
server/src/external/unkeyUtils.ts
vendored
@@ -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;
|
||||
// };
|
||||
283
server/src/external/webhooks/clerkWebhooks.ts
vendored
283
server/src/external/webhooks/clerkWebhooks.ts
vendored
@@ -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",
|
||||
// });
|
||||
// };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
// };
|
||||
// }
|
||||
|
||||
@@ -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,
|
||||
// };
|
||||
|
||||
@@ -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";
|
||||
|
||||
40
server/src/internal/emails/sendInvitationEmail.ts
Normal file
40
server/src/internal/emails/sendInvitationEmail.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { sendTextEmail } from "@/external/resend/resendUtils.js";
|
||||
import { safeResend } from "@/external/resend/safeResend.js";
|
||||
|
||||
const getInvitationEmailBody = ({
|
||||
orgName,
|
||||
inviteLink,
|
||||
}: {
|
||||
orgName: string;
|
||||
inviteLink: string;
|
||||
}) => {
|
||||
return `Hey there! You've been invited to join ${orgName} on Autumn.
|
||||
|
||||
Click the link below to create an account / sign in and you'll be automatically added to the organization.
|
||||
|
||||
${process.env.CLIENT_URL}/sign-in
|
||||
`;
|
||||
};
|
||||
|
||||
export const sendInvitationEmail = safeResend({
|
||||
fn: async ({
|
||||
email,
|
||||
|
||||
orgName,
|
||||
inviteLink,
|
||||
}: {
|
||||
email: string;
|
||||
|
||||
orgName: string;
|
||||
inviteLink: string;
|
||||
}) => {
|
||||
console.log("Sending invitation email to", email);
|
||||
await sendTextEmail({
|
||||
from: `Autumn <hey@${process.env.RESEND_DOMAIN}>`,
|
||||
to: email,
|
||||
subject: `Join ${orgName} on Autumn`,
|
||||
body: getInvitationEmailBody({ orgName, inviteLink }),
|
||||
});
|
||||
},
|
||||
action: "send org invitation email",
|
||||
});
|
||||
49
server/src/internal/emails/sendMigrationEmail.ts
Normal file
49
server/src/internal/emails/sendMigrationEmail.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { MigrationService } from "../migrations/MigrationService.js";
|
||||
import { sendTextEmail } from "@/external/resend/resendUtils.js";
|
||||
import { MigrationJobStep, Organization } from "@autumn/shared";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { safeResend } from "@/external/resend/safeResend.js";
|
||||
|
||||
export const sendMigrationEmail = safeResend({
|
||||
fn: async ({
|
||||
db,
|
||||
migrationJobId,
|
||||
org,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
migrationJobId: string;
|
||||
org: Organization;
|
||||
}) => {
|
||||
let migrationJob = await MigrationService.getJob({
|
||||
db,
|
||||
id: migrationJobId,
|
||||
});
|
||||
|
||||
// Send email
|
||||
let getCustomersStep =
|
||||
migrationJob.step_details[MigrationJobStep.GetCustomers];
|
||||
let migrateStep =
|
||||
migrationJob.step_details[MigrationJobStep.MigrateCustomers];
|
||||
|
||||
console.log("Sending migration email");
|
||||
await sendTextEmail({
|
||||
to: "johnyeocx@gmail.com",
|
||||
subject: `Migration Job Finished -- ${migrationJob.id}`,
|
||||
body: `
|
||||
|
||||
ORG: ${org.id}, ${org.slug}
|
||||
Step: Get migration customers
|
||||
|
||||
1. Total customers: ${getCustomersStep?.total_customers}
|
||||
2. Canceled customers: ${getCustomersStep?.canceled_customers}
|
||||
|
||||
Step: Migrate customers
|
||||
|
||||
1. Number of errors: ${migrateStep?.num_errors}
|
||||
2. Failed customers:
|
||||
${migrateStep?.failed_customers}
|
||||
`,
|
||||
});
|
||||
},
|
||||
action: "send migration email",
|
||||
});
|
||||
@@ -1,9 +1,14 @@
|
||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
||||
import { createResendCli } from "@/external/resend/resendUtils.js";
|
||||
import OTPEmail from "@emails/OTPEmail.js";
|
||||
|
||||
const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => {
|
||||
const resend = createResendCli();
|
||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
|
||||
logger.warn(`RESEND NOT SET UP, SIGN IN OTP: ${otp}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const resend = createResendCli();
|
||||
await resend.emails.send({
|
||||
from: `Autumn <hey@${process.env.RESEND_DOMAIN}>`,
|
||||
to: email,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ClerkClient } from "@clerk/express";
|
||||
import { sendHtmlEmail, sendTextEmail } from "../resend/resendUtils.js";
|
||||
import { sendHtmlEmail } from "@/external/resend/resendUtils.js";
|
||||
import { safeResend } from "@/external/resend/safeResend.js";
|
||||
|
||||
const getWelcomeEmailBody = (userFirstName: string) => {
|
||||
return `
|
||||
@@ -18,20 +18,15 @@ Co-founder, Autumn</p>
|
||||
`;
|
||||
};
|
||||
|
||||
export const sendOnboardingEmail = async ({
|
||||
name,
|
||||
email,
|
||||
}: {
|
||||
name: string;
|
||||
email: string;
|
||||
}) => {
|
||||
console.log("Sending onboarding email to", email);
|
||||
export const sendOnboardingEmail = safeResend({
|
||||
fn: async ({ name, email }: { name: string; email: string }) => {
|
||||
const firstName = name.split(" ")[0];
|
||||
|
||||
const firstName = name.split(" ")[0];
|
||||
|
||||
await sendHtmlEmail({
|
||||
to: email,
|
||||
subject: "Anything I can help with?",
|
||||
body: getWelcomeEmailBody(firstName),
|
||||
});
|
||||
};
|
||||
await sendHtmlEmail({
|
||||
to: email,
|
||||
subject: "Anything I can help with?",
|
||||
body: getWelcomeEmailBody(firstName),
|
||||
});
|
||||
},
|
||||
action: "send onboarding email",
|
||||
});
|
||||
@@ -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})`,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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}
|
||||
`,
|
||||
});
|
||||
};
|
||||
@@ -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 }),
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
83
server/src/utils/authUtils/afterOrgCreated.ts
Normal file
83
server/src/utils/authUtils/afterOrgCreated.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { db } from "@/db/initDrizzle.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { generatePublishableKey } from "../encryptUtils.js";
|
||||
import { createSvixApp } from "@/external/svix/svixHelpers.js";
|
||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
||||
import { Organization } from "better-auth/plugins";
|
||||
|
||||
export const initOrgSvixApps = async ({
|
||||
id,
|
||||
slug,
|
||||
}: {
|
||||
id: string;
|
||||
slug: string;
|
||||
}) => {
|
||||
const batchCreate = [];
|
||||
batchCreate.push(
|
||||
createSvixApp({
|
||||
name: `${slug}_${AppEnv.Sandbox}`,
|
||||
orgId: id,
|
||||
env: AppEnv.Sandbox,
|
||||
}),
|
||||
);
|
||||
batchCreate.push(
|
||||
createSvixApp({
|
||||
name: `${slug}_${AppEnv.Live}`,
|
||||
orgId: id,
|
||||
env: AppEnv.Live,
|
||||
}),
|
||||
);
|
||||
|
||||
const [sandboxApp, liveApp] = await Promise.all(batchCreate);
|
||||
|
||||
return { sandboxApp, liveApp };
|
||||
};
|
||||
|
||||
export const afterOrgCreated = async ({ org }: { org: Organization }) => {
|
||||
logger.info(`Org created: ${org.id} (${org.slug})`);
|
||||
|
||||
const { id, slug, createdAt } = org;
|
||||
|
||||
try {
|
||||
await OrgService.update({
|
||||
db,
|
||||
orgId: id,
|
||||
updates: {
|
||||
created_at: createdAt.getTime(),
|
||||
},
|
||||
});
|
||||
|
||||
// 1. Create svix webhoooks
|
||||
const { sandboxApp, liveApp } = await initOrgSvixApps({
|
||||
slug,
|
||||
id,
|
||||
});
|
||||
|
||||
await OrgService.update({
|
||||
db,
|
||||
orgId: id,
|
||||
updates: {
|
||||
svix_config: {
|
||||
sandbox_app_id: sandboxApp?.id,
|
||||
live_app_id: liveApp?.id,
|
||||
},
|
||||
test_pkey: generatePublishableKey(AppEnv.Sandbox),
|
||||
live_pkey: generatePublishableKey(AppEnv.Live),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`Initialized resources for org ${id} (${slug})`);
|
||||
} catch (error: any) {
|
||||
if (error?.data && error.data.code == "23505") {
|
||||
logger.error(
|
||||
`Org ${id} already exists in Supabase -- skipping creationg`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
logger.error(
|
||||
`Failed to insert org. Code: ${error.code}, message: ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
39
server/src/utils/initUtils.ts
Normal file
39
server/src/utils/initUtils.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
||||
import "dotenv/config";
|
||||
|
||||
export const checkEnvVars = () => {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error(`DATABASE_URL is not set`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.ENCRYPTION_IV || !process.env.ENCRYPTION_PASSWORD) {
|
||||
console.error(
|
||||
`ENCRYPTION_IV or ENCRYPTION_PASSWORD is not set (used for Stripe key encryption)`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.REDIS_URL) {
|
||||
console.error(`REDIS_URL is not set`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.BETTER_AUTH_SECRET || !process.env.BETTER_AUTH_URL) {
|
||||
console.error(`BETTER_AUTH_SECRET or BETTER_AUTH_URL is not set`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
|
||||
logger.warn(
|
||||
"RESEND_API_KEY or RESEND_DOMAIN is not set (use terminal for sign in OTP)",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!process.env.LOGTAIL_SOURCE_TOKEN ||
|
||||
!process.env.LOGTAIL_INGESTING_HOST
|
||||
) {
|
||||
logger.warn("LOGTAIL ENV VARs not found, skipping logtail");
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
// });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
// });
|
||||
|
||||
@@ -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,
|
||||
// // });
|
||||
// });
|
||||
// });
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,15 +12,11 @@ import { usePostHog } from "posthog-js/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowUpRightFromSquare } from "lucide-react";
|
||||
import { AutumnProvider } from "autumn-js/react";
|
||||
import { useAuth } from "@clerk/clerk-react";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import { CustomToaster } from "@/components/general/CustomToaster";
|
||||
|
||||
export function MainLayout() {
|
||||
const env = useEnv();
|
||||
|
||||
const { getToken } = useAuth();
|
||||
const { pathname } = useLocation();
|
||||
const { data, isPending } = useSession();
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -88,16 +84,7 @@ export function MainLayout() {
|
||||
// }
|
||||
|
||||
return (
|
||||
<AutumnProvider
|
||||
includeCredentials={false}
|
||||
backendUrl={import.meta.env.VITE_BACKEND_URL}
|
||||
getBearerToken={async () => {
|
||||
const token = await getToken({
|
||||
template: "custom_template",
|
||||
});
|
||||
return token;
|
||||
}}
|
||||
>
|
||||
<AutumnProvider backendUrl={import.meta.env.VITE_BACKEND_URL}>
|
||||
<main className="w-screen h-screen flex bg-stone-100">
|
||||
<CustomToaster />
|
||||
<MainSidebar />
|
||||
|
||||
@@ -3,33 +3,25 @@ import App from "./App";
|
||||
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ClerkProvider } from "@clerk/clerk-react";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
|
||||
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
|
||||
if (!PUBLISHABLE_KEY) {
|
||||
throw new Error("Add your Clerk Publishable Key to the .env file");
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
|
||||
{process.env.NODE_ENV === "development" ? (
|
||||
{process.env.NODE_ENV === "development" ? (
|
||||
<App />
|
||||
) : (
|
||||
<PostHogProvider
|
||||
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
|
||||
options={{
|
||||
// autocapture: false,
|
||||
// capture_pageview: false,
|
||||
// capture_pageleave: false,
|
||||
// session_recording: {}
|
||||
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
) : (
|
||||
<PostHogProvider
|
||||
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
|
||||
options={{
|
||||
// autocapture: false,
|
||||
// capture_pageview: false,
|
||||
// capture_pageleave: false,
|
||||
// session_recording: {}
|
||||
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</PostHogProvider>
|
||||
)}
|
||||
</ClerkProvider>
|
||||
</PostHogProvider>
|
||||
)}
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import axios from "axios";
|
||||
import { useOrg } from "@/hooks/useOrg";
|
||||
import { getOrgLogoUrl } from "@/utils/orgUtils";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
|
||||
const MAX_SIZE_MB = 10;
|
||||
const MAX_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024;
|
||||
@@ -89,7 +90,7 @@ const OrgLogoUploader: React.FC<OrgLogoUploaderProps> = ({
|
||||
setLogoVersion(logoVersion + 1);
|
||||
toast.success("Successfully uploaded logo");
|
||||
} catch (error) {
|
||||
toast.error("Failed to upload logo");
|
||||
toast.error(getBackendErr(error, "Failed to upload logo"));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import SmallSpinner from "@/components/general/SmallSpinner";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faStripeS } from "@fortawesome/free-brands-svg-icons";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useOrg } from "@/hooks/useOrg";
|
||||
|
||||
function ConnectStripe({
|
||||
className,
|
||||
@@ -45,29 +46,19 @@ function ConnectStripe({
|
||||
className?: string;
|
||||
onboarding?: boolean;
|
||||
}) {
|
||||
const axiosInstance = useAxiosInstance({ env: AppEnv.Live });
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { org, mutate, isLoading: isOrgLoading } = useOrg();
|
||||
const [searchParams] = useSearchParams();
|
||||
const redirect = searchParams.get("redirect");
|
||||
|
||||
const axiosInstance = useAxiosInstance({ env: AppEnv.Live });
|
||||
|
||||
const [testApiKey, setTestApiKey] = useState("");
|
||||
const [liveApiKey, setLiveApiKey] = useState("");
|
||||
const [successUrl, setSuccessUrl] = useState("https://useautumn.com");
|
||||
const [defaultCurrency, setDefaultCurrency] = useState("USD");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const {
|
||||
data: orgData,
|
||||
mutate: mutateOrg,
|
||||
isLoading: isOrgLoading,
|
||||
} = useAxiosSWR({
|
||||
url: `/organization`,
|
||||
env: AppEnv.Live,
|
||||
});
|
||||
|
||||
const org = orgData?.org;
|
||||
|
||||
const handleConnectStripe = async () => {
|
||||
if (!testApiKey || !successUrl || !defaultCurrency) {
|
||||
toast.error("Please fill in all fields");
|
||||
@@ -90,7 +81,7 @@ function ConnectStripe({
|
||||
});
|
||||
|
||||
toast.success("Successfully connected to Stripe");
|
||||
await mutateOrg();
|
||||
await mutate();
|
||||
if (redirect && !onboarding) {
|
||||
navigate(redirect);
|
||||
}
|
||||
@@ -107,7 +98,7 @@ function ConnectStripe({
|
||||
try {
|
||||
setIsDisconnecting(true);
|
||||
await OrgService.disconnectStripe(axiosInstance);
|
||||
await mutateOrg();
|
||||
await mutate();
|
||||
toast.success("Successfully disconnected from Stripe");
|
||||
} catch (error) {
|
||||
toast.error(getBackendErr(error, "Failed to disconnect Stripe"));
|
||||
@@ -125,7 +116,7 @@ function ConnectStripe({
|
||||
className={cn(
|
||||
"flex flex-col gap-4",
|
||||
className,
|
||||
onboarding && "flex-row justify-between items-center"
|
||||
onboarding && "flex-row justify-between items-center",
|
||||
)}
|
||||
>
|
||||
<p className="text-t3 text-sm">Stripe Connected ✅</p>
|
||||
@@ -222,7 +213,7 @@ export const CurrencySelect = ({
|
||||
"w-full justify-between transition-colors duration-100",
|
||||
open &&
|
||||
"border-[rgb(139,92,246)] shadow-[0_0_2px_1px_rgba(139,92,246,0.25)]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user