fix: coupons on new stripe version

This commit is contained in:
John Yeo
2025-11-16 18:52:54 +00:00
parent ce8616a00b
commit b865a958ab
160 changed files with 935 additions and 6565 deletions

View File

@@ -22,7 +22,7 @@
"setup": "node scripts/setup/setup.js", "setup": "node scripts/setup/setup.js",
"setup:test": "bun scripts/setup/setup-test.ts", "setup:test": "bun scripts/setup/setup-test.ts",
"tests": "bun scripts/test.ts", "tests": "infisical run --env=dev -- bun scripts/test.ts",
"setupci": "node scripts/setup/setupci.js", "setupci": "node scripts/setup/setupci.js",
"replicate": "bun scripts/db/replicate.ts", "replicate": "bun scripts/db/replicate.ts",

View File

@@ -2,8 +2,11 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { existsSync, readdirSync, statSync } from "node:fs"; import { existsSync, readdirSync, statSync } from "node:fs";
import { join, relative, resolve } from "node:path"; import { join, relative, resolve } from "node:path";
import { loadLocalEnv } from "@server/utils/envUtils.js";
import chalk from "chalk"; import chalk from "chalk";
loadLocalEnv();
/** /**
* Recursively finds all test files in a directory * Recursively finds all test files in a directory
*/ */
@@ -267,46 +270,17 @@ async function runTest() {
const frameworkLabel = framework === "bun" ? "Bun" : "Mocha"; const frameworkLabel = framework === "bun" ? "Bun" : "Mocha";
console.log(chalk.cyan(`🧪 Running test file with ${frameworkLabel}...\n`)); console.log(chalk.cyan(`🧪 Running test file with ${frameworkLabel}...\n`));
if (framework !== "bun") {
console.error(chalk.red("❌ Mocha tests are deprecated"));
process.exit(1);
}
// Run the test file with the appropriate framework, wrapped with Infisical // Run the test file with the appropriate framework, wrapped with Infisical
const child = const child = spawn("bun", ["test", "--timeout", "0", testFile.relative], {
framework === "bun" cwd: serverDir,
? spawn( stdio: "inherit",
"infisical", env: { ...process.env, NODE_ENV: "production" },
[ });
"run",
"--env=dev",
"--",
"bun",
"test",
"--timeout",
"0",
testFile.relative,
],
{
cwd: serverDir,
stdio: "inherit",
env: { ...process.env, NODE_ENV: "production" },
},
)
: spawn(
"infisical",
[
"run",
"--env=dev",
"--",
"npx",
"mocha",
"--bail",
"--timeout",
"10000000",
testFile.relative,
],
{
cwd: serverDir,
stdio: "inherit",
env: { ...process.env, NODE_ENV: "production" },
},
);
// Store the process group ID // Store the process group ID
const pgid = child.pid; const pgid = child.pid;
@@ -363,3 +337,25 @@ async function runTest() {
} }
runTest(); runTest();
// framework === "bun"
// ?
// : spawn(
// "infisical",
// [
// "run",
// "--env=dev",
// "--",
// "npx",
// "mocha",
// "--bail",
// "--timeout",
// "10000000",
// testFile.relative,
// ],
// {
// cwd: serverDir,
// stdio: "inherit",
// env: { ...process.env, NODE_ENV: "production" },
// },
// );

View File

@@ -14,28 +14,28 @@ fi
# Run tests using TypeScript runner with compact mode # Run tests using TypeScript runner with compact mode
# Adjust --max to control concurrency (default: 6) # Adjust --max to control concurrency (default: 6)
# BUN_PARALLEL_COMPACT \
# 'server/tests/balances/check/basic' \
# 'server/tests/balances/check/credit-systems' \
# 'server/tests/balances/check/misc' \
# 'server/tests/balances/check/prepaid' \
# 'server/tests/balances/track/basic' \
# 'server/tests/balances/track/credit-systems' \
# 'server/tests/balances/track/entity-products' \
# 'server/tests/balances/track/legacy' \
# 'server/tests/balances/track/allocated' \
# 'server/tests/balances/track/entity-balances' \
# 'server/tests/balances/track/concurrency' \
BUN_PARALLEL_COMPACT \ BUN_PARALLEL_COMPACT \
'server/tests/attach/basic' \ 'server/tests/balances/check/basic' \
'server/tests/attach/entities' \ 'server/tests/balances/check/credit-systems' \
'server/tests/attach/upgrade' \ 'server/tests/balances/check/misc' \
'server/tests/attach/downgrade' \ 'server/tests/balances/check/prepaid' \
'server/tests/attach/free' \ 'server/tests/balances/track/basic' \
'server/tests/attach/addOn' \ 'server/tests/balances/track/credit-systems' \
'server/tests/attach/entities' \ 'server/tests/balances/track/entity-products' \
'server/tests/attach/checkout' \ 'server/tests/balances/track/legacy' \
'server/tests/attach/misc' \ 'server/tests/balances/track/allocated' \
--max=6 \ 'server/tests/balances/track/entity-balances' \
'server/tests/balances/track/concurrency' \
# BUN_PARALLEL_COMPACT \
# 'server/tests/attach/basic' \
# 'server/tests/attach/entities' \
# 'server/tests/attach/upgrade' \
# 'server/tests/attach/downgrade' \
# 'server/tests/attach/free' \
# 'server/tests/attach/addOn' \
# 'server/tests/attach/entities' \
# 'server/tests/attach/checkout' \
# 'server/tests/attach/misc' \
# --max=6 \

View File

@@ -14,15 +14,15 @@ fi
# BUN_PARALLEL_COMPACT \ BUN_PARALLEL_COMPACT \
# 'server/tests/attach/migrations' \ 'server/tests/attach/migrations' \
# 'server/tests/attach/others' \ 'server/tests/attach/others' \
# 'server/tests/attach/newVersion' \ 'server/tests/attach/newVersion' \
# 'server/tests/attach/upgradeOld' \ 'server/tests/attach/upgradeOld' \
# 'server/tests/attach/updateEnts' \ 'server/tests/attach/updateEnts' \
# 'server/tests/advanced/check' \ 'server/tests/advanced/check' \
# 'server/tests/attach/prepaid' \ 'server/tests/attach/prepaid' \
# 'server/tests/interval/upgrade' \ 'server/tests/interval/upgrade' \
# 'server/tests/interval/multiSub' \ 'server/tests/interval/multiSub' \
# --max=6 --max=6

View File

@@ -31,8 +31,8 @@ BUN_PARALLEL_COMPACT \
--max=6 --max=6
# BUN_PARALLEL_COMPACT \ BUN_PARALLEL_COMPACT \
# 'server/tests/advanced/usage' 'server/tests/advanced/usage'
# 'server/tests/crud/plan' # 'server/tests/crud/plan'
# 'server/tests/advanced/referrals/paid' \ # 'server/tests/advanced/referrals/paid' \

View File

@@ -1,11 +1,14 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { readdir } from "node:fs/promises";
import { basename, resolve } from "node:path";
import { loadLocalEnv } from "@server/utils/envUtils.js";
import { spawn } from "bun"; import { spawn } from "bun";
import chalk from "chalk"; import chalk from "chalk";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { readdir } from "fs/promises";
import pLimit from "p-limit"; import pLimit from "p-limit";
import { basename, resolve } from "path";
loadLocalEnv();
// Load environment variables from server/.env // Load environment variables from server/.env
dotenv.config({ path: resolve(process.cwd(), "server", ".env") }); dotenv.config({ path: resolve(process.cwd(), "server", ".env") });

View File

@@ -6,13 +6,14 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"email": "email dev -p 3001", "email": "email dev -p 3001",
"start": "bun src/index.ts",
"d": "ENV_FILE=.env infisical run --env=dev -- bun dev", "d": "ENV_FILE=.env infisical run --env=dev -- bun dev",
"p": "ENV_FILE=.env.prod infisical run --env=prod -- bun dev", "p": "ENV_FILE=.env.prod infisical run --env=prod -- bun dev",
"w": "ENV_FILE=.env infisical run --env=dev -- bun workers:dev", "w": "ENV_FILE=.env infisical run --env=dev -- bun workers:dev",
"c": "ENV_FILE=.env infisical run --env=dev -- bun cron", "c": "ENV_FILE=.env infisical run --env=dev -- bun cron",
"dev": "cross-env NODE_ENV=development bunx nodemon", "dev": "cross-env NODE_ENV=development bunx nodemon",
"workers:dev": "cross-env NODE_ENV=development bunx nodemon --exec bun src/workers.ts --signal SIGTERM --delay 500ms", "workers:dev": "cross-env NODE_ENV=development bunx nodemon --exec bun src/workers.ts --signal SIGTERM --delay 500ms",
"start": "bun src/index.ts",
"workers": "bun src/workers.ts", "workers": "bun src/workers.ts",
"cron": "bun src/cron.ts", "cron": "bun src/cron.ts",
"check": "bun src/check.ts", "check": "bun src/check.ts",

View File

@@ -4,8 +4,6 @@
filename="$1" filename="$1"
# Check if the file path contains "shell"
if [[ "$filename" == *"shell"* ]]; then if [[ "$filename" == *"shell"* ]]; then
"$filename" "${@:2}" "$filename" "${@:2}"
@@ -15,7 +13,7 @@ elif [[ "$filename" == *"/tests/"* ]]; then
# Remove .ts extension if present # Remove .ts extension if present
path_after_tests="${path_after_tests%.ts}" path_after_tests="${path_after_tests%.ts}"
# Use scripts/test.ts which auto-detects framework # Use scripts/test.ts which auto-detects framework
bun ../scripts/test.ts "$path_after_tests" infisical run --env=dev -- bun ../scripts/test.ts "$path_after_tests"
elif [[ "$filename" == *".sh"* ]]; then elif [[ "$filename" == *".sh"* ]]; then
"$filename" "$filename"

View File

@@ -41,6 +41,7 @@ for _, entityWrapper in ipairs(entities) do
created_at = entityData.created_at, created_at = entityData.created_at,
env = entityData.env, env = entityData.env,
subscriptions = entityData.subscriptions, subscriptions = entityData.subscriptions,
legacyData = entityData.legacyData,
_balanceFeatureIds = balanceFeatureIds _balanceFeatureIds = balanceFeatureIds
} }

View File

@@ -43,6 +43,7 @@ local baseEntity = {
created_at = entityData.created_at, created_at = entityData.created_at,
env = entityData.env, env = entityData.env,
subscriptions = entityData.subscriptions, subscriptions = entityData.subscriptions,
legacyData = entityData.legacyData,
_balanceFeatureIds = balanceFeatureIds _balanceFeatureIds = balanceFeatureIds
} }

View File

@@ -7,7 +7,6 @@ import { resetCustomerEntitlement } from "./cron/cronUtils.js";
import { runProductCron } from "./cron/productCron/runProductCron.js"; import { runProductCron } from "./cron/productCron/runProductCron.js";
import { initDrizzle } from "./db/initDrizzle.js"; import { initDrizzle } from "./db/initDrizzle.js";
import { CusEntService } from "./internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { CusEntService } from "./internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { OrgService } from "./internal/orgs/OrgService.js";
import { notNullish } from "./utils/genUtils.js"; import { notNullish } from "./utils/genUtils.js";
dotenv.config(); dotenv.config();
@@ -26,8 +25,6 @@ export const cronTask = async () => {
batchSize: 500, batchSize: 500,
}); });
const cacheEnabledOrgs = await OrgService.getCacheEnabledOrgs({ db });
const batchSize = 100; const batchSize = 100;
for (let i = 0; i < cusEnts.length; i += batchSize) { for (let i = 0; i < cusEnts.length; i += batchSize) {
const batch = cusEnts.slice(i, i + batchSize); const batch = cusEnts.slice(i, i + batchSize);
@@ -37,7 +34,6 @@ export const cronTask = async () => {
resetCustomerEntitlement({ resetCustomerEntitlement({
db, db,
cusEnt: cusEnt, cusEnt: cusEnt,
cacheEnabledOrgs,
}), }),
); );
} }

View File

@@ -94,11 +94,9 @@ const checkSubAnchor = async ({
const handleShortDurationCusEnt = async ({ const handleShortDurationCusEnt = async ({
db, db,
cusEnt, cusEnt,
cacheEnabledOrgs,
}: { }: {
db: DrizzleCli; db: DrizzleCli;
cusEnt: ResetCusEnt; cusEnt: ResetCusEnt;
cacheEnabledOrgs: any[];
}) => { }) => {
const ent = cusEnt.entitlement as FullEntitlement; const ent = cusEnt.entitlement as FullEntitlement;
@@ -155,11 +153,9 @@ const shortDurations = [EntInterval.Minute, EntInterval.Hour, EntInterval.Day];
export const resetCustomerEntitlement = async ({ export const resetCustomerEntitlement = async ({
db, db,
cusEnt, cusEnt,
cacheEnabledOrgs,
}: { }: {
db: DrizzleCli; db: DrizzleCli;
cusEnt: ResetCusEnt; cusEnt: ResetCusEnt;
cacheEnabledOrgs: any[];
}) => { }) => {
try { try {
const ent = cusEnt.entitlement as FullEntitlement; const ent = cusEnt.entitlement as FullEntitlement;
@@ -171,7 +167,6 @@ export const resetCustomerEntitlement = async ({
return await handleShortDurationCusEnt({ return await handleShortDurationCusEnt({
db, db,
cusEnt, cusEnt,
cacheEnabledOrgs,
}); });
} }
@@ -299,10 +294,6 @@ export const resetCustomerEntitlement = async ({
)}`, )}`,
); );
// let cacheOrg = cacheEnabledOrgs.find(
// (org) => org.id === cusEnt.customer.org_id
// );
const org = await OrgService.get({ const org = await OrgService.get({
db, db,
orgId: cusEnt.customer.org_id, orgId: cusEnt.customer.org_id,

View File

@@ -1,4 +1,4 @@
import type { Many, One, Relations } from "drizzle-orm"; import type { Relations } from "drizzle-orm";
import type { PgTable } from "drizzle-orm/pg-core"; import type { PgTable } from "drizzle-orm/pg-core";
export interface RelationPath { export interface RelationPath {

View File

@@ -34,7 +34,6 @@ export function generateArrayAggSQL({
alias, alias,
filter, filter,
orderBy, orderBy,
limit,
distinct = false, distinct = false,
}: ArrayAggregationConfig): SQL { }: ArrayAggregationConfig): SQL {
const tableAlias = alias || getTableAlias(table); const tableAlias = alias || getTableAlias(table);
@@ -81,34 +80,33 @@ export function generateRowSubquerySQL({
return query; return query;
} }
/** // /**
* Generate SQL for many-to-many join through junction table // * Generate SQL for many-to-many join through junction table
* Example: // * Example:
* SELECT json_agg(o) // * SELECT json_agg(o)
* FROM member m // * FROM member m
* INNER JOIN organizations o ON o.id = m.organization_id // * INNER JOIN organizations o ON o.id = m.organization_id
* WHERE m.user_id = ${userId} // * WHERE m.user_id = ${userId}
*/ // */
export function generateJunctionJoinSQL({ // export function generateJunctionJoinSQL({
junctionTable, // junctionTable,
fromField, // fromField,
toField, // toField,
fromTable, // toTable,
toTable, // fromId,
fromId, // }: JunctionJoinConfig): SQL {
}: JunctionJoinConfig): SQL { // const junctionAlias = getTableAlias(junctionTable);
const junctionAlias = getTableAlias(junctionTable); // const toAlias = getTableAlias(toTable);
const toAlias = getTableAlias(toTable); // const junctionTableName = getTableName(junctionTable);
const junctionTableName = getTableName(junctionTable); // const toTableName = getTableName(toTable);
const toTableName = getTableName(toTable);
return sql` // return sql`
FROM ${sql.identifier(junctionTableName)} ${sql.identifier(junctionAlias)} // FROM ${sql.identifier(junctionTableName)} ${sql.identifier(junctionAlias)}
INNER JOIN ${sql.identifier(toTableName)} ${sql.identifier(toAlias)} // INNER JOIN ${sql.identifier(toTableName)} ${sql.identifier(toAlias)}
ON ${sql.identifier(toAlias)}.${sql.identifier("id")} = ${sql.identifier(junctionAlias)}.${sql.identifier(toField)} // ON ${sql.identifier(toAlias)}.${sql.identifier("id")} = ${sql.identifier(junctionAlias)}.${sql.identifier(toField)}
WHERE ${sql.identifier(junctionAlias)}.${sql.identifier(fromField)} = ${fromId} // WHERE ${sql.identifier(junctionAlias)}.${sql.identifier(fromField)} = ${fromId}
`; // `;
} // }
/** /**
* Generate SQL for limiting results per parent using window functions * Generate SQL for limiting results per parent using window functions

View File

@@ -1,12 +1,7 @@
import { type SQL, sql } from "drizzle-orm"; import { type SQL, sql } from "drizzle-orm";
import type { PgTable } from "drizzle-orm/pg-core"; import type { PgTable } from "drizzle-orm/pg-core";
import type { CTEConfig } from "../buildCte.js"; import type { CTEConfig } from "../buildCte.js";
import { import { buildRelationGraph, type RelationNode } from "./relationGraph.js";
buildRelationGraph,
getTableName,
parseJoinCondition,
type RelationNode,
} from "./relationGraph.js";
/** /**
* Build the optimized query using JOIN + GROUP BY strategy * Build the optimized query using JOIN + GROUP BY strategy
@@ -26,7 +21,6 @@ export function buildJoinGroupByQuery({
}) => SQL | undefined; }) => SQL | undefined;
}): SQL { }): SQL {
// Build relation graph // Build relation graph
const rootTable = getSourceTable(config.from);
const graph = buildRelationGraph({ const graph = buildRelationGraph({
config, config,
relations, relations,
@@ -34,7 +28,7 @@ export function buildJoinGroupByQuery({
}); });
// Step 1: Build aggregation CTEs for array (one-to-many) relations // Step 1: Build aggregation CTEs for array (one-to-many) relations
const aggregationCTEs = buildAggregationCTEs({ graph, rootTable }); const aggregationCTEs = buildAggregationCTEs({ graph });
// Step 2: Build main query with row (one-to-one) relations as direct JOINs // Step 2: Build main query with row (one-to-one) relations as direct JOINs
const mainQuery = buildMainQuery({ graph, config }); const mainQuery = buildMainQuery({ graph, config });
@@ -181,10 +175,8 @@ function addNestedJoins({
*/ */
function buildAggregationCTEs({ function buildAggregationCTEs({
graph, graph,
rootTable,
}: { }: {
graph: RelationNode; graph: RelationNode;
rootTable: PgTable;
}): Array<{ name: string; definition: SQL }> { }): Array<{ name: string; definition: SQL }> {
const ctes: Array<{ name: string; definition: SQL }> = []; const ctes: Array<{ name: string; definition: SQL }> = [];
@@ -301,13 +293,3 @@ function buildAggregationCTEs({
return ctes; return ctes;
} }
/**
* Get source table from config (unwrap CTEBuilder if needed)
*/
function getSourceTable(from: any): PgTable {
if (from?.config?.from) {
return getSourceTable(from.config.from);
}
return from;
}

View File

@@ -1,4 +1,4 @@
import { type SQL } from "drizzle-orm"; import type { SQL } from "drizzle-orm";
import type { PgTable } from "drizzle-orm/pg-core"; import type { PgTable } from "drizzle-orm/pg-core";
import type { CTEConfig } from "../buildCte.js"; import type { CTEConfig } from "../buildCte.js";
import { CTEBuilder } from "../buildCte.js"; import { CTEBuilder } from "../buildCte.js";
@@ -80,12 +80,10 @@ export function parseJoinCondition({
*/ */
export function buildRelationGraph({ export function buildRelationGraph({
config, config,
parentTable,
relations, relations,
extractJoinCondition, extractJoinCondition,
}: { }: {
config: CTEConfig; config: CTEConfig;
parentTable?: PgTable;
relations: Record<string, any>; relations: Record<string, any>;
extractJoinCondition: (params: { extractJoinCondition: (params: {
parentTable: PgTable; parentTable: PgTable;
@@ -128,7 +126,6 @@ export function buildRelationGraph({
// Recursively build nested nodes // Recursively build nested nodes
const nestedNode = buildRelationGraph({ const nestedNode = buildRelationGraph({
config: nested, config: nested,
parentTable: table,
relations, relations,
extractJoinCondition, extractJoinCondition,
}); });

View File

@@ -36,11 +36,7 @@ export function inferMode(config: ModeDetectionConfig): CTEMode {
// 5. Plural field name? → array (entities, organizations, products) // 5. Plural field name? → array (entities, organizations, products)
// Exclude words ending in 'ss' (address, process, etc.) // Exclude words ending in 'ss' (address, process, etc.)
if ( if (config.fieldName?.endsWith("s") && !config.fieldName.endsWith("ss")) {
config.fieldName &&
config.fieldName.endsWith("s") &&
!config.fieldName.endsWith("ss")
) {
return "array"; return "array";
} }

View File

@@ -1,5 +1,5 @@
import { getTableColumns, sql, SQL } from "drizzle-orm"; import { getTableColumns, type SQL, sql } from "drizzle-orm";
import { PgTable } from "drizzle-orm/pg-core"; import type { PgTable } from "drizzle-orm/pg-core";
export const buildConflictUpdateColumns = <T extends PgTable>( export const buildConflictUpdateColumns = <T extends PgTable>(
table: T, table: T,

View File

@@ -1,4 +1,4 @@
import { ClickHouseClient, createClient } from "@clickhouse/client"; import { type ClickHouseClient, createClient } from "@clickhouse/client";
export const clickhouseClient: ClickHouseClient = createClient({ export const clickhouseClient: ClickHouseClient = createClient({
url: process.env.CLICKHOUSE_URL!, url: process.env.CLICKHOUSE_URL!,

View File

@@ -1,5 +1,5 @@
import { Writable } from "node:stream";
import pino from "pino"; import pino from "pino";
import { Writable } from "stream";
// Custom log formatter for Bun compatibility // Custom log formatter for Bun compatibility
const createDevLogStream = () => { const createDevLogStream = () => {
@@ -53,7 +53,7 @@ const createDevLogStream = () => {
}; };
return new Writable({ return new Writable({
write(chunk, encoding, callback) { write(chunk, _encoding, callback) {
try { try {
const log = JSON.parse(chunk.toString()); const log = JSON.parse(chunk.toString());
const timestamp = new Date(log.time) const timestamp = new Date(log.time)

View File

@@ -128,6 +128,40 @@ export class AutumnInt {
return response.json(); return response.json();
} }
async patch(path: string, body: any) {
const response = await fetch(`${this.baseUrl}${path}`, {
method: "PATCH",
headers: this.headers,
body: JSON.stringify(body),
});
if (response.status !== 200) {
// Handle rate limit errors
if (response.status === 429) {
throw new AutumnError({
message: `request failed, rate limit exceeded`,
code: "rate_limit_exceeded",
});
}
let error: any;
try {
error = await response.json();
} catch (error) {
throw new AutumnError({
message: `request failed, error: ${error}`,
code: ErrCode.InternalError,
});
}
throw new AutumnError({
message: error.message,
code: error.code,
});
}
return response.json();
}
async delete( async delete(
path: string, path: string,
@@ -418,7 +452,7 @@ export class AutumnInt {
// if (product.items && typeof product.items === "object") { // if (product.items && typeof product.items === "object") {
// product.items = Object.values(product.items); // product.items = Object.values(product.items);
// } // }
const data = await this.post(`/products/${productId}`, product); const data = await this.patch(`/products/${productId}`, product);
return data; return data;
}, },

View File

@@ -1,7 +1,7 @@
import { AppEnv, ErrCode, Organization } from "@autumn/shared"; import { AppEnv, ErrCode, type Organization } from "@autumn/shared";
import { Autumn } from "autumn-js";
// import { Autumn } from "./autumnCli.js"; // import { Autumn } from "./autumnCli.js";
import RecaseError from "@/utils/errorUtils.js"; import RecaseError from "@/utils/errorUtils.js";
import { Autumn } from "autumn-js";
export enum FeatureId { export enum FeatureId {
Products = "products", Products = "products",

View File

@@ -5,7 +5,7 @@ import RecaseError from "@/utils/errorUtils.js";
export const autumnWebhookRouter: Router = express.Router(); export const autumnWebhookRouter: Router = express.Router();
const verifyAutumnWebhook = async (req: any, res: any) => { const verifyAutumnWebhook = async (req: any) => {
const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!); const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!);
const headers = req.headers; const headers = req.headers;
@@ -49,7 +49,7 @@ autumnWebhookRouter.post(
express.raw({ type: "application/json" }), express.raw({ type: "application/json" }),
async (req, res) => { async (req, res) => {
try { try {
const evt = await verifyAutumnWebhook(req, res); const evt = await verifyAutumnWebhook(req);
console.log("Received webhook from autumn"); console.log("Received webhook from autumn");
const { type, data } = evt; const { type, data } = evt;

View File

@@ -1,6 +1,6 @@
import fs from "fs"; import fs from "node:fs";
import path from "path"; import path from "node:path";
import { ClickHouseClient, QueryParams } from "@clickhouse/client"; import type { ClickHouseClient, QueryParams } from "@clickhouse/client";
import { clickhouseClient } from "../../db/initClickHouse.js"; import { clickhouseClient } from "../../db/initClickHouse.js";
export enum ClickHouseQuery { export enum ClickHouseQuery {
@@ -115,6 +115,7 @@ export class ClickHouseManager {
} }
} }
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: Might comment this back in in the future
private async ensureQueriesExist() { private async ensureQueriesExist() {
if (!this.client) { if (!this.client) {
throw new Error("ClickHouse client not initialized"); throw new Error("ClickHouse client not initialized");

View File

@@ -1,34 +1,5 @@
import { join } from "node:path";
import { InfisicalSDK } from "@infisical/sdk"; import { InfisicalSDK } from "@infisical/sdk";
import { config } from "dotenv"; import { loadLocalEnv } from "@/utils/envUtils.js";
export const loadLocalEnv = () => {
const processDir = process.cwd();
const serverDir = processDir.includes("server")
? processDir
: join(processDir, "server");
// Determine which env file to load based on ENV_FILE environment variable
// Defaults to .env if not specified
const envFileName = process.env.ENV_FILE || ".env";
const envPath = join(serverDir, envFileName);
// Load local .env file FIRST - these will take precedence over Infisical
const result = config({ path: envPath });
if (result.parsed) {
console.log(
`📄 Loading ${Object.keys(result.parsed).length} variables from ${envFileName}`,
);
for (const [key, value] of Object.entries(result.parsed)) {
process.env[key] = value;
}
} else {
console.log(
` No ${envFileName} file found (using only Infisical secrets)`,
);
}
};
/** /**
* Initialize Infisical and load secrets into process.env * Initialize Infisical and load secrets into process.env
* This allows all existing code using process.env to work seamlessly * This allows all existing code using process.env to work seamlessly

View File

@@ -25,6 +25,7 @@ const redis = new Redis(regionalCacheUrl || process.env.CACHE_URL, {
tls: caText ? { ca: caText } : undefined, tls: caText ? { ca: caText } : undefined,
}); });
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might uncomment this back in in the future
redis.on("error", (error) => { redis.on("error", (error) => {
// logger.error(`redis (cache) error: ${error.message}`); // logger.error(`redis (cache) error: ${error.message}`);
}); });

View File

@@ -19,7 +19,7 @@ export const loadCaCert = async ({
const ca = Bun.file(caPath || `/etc/secrets/${type}-cert.pem`); const ca = Bun.file(caPath || `/etc/secrets/${type}-cert.pem`);
const caText = await ca.text(); const caText = await ca.text();
return undefined; return caText;
} catch (_error) { } catch (_error) {
return; return;
} }

View File

@@ -24,7 +24,7 @@ export const sendTextEmail = async ({
try { try {
logger.info(`Sending email to ${to} with subject ${subject}`); logger.info(`Sending email to ${to} with subject ${subject}`);
const { data, error } = await resend.emails.send({ const { error } = await resend.emails.send({
from: from, from: from,
to: to, to: to,
subject: subject, subject: subject,

View File

@@ -20,7 +20,6 @@ import { billingIntervalToStripe } from "../stripePriceUtils.js";
import { priceToInArrearTiers } from "./createStripeInArrear.js"; import { priceToInArrearTiers } from "./createStripeInArrear.js";
export interface StripeMeteredPriceParams { export interface StripeMeteredPriceParams {
db: DrizzleCli;
stripeCli: Stripe; stripeCli: Stripe;
price: Price; price: Price;
entitlements: EntitlementWithFeature[]; entitlements: EntitlementWithFeature[];
@@ -29,7 +28,6 @@ export interface StripeMeteredPriceParams {
} }
export const createStripeMeteredPrice = async ({ export const createStripeMeteredPrice = async ({
db,
stripeCli, stripeCli,
price, price,
entitlements, entitlements,
@@ -225,7 +223,6 @@ export const createStripeArrearProrated = async ({
// CREATE PLACEHOLDER PRICE FOR INARREAR PRORATED PRICING // CREATE PLACEHOLDER PRICE FOR INARREAR PRORATED PRICING
if (billingType === BillingType.InArrearProrated) { if (billingType === BillingType.InArrearProrated) {
const placeholderPrice = await createStripeMeteredPrice({ const placeholderPrice = await createStripeMeteredPrice({
db,
stripeCli, stripeCli,
price, price,
entitlements, entitlements,

View File

@@ -178,7 +178,6 @@ export const createStripePriceIFNotExist = async ({
} else if (!config.stripe_placeholder_price_id) { } else if (!config.stripe_placeholder_price_id) {
logger.info(`Creating stripe placeholder price`); logger.info(`Creating stripe placeholder price`);
const placeholderPrice = await createStripeMeteredPrice({ const placeholderPrice = await createStripeMeteredPrice({
db,
stripeCli, stripeCli,
price, price,
entitlements, entitlements,

View File

@@ -201,8 +201,6 @@ export const handleStripeWebhookEvent = async ({
case "invoice.updated": case "invoice.updated":
await handleInvoiceUpdated({ await handleInvoiceUpdated({
stripeCli,
env,
event, event,
req: ctx as unknown as ExtendedRequest, req: ctx as unknown as ExtendedRequest,
}); });
@@ -251,7 +249,6 @@ export const handleStripeWebhookEvent = async ({
org, org,
env, env,
schedule: canceledSchedule, schedule: canceledSchedule,
logger,
}); });
break; break;
} }

View File

@@ -1,8 +0,0 @@
import Stripe from "stripe";
const classifyStripePaymentMethod = (paymentMethod: Stripe.PaymentMethod) => {
let cardPaymentMethods = [];
};
// Note: us_bank_account -> ACH
// customer_balance -> Bank Account

View File

@@ -1,5 +1,4 @@
import { UsagePriceConfig } from "@autumn/shared"; import type { Price, UsagePriceConfig } from "@autumn/shared";
import { Price } from "@autumn/shared";
export const priceToInArrearProrated = ({ export const priceToInArrearProrated = ({
price, price,
@@ -11,9 +10,9 @@ export const priceToInArrearProrated = ({
existingUsage: number; existingUsage: number;
}) => { }) => {
const config = price.config as UsagePriceConfig; const config = price.config as UsagePriceConfig;
let quantity = existingUsage || 0; const quantity = existingUsage || 0;
if (quantity == 0 && isCheckout) { if (quantity === 0 && isCheckout) {
return { return {
price: config.stripe_placeholder_price_id, price: config.stripe_placeholder_price_id,
}; };

View File

@@ -124,7 +124,6 @@ export const priceToStripeItem = ({
price, price,
options, options,
isCheckout, isCheckout,
relatedEnt,
}); });
} }

View File

@@ -24,13 +24,8 @@ export const priceToOneOffAndTiered = ({
stripeProductId: string; stripeProductId: string;
}) => { }) => {
const config = price.config as UsagePriceConfig; const config = price.config as UsagePriceConfig;
const quantity = options?.quantity!; const quantity = options?.quantity ?? 0;
const overage = new Decimal(quantity).mul(config.billing_units!).toNumber(); const overage = new Decimal(quantity).mul(config.billing_units!).toNumber();
// let overage = quantity * config.billing_units! - relatedEnt.allowance!;
// if (overage <= 0) {
// return null;
// }
const amount = getPriceForOverage(price, overage); const amount = getPriceForOverage(price, overage);
if (!config.stripe_product_id) { if (!config.stripe_product_id) {
@@ -53,12 +48,10 @@ export const priceToOneOffAndTiered = ({
export const priceToUsageInAdvance = ({ export const priceToUsageInAdvance = ({
price, price,
relatedEnt,
options, options,
isCheckout, isCheckout,
}: { }: {
price: Price; price: Price;
relatedEnt: EntitlementWithFeature;
options: FeatureOptions | undefined | null; options: FeatureOptions | undefined | null;
isCheckout: boolean; isCheckout: boolean;
}) => { }) => {
@@ -69,9 +62,7 @@ export const priceToUsageInAdvance = ({
// 1. If adjustable quantity is set, use that, else if quantity is undefined, adjustable is true, else false // 1. If adjustable quantity is set, use that, else if quantity is undefined, adjustable is true, else false
const adjustable = notNullish(options?.adjustable_quantity) const adjustable = notNullish(options?.adjustable_quantity)
? options!.adjustable_quantity ? options!.adjustable_quantity
: nullish(optionsQuantity) : nullish(optionsQuantity);
? true
: false;
if (optionsQuantity === 0 && isCheckout) { if (optionsQuantity === 0 && isCheckout) {
// 1. If quantity is 0 and is checkout, skip over line item // 1. If quantity is 0 and is checkout, skip over line item
@@ -81,12 +72,6 @@ export const priceToUsageInAdvance = ({
finalQuantity = 1; finalQuantity = 1;
} }
// Divide final quantity by billing units...?
// let minimum = new Decimal(relatedEnt.allowance!)
// .div(config.billing_units || 1)
// .toNumber();
const adjustableQuantity = const adjustableQuantity =
isCheckout && adjustable isCheckout && adjustable
? { ? {

View File

@@ -1,4 +1,4 @@
import { Stripe } from "stripe"; import type { Stripe } from "stripe";
export const deleteCouponFromSub = async ({ export const deleteCouponFromSub = async ({
stripeCli, stripeCli,
@@ -12,23 +12,14 @@ export const deleteCouponFromSub = async ({
logger: any; logger: any;
}) => { }) => {
try { try {
let stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId); const stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
let newDiscounts = stripeSub.discounts
?.filter((d: any) => d !== discountId)
.map((d: any) => ({
discount: d,
}));
if (stripeSub.discounts.some((d: any) => d === discountId)) { if (stripeSub.discounts.some((d: any) => d === discountId)) {
await stripeCli.subscriptions.deleteDiscount(stripeSubId); await stripeCli.subscriptions.deleteDiscount(stripeSubId);
// console.log("DELETED DISCOUNT FROM SUB", stripeSubId);
} }
} catch (error: any) { } catch (error: any) {
// if (!error.message.includes("no active discount for subscription")) {
logger.error(`Failed to delete discount from subscription ${stripeSubId}`); logger.error(`Failed to delete discount from subscription ${stripeSubId}`);
logger.error(error.message); logger.error(error.message);
// }
} }
}; };
@@ -46,7 +37,7 @@ export const deleteCouponFromCus = async ({
logger: any; logger: any;
}) => { }) => {
try { try {
let stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId); const stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
if (stripeSub.discounts.some((d: any) => d === discountId)) { if (stripeSub.discounts.some((d: any) => d === discountId)) {
await stripeCli.subscriptions.deleteDiscount(stripeSubId); await stripeCli.subscriptions.deleteDiscount(stripeSubId);
} }
@@ -56,7 +47,7 @@ export const deleteCouponFromCus = async ({
} }
try { try {
let stripeCus = (await stripeCli.customers.retrieve( const stripeCus = (await stripeCli.customers.retrieve(
stripeCusId, stripeCusId,
)) as Stripe.Customer; )) as Stripe.Customer;
if (stripeCus.discount?.id === discountId) { if (stripeCus.discount?.id === discountId) {

View File

@@ -22,7 +22,7 @@ export const getStripeCus = async ({
try { try {
const stripeCus = await stripeCli.customers.retrieve(stripeId); const stripeCus = await stripeCli.customers.retrieve(stripeId);
return stripeCus as Stripe.Customer; return stripeCus as Stripe.Customer;
} catch (error) { } catch (_error) {
return undefined; return undefined;
} }
}; };
@@ -57,7 +57,7 @@ export const createStripeCusIfNotExists = async ({
} else { } else {
createNew = true; createNew = true;
} }
} catch (error) { } catch (_error) {
createNew = true; createNew = true;
} }
} }

View File

@@ -1,4 +0,0 @@
export const stripeErrToCusMsg = (error: any) => {
let code = error.code;
let msg = error.message;
};

View File

@@ -1,96 +0,0 @@
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
Customer,
FreeTrial,
Organization,
Price,
ErrCode,
BillingInterval,
} from "@autumn/shared";
import Stripe from "stripe";
import { getCusPaymentMethod } from "./stripeCusUtils.js";
export const createStripeSubThroughInvoice = async ({
stripeCli,
customer,
org,
items,
freeTrial,
metadata = {},
prices,
}: {
stripeCli: Stripe;
customer: Customer;
items: any;
freeTrial: FreeTrial | null;
org: Organization;
metadata?: any;
prices: Price[];
}) => {
// 1. Get payment method
let paymentMethod;
try {
paymentMethod = await getCusPaymentMethod({
stripeCli,
stripeId: customer.processor.id,
});
} catch (error) {}
let paymentMethodData = {};
if (paymentMethod) {
paymentMethodData = {
default_payment_method: paymentMethod.id,
};
}
let subItems = items.filter(
(i: any, index: number) =>
prices[index].config!.interval !== BillingInterval.OneOff,
);
let invoiceItems = items.filter(
(i: any, index: number) =>
prices[index].config!.interval === BillingInterval.OneOff,
);
try {
const subscription = await stripeCli.subscriptions.create({
// ...paymentMethodData,
customer: customer.processor.id,
items: subItems as any,
trial_end: freeTrialToStripeTimestamp({ freeTrial }),
metadata,
add_invoice_items: invoiceItems,
collection_method: "send_invoice",
days_until_due: 30,
});
return subscription;
} catch (error: any) {
// console.log("Error creating stripe subscription", error?.message || error);
console.log("Warning: Failed to create stripe subscription");
console.log("Error code:", error.code);
console.log("Message:", error.message);
console.log("Decline code:", error.decline_code);
throw new RecaseError({
// code: ErrCode.StripeCardDeclined,
code: ErrCode.CreateStripeSubscriptionFailed,
message: `Stripe subscription failed (${error.code}): ${error.message}`,
statusCode: 500,
});
// if (isStripeCardDeclined(error)) {
// }
// console.log("Error creating stripe subscription", error?.message || error);
// console.log("Error code:", error.code);
// throw new RecaseError({
// code: ErrCode.CreateStripeSubscriptionFailed,
// message: "Failed to create stripe subscription",
// statusCode: 500,
// });
}
};

View File

@@ -1,8 +1,12 @@
import { Customer, Feature } from "@autumn/shared"; import {
BillingType,
type Customer,
type Feature,
type Price,
type UsagePriceConfig,
} from "@autumn/shared";
import type Stripe from "stripe";
import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { BillingType, Price, UsagePriceConfig } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
export const submitUsageToStripe = async ({ export const submitUsageToStripe = async ({
price, price,
@@ -21,10 +25,10 @@ export const submitUsageToStripe = async ({
feature: Feature; feature: Feature;
logger: any; logger: any;
}) => { }) => {
let config = price.config as UsagePriceConfig; const config = price.config as UsagePriceConfig;
let billingType = getBillingType(config); const billingType = getBillingType(config);
if (billingType != BillingType.UsageInArrear) { if (billingType !== BillingType.UsageInArrear) {
logger.warn( logger.warn(
`Price ${price.id} is not usage in arrear type, can't send usage`, `Price ${price.id} is not usage in arrear type, can't send usage`,
); );

View File

@@ -6,11 +6,7 @@ export const checkKeyValid = async (apiKey: string) => {
const stripe = new Stripe(apiKey); const stripe = new Stripe(apiKey);
// Call customers.list // Call customers.list
const customers = await stripe.customers.list(); await stripe.customers.list();
// const account = await stripe.accounts.retrieve();
// console.log("Account", account);
// return account;
}; };
export const createWebhookEndpoint = async ( export const createWebhookEndpoint = async (

View File

@@ -52,7 +52,7 @@ export const deleteStripeProduct = async (
try { try {
await stripe.products.del(product.processor.id); await stripe.products.del(product.processor.id);
} catch (error) { } catch (_error) {
throw new RecaseError({ throw new RecaseError({
message: "Failed to delete stripe product", message: "Failed to delete stripe product",
code: ErrCode.DeleteStripeProductFailed, code: ErrCode.DeleteStripeProductFailed,
@@ -136,7 +136,7 @@ export const deleteAllStripeProducts = async ({
batch.map(async (p) => { batch.map(async (p) => {
try { try {
await stripeCli.products.del(p.id); await stripeCli.products.del(p.id);
} catch (error) { } catch (_error) {
await stripeCli.products.update(p.id, { await stripeCli.products.update(p.id, {
active: false, active: false,
}); });

View File

@@ -232,7 +232,6 @@ export const getStripeSubItems2 = async ({
cusProducts, cusProducts,
customer, customer,
internalEntityId, internalEntityId,
apiVersion,
products, products,
} = attachParams; } = attachParams;
@@ -320,7 +319,7 @@ export const getStripeSubItems2 = async ({
export const sanitizeSubItems = (subItems: any[]) => { export const sanitizeSubItems = (subItems: any[]) => {
return subItems.map((si) => { return subItems.map((si) => {
const { autumnPrice, ...rest } = si; const { autumnPrice: _autumnPrice, ...rest } = si;
return { return {
...rest, ...rest,
}; };

View File

@@ -1,13 +1,12 @@
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { import {
BillingInterval, type BillingInterval,
BillingType, BillingType,
intervalsDifferent, intervalsDifferent,
Organization, type Organization,
UsagePriceConfig, type Price,
type UsagePriceConfig,
} from "@autumn/shared"; } from "@autumn/shared";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { Price } from "@autumn/shared";
import { billingIntervalToStripe } from "../../stripePriceUtils.js"; import { billingIntervalToStripe } from "../../stripePriceUtils.js";
export const getArrearItems = ({ export const getArrearItems = ({
@@ -21,9 +20,9 @@ export const getArrearItems = ({
intervalCount: number; intervalCount: number;
org: Organization; org: Organization;
}) => { }) => {
let placeholderItems: any[] = []; const placeholderItems: any[] = [];
for (const price of prices) { for (const price of prices) {
let billingType = getBillingType(price.config!); const billingType = getBillingType(price.config!);
if ( if (
intervalsDifferent({ intervalsDifferent({
intervalA: { intervalA: {
@@ -36,8 +35,8 @@ export const getArrearItems = ({
continue; continue;
} }
if (billingType == BillingType.UsageInArrear) { if (billingType === BillingType.UsageInArrear) {
let config = price.config! as UsagePriceConfig; const config = price.config! as UsagePriceConfig;
placeholderItems.push({ placeholderItems.push({
price_data: { price_data: {
product: config.stripe_product_id!, product: config.stripe_product_id!,

View File

@@ -1,7 +1,6 @@
import RecaseError from "@/utils/errorUtils.js";
import Stripe from "stripe";
import { Decimal } from "decimal.js"; import { Decimal } from "decimal.js";
import { notNullish, nullish } from "@/utils/genUtils.js"; import type Stripe from "stripe";
import { notNullish } from "@/utils/genUtils.js";
const calculateTieredAmount = ({ const calculateTieredAmount = ({
tiers, tiers,
@@ -40,12 +39,12 @@ export const getSubItemAmount = ({
}: { }: {
subItem: Stripe.SubscriptionItem; subItem: Stripe.SubscriptionItem;
}) => { }) => {
let price = subItem.price; const price = subItem.price;
const quantity = subItem.quantity || 0; const quantity = subItem.quantity || 0;
if (price.billing_scheme == "tiered") { if (price.billing_scheme === "tiered") {
let tieredAmount = calculateTieredAmount({ const tieredAmount = calculateTieredAmount({
tiers: price.tiers!, tiers: price.tiers!,
quantity, quantity,
}); });
@@ -53,7 +52,7 @@ export const getSubItemAmount = ({
return tieredAmount; return tieredAmount;
} }
if (price.billing_scheme == "per_unit") { if (price.billing_scheme === "per_unit") {
if (price.unit_amount_decimal) { if (price.unit_amount_decimal) {
return new Decimal(price.unit_amount_decimal).mul(quantity).toNumber(); return new Decimal(price.unit_amount_decimal).mul(quantity).toNumber();
} else { } else {

View File

@@ -1,37 +0,0 @@
import { Price, UsagePriceConfig } from "@autumn/shared";
import Stripe from "stripe";
// export const findPriceInScheduleItems = ({
// prices,
// subItem,
// billingType,
// }: {
// prices: Price[];
// subItem: Stripe.SubscriptionItem | Stripe.InvoiceLineItem;
// billingType?: BillingType;
// }) => {
// return prices.find((p: Price) => {
// let config = p.config;
// let itemMatch =
// config.stripe_price_id == subItem.price?.id ||
// config.stripe_product_id == subItem.price?.product;
// const priceBillingType = getBillingType(config);
// let billingTypeMatch = billingType ? priceBillingType == billingType : true;
// return itemMatch && billingTypeMatch;
// });
// };
// export const findScheduleItemForPrice = ({
// price,
// items,
// }: {
// price: Price;
// items: Stripe.SubscriptionSchedule.Phase.Item[];
// }) => {
// return items.find((si) => {
// const config = price.config as UsagePriceConfig;
// return config.stripe_price_id == si.price?.id;
// });
// };

View File

@@ -69,7 +69,6 @@ export const createProrationInvoice = async ({
}) => { }) => {
const { stripeCli, customer, paymentMethod } = attachParams; const { stripeCli, customer, paymentMethod } = attachParams;
const proratedItems = [];
// How to retrieve upcoming invoice items? // How to retrieve upcoming invoice items?
const items = await stripeCli.invoiceItems.list({ const items = await stripeCli.invoiceItems.list({
customer: customer.processor.id, customer: customer.processor.id,

View File

@@ -20,7 +20,6 @@ stripeWebhookRouter.post(
let event: Stripe.Event; let event: Stripe.Event;
const { orgId, env } = request.params; const { orgId, env } = request.params;
const { db } = request;
let org: Organization; let org: Organization;

View File

@@ -78,7 +78,6 @@ export const handleCheckoutSessionCompleted = async ({
if (attachParams.setupPayment) { if (attachParams.setupPayment) {
await handleSetupCheckout({ await handleSetupCheckout({
req, req,
db,
attachParams, attachParams,
}); });
return; return;
@@ -107,18 +106,15 @@ export const handleCheckoutSessionCompleted = async ({
db, db,
subscription: checkoutSub, subscription: checkoutSub,
attachParams, attachParams,
logger,
}); });
// Create other subscriptions // Create other subscriptions
const { invoiceIds } = await handleRemainingSets({ const { invoiceIds } = await handleRemainingSets({
stripeCli, stripeCli,
db,
org, org,
checkoutSession, checkoutSession,
attachParams, attachParams,
checkoutSub, checkoutSub,
logger,
}); });
const anchorToUnix = checkoutSub const anchorToUnix = checkoutSub
@@ -147,7 +143,7 @@ export const handleCheckoutSessionCompleted = async ({
product, product,
productOptions.entity_id || undefined, productOptions.entity_id || undefined,
), ),
subscriptionIds: checkoutSub ? [checkoutSub?.id!] : undefined, subscriptionIds: checkoutSub ? [checkoutSub.id] : undefined,
anchorToUnix, anchorToUnix,
scenario: AttachScenario.New, scenario: AttachScenario.New,
logger, logger,
@@ -160,7 +156,7 @@ export const handleCheckoutSessionCompleted = async ({
await createFullCusProduct({ await createFullCusProduct({
db, db,
attachParams: attachToInsertParams(attachParams, product), attachParams: attachToInsertParams(attachParams, product),
subscriptionIds: checkoutSub ? [checkoutSub?.id!] : undefined, subscriptionIds: checkoutSub ? [checkoutSub.id] : undefined,
anchorToUnix, anchorToUnix,
scenario: AttachScenario.New, scenario: AttachScenario.New,
logger, logger,

View File

@@ -1,12 +1,11 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { BillingType, type UsagePriceConfig } from "@autumn/shared";
import type Stripe from "stripe";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { import {
formatPrice,
getBillingType, getBillingType,
getPriceEntitlement, getPriceEntitlement,
priceIsOneOffAndTiered, priceIsOneOffAndTiered,
} from "@/internal/products/prices/priceUtils.js"; } from "@/internal/products/prices/priceUtils.js";
import { BillingType, UsagePriceConfig } from "@autumn/shared";
import Stripe from "stripe";
import { findStripeItemForPrice } from "../../stripeSubUtils/stripeSubItemUtils.js"; import { findStripeItemForPrice } from "../../stripeSubUtils/stripeSubItemUtils.js";
export const getOptionsFromCheckoutSession = async ({ export const getOptionsFromCheckoutSession = async ({
@@ -18,7 +17,7 @@ export const getOptionsFromCheckoutSession = async ({
}) => { }) => {
const usageInAdvanceExists = attachParams.prices.some( const usageInAdvanceExists = attachParams.prices.some(
(price) => (price) =>
getBillingType(price.config as UsagePriceConfig) == getBillingType(price.config as UsagePriceConfig) ===
BillingType.UsageInAdvance, BillingType.UsageInAdvance,
); );
@@ -31,9 +30,9 @@ export const getOptionsFromCheckoutSession = async ({
// Should still work with old method? // Should still work with old method?
for (const price of prices) { for (const price of prices) {
let config = price.config as UsagePriceConfig; const config = price.config as UsagePriceConfig;
if (getBillingType(config) != BillingType.UsageInAdvance) continue; if (getBillingType(config) !== BillingType.UsageInAdvance) continue;
const lineItem = findStripeItemForPrice({ const lineItem = findStripeItemForPrice({
price, price,
@@ -43,7 +42,7 @@ export const getOptionsFromCheckoutSession = async ({
let quantity = 0; let quantity = 0;
if (lineItem) { if (lineItem) {
let relatedEnt = getPriceEntitlement(price, ents); const relatedEnt = getPriceEntitlement(price, ents);
if (priceIsOneOffAndTiered(price, relatedEnt)) { if (priceIsOneOffAndTiered(price, relatedEnt)) {
// quantity = lineItem.quantity || 0; // quantity = lineItem.quantity || 0;
@@ -54,10 +53,10 @@ export const getOptionsFromCheckoutSession = async ({
} }
const index = optionsList.findIndex( const index = optionsList.findIndex(
(feature) => feature.internal_feature_id == config.internal_feature_id, (feature) => feature.internal_feature_id === config.internal_feature_id,
); );
if (index == -1) { if (index === -1) {
attachParams.optionsList.push({ attachParams.optionsList.push({
feature_id: config.feature_id, feature_id: config.feature_id,
internal_feature_id: config.internal_feature_id, internal_feature_id: config.internal_feature_id,

View File

@@ -16,13 +16,11 @@ export const handleCheckoutSub = async ({
db, db,
subscription, subscription,
attachParams, attachParams,
logger,
}: { }: {
stripeCli: Stripe; stripeCli: Stripe;
db: DrizzleCli; db: DrizzleCli;
subscription: Stripe.Subscription | null; subscription: Stripe.Subscription | null;
attachParams: AttachParams; attachParams: AttachParams;
logger: any;
}) => { }) => {
const { org } = attachParams; const { org } = attachParams;

View File

@@ -1,25 +1,20 @@
import { ApiVersion, isUsagePrice, type Organization } from "@autumn/shared"; import { ApiVersion, isUsagePrice, type Organization } from "@autumn/shared";
import type Stripe from "stripe"; import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js"; import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js";
export const handleRemainingSets = async ({ export const handleRemainingSets = async ({
stripeCli, stripeCli,
db,
org, org,
checkoutSession, checkoutSession,
attachParams, attachParams,
checkoutSub, checkoutSub,
logger,
}: { }: {
stripeCli: Stripe; stripeCli: Stripe;
db: DrizzleCli;
org: Organization; org: Organization;
checkoutSession: Stripe.Checkout.Session; checkoutSession: Stripe.Checkout.Session;
attachParams: AttachParams; attachParams: AttachParams;
checkoutSub: Stripe.Subscription | null; checkoutSub: Stripe.Subscription | null;
logger: any;
}) => { }) => {
const itemSets = attachParams.itemSets; const itemSets = attachParams.itemSets;
const remainingSets = itemSets ? itemSets.slice(1) : []; const remainingSets = itemSets ? itemSets.slice(1) : [];

View File

@@ -1,5 +1,4 @@
import { AttachBranch } from "@autumn/shared"; import { AttachBranch } from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js";
import { handleOneOffFunction } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.js"; import { handleOneOffFunction } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
@@ -11,11 +10,9 @@ import { getCusPaymentMethod } from "../../stripeCusUtils.js";
export const handleSetupCheckout = async ({ export const handleSetupCheckout = async ({
req, req,
db,
attachParams, attachParams,
}: { }: {
req: ExtendedRequest; req: ExtendedRequest;
db: DrizzleCli;
attachParams: AttachParams; attachParams: AttachParams;
}) => { }) => {
const logger = req.logger; const logger = req.logger;

View File

@@ -15,7 +15,6 @@ export const handleContUsePrices = async ({
db, db,
cusEnts, cusEnts,
cusPrice, cusPrice,
stripeCli,
invoice, invoice,
usageSub, usageSub,
logger, logger,
@@ -24,8 +23,6 @@ export const handleContUsePrices = async ({
db: DrizzleCli; db: DrizzleCli;
cusEnts: FullCustomerEntitlement[]; cusEnts: FullCustomerEntitlement[];
cusPrice: FullCustomerPrice; cusPrice: FullCustomerPrice;
stripeCli: Stripe;
invoice: Stripe.Invoice; invoice: Stripe.Invoice;
usageSub: Stripe.Subscription; usageSub: Stripe.Subscription;
logger: any; logger: any;

View File

@@ -2,24 +2,18 @@ import {
type AppEnv, type AppEnv,
BillingType, BillingType,
CusProductStatus, CusProductStatus,
type Customer,
type FullCusProduct, type FullCusProduct,
type FullCustomerEntitlement,
type FullCustomerPrice,
type Organization, type Organization,
} from "@autumn/shared"; } from "@autumn/shared";
import type Stripe from "stripe"; import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js"; import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { EntityService } from "@/internal/api/entities/EntityService.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js"; import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { FeatureService } from "@/internal/features/FeatureService.js"; import { FeatureService } from "@/internal/features/FeatureService.js";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { deleteCachedApiCustomer } from "../../../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js"; import { deleteCachedApiCustomer } from "../../../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
import { import {
getFullStripeInvoice, getFullStripeInvoice,
@@ -31,139 +25,6 @@ import { handleContUsePrices } from "./handleContUsePrices.js";
import { handlePrepaidPrices } from "./handlePrepaidPrices.js"; import { handlePrepaidPrices } from "./handlePrepaidPrices.js";
import { handleUsagePrices } from "./handleUsagePrices.js"; import { handleUsagePrices } from "./handleUsagePrices.js";
const handleInArrearProrated = async ({
db,
cusEnts,
cusPrice,
customer,
org,
env,
invoice,
usageSub,
logger,
}: {
db: DrizzleCli;
cusEnts: FullCustomerEntitlement[];
cusPrice: FullCustomerPrice;
customer: Customer;
org: Organization;
env: AppEnv;
invoice: Stripe.Invoice;
usageSub: Stripe.Subscription;
logger: any;
}) => {
const cusEnt = getRelatedCusEnt({
cusPrice,
cusEnts,
});
if (!cusEnt) {
console.log("No related cus ent found");
return;
}
// console.log("Invoice period start:\t", formatUnixToDateTime(invoice.period_start * 1000));
// console.log("Invoice period end:\t", formatUnixToDateTime(invoice.period_end * 1000));
// console.log("Sub period start:\t", formatUnixToDateTime(usageSub.current_period_start * 1000));
// console.log("Sub period end:\t", formatUnixToDateTime(usageSub.current_period_end * 1000));
// Check if invoice is for new subscription period by comparing billing period
const { start: periodStart, end: periodEnd } = subToPeriodStartEnd({
sub: usageSub,
});
const isNewPeriod = invoice.period_start !== periodStart;
if (!isNewPeriod) {
logger.info("Invoice is not for new subscription period, skipping...");
return;
}
const feature = cusEnt.entitlement.feature;
logger.info(
`Handling invoice.created for in arrear prorated, feature: ${feature.id}`,
);
const deletedEntities = await EntityService.list({
db,
internalCustomerId: customer.internal_id!,
inFeatureIds: [feature.internal_id!],
isDeleted: true,
});
if (deletedEntities.length === 0) {
logger.info("No deleted entities found");
return;
}
logger.info(
`✨ Handling in arrear prorated, customer ${customer.name}, org: ${org.slug}`,
);
logger.info(
`Deleting entities, feature ${feature.id}, customer ${customer.id}, org ${org.slug}`,
deletedEntities,
);
// Get linked cus ents
for (const linkedCusEnt of cusEnts) {
// isLinked
const isLinked = linkedCusEnt.entitlement.entity_feature_id === feature.id;
if (!isLinked) {
continue;
}
logger.info(
`Linked cus ent: ${linkedCusEnt.feature_id}, isLinked: ${isLinked}`,
);
// Delete cus ent ids
const newEntities = structuredClone(linkedCusEnt.entities!);
for (const entityId in newEntities) {
if (deletedEntities.some((e) => e.id === entityId)) {
delete newEntities[entityId];
}
}
const updated = await CusEntService.update({
db,
id: linkedCusEnt.id,
updates: {
entities: newEntities,
},
});
console.log(`Updated ${updated.length} cus ents`);
logger.info(
`Feature: ${feature.id}, customer: ${customer.id}, deleted entities from cus ent`,
);
linkedCusEnt.entities = newEntities;
}
await EntityService.deleteInInternalIds({
db,
internalIds: deletedEntities.map((e) => e.internal_id!),
orgId: org.id,
env,
});
logger.info(
`Feature: ${feature.id}, Deleted ${
deletedEntities.length
}, entities: ${deletedEntities.map((e) => `${e.id}`).join(", ")}`,
);
// Increase balance
if (notNullish(cusEnt.balance)) {
logger.info(`Incrementing balance for cus ent: ${cusEnt.id}`);
await CusEntService.increment({
db,
id: cusEnt.id,
amount: deletedEntities.length,
});
}
};
// For cancel at period end: invoice period start = sub period start (cur cycle), invoice period end = sub period end (a month later...) // For cancel at period end: invoice period start = sub period start (cur cycle), invoice period end = sub period end (a month later...)
// For cancel immediately: invoice period start = sub period start (cur cycle), invoice period end cancel immediately date // For cancel immediately: invoice period start = sub period start (cur cycle), invoice period end cancel immediately date
// For regular billing: invoice period end = sub period start (next cycle) // For regular billing: invoice period end = sub period start (next cycle)
@@ -175,7 +36,6 @@ export const sendUsageAndReset = async ({
org, org,
env, env,
invoice, invoice,
stripeSubs,
logger, logger,
submitUsage = true, submitUsage = true,
resetBalance = true, resetBalance = true,
@@ -185,7 +45,6 @@ export const sendUsageAndReset = async ({
org: Organization; org: Organization;
env: AppEnv; env: AppEnv;
invoice: Stripe.Invoice; invoice: Stripe.Invoice;
stripeSubs: Stripe.Subscription[];
logger: any; logger: any;
submitUsage?: boolean; submitUsage?: boolean;
resetBalance?: boolean; resetBalance?: boolean;
@@ -249,7 +108,6 @@ export const sendUsageAndReset = async ({
if (billingType === BillingType.InArrearProrated) { if (billingType === BillingType.InArrearProrated) {
const handledContUse = await handleContUsePrices({ const handledContUse = await handleContUsePrices({
db, db,
stripeCli,
cusEnts, cusEnts,
cusPrice, cusPrice,
invoice, invoice,
@@ -264,11 +122,9 @@ export const sendUsageAndReset = async ({
if (billingType === BillingType.UsageInAdvance) { if (billingType === BillingType.UsageInAdvance) {
const handledPrepaid = await handlePrepaidPrices({ const handledPrepaid = await handlePrepaidPrices({
db, db,
stripeCli,
cusPrice, cusPrice,
cusProduct: activeProduct, cusProduct: activeProduct,
usageSub: usageBasedSub, usageSub: usageBasedSub,
customer,
invoice, invoice,
logger, logger,
resetBalance, resetBalance,
@@ -333,7 +189,7 @@ export const handleInvoiceCreated = async ({
(p) => p.internal_entity_id, (p) => p.internal_entity_id,
)?.internal_entity_id; )?.internal_entity_id;
const features = await FeatureService.list({ await FeatureService.list({
db, db,
orgId: org.id, orgId: org.id,
env, env,
@@ -389,7 +245,6 @@ export const handleInvoiceCreated = async ({
activeProduct, activeProduct,
org, org,
env, env,
stripeSubs,
invoice, invoice,
logger, logger,
submitUsage: true, // Always submit usage during invoice.created submitUsage: true, // Always submit usage during invoice.created

View File

@@ -1,5 +1,4 @@
import { import {
type Customer,
EntInterval, EntInterval,
type FeatureOptions, type FeatureOptions,
type FullCusProduct, type FullCusProduct,
@@ -20,21 +19,17 @@ import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
export const handlePrepaidPrices = async ({ export const handlePrepaidPrices = async ({
db, db,
stripeCli,
cusProduct, cusProduct,
cusPrice, cusPrice,
usageSub, usageSub,
customer,
invoice, invoice,
logger, logger,
resetBalance = true, resetBalance = true,
}: { }: {
db: DrizzleCli; db: DrizzleCli;
stripeCli: Stripe;
cusProduct: FullCusProduct; cusProduct: FullCusProduct;
cusPrice: FullCustomerPrice; cusPrice: FullCustomerPrice;
usageSub: Stripe.Subscription; usageSub: Stripe.Subscription;
customer: Customer;
invoice: Stripe.Invoice; invoice: Stripe.Invoice;
logger: any; logger: any;
resetBalance?: boolean; resetBalance?: boolean;
@@ -60,7 +55,7 @@ export const handlePrepaidPrices = async ({
const options = getEntOptions(cusProduct.options, cusEnt.entitlement); const options = getEntOptions(cusProduct.options, cusEnt.entitlement);
const resetQuantity = options?.upcoming_quantity || options?.quantity!; const resetQuantity = (options?.upcoming_quantity || options?.quantity) ?? 0;
const config = cusPrice.price.config as UsagePriceConfig; const config = cusPrice.price.config as UsagePriceConfig;
const billingUnits = config.billing_units || 1; const billingUnits = config.billing_units || 1;
const newAllowance = const newAllowance =
@@ -106,7 +101,8 @@ export const handlePrepaidPrices = async ({
}); });
if (ent.interval === EntInterval.Lifetime) { if (ent.interval === EntInterval.Lifetime) {
const difference = options?.quantity! - options?.upcoming_quantity!; const difference =
(options?.quantity ?? 0) - (options?.upcoming_quantity ?? 0);
await CusEntService.decrement({ await CusEntService.decrement({
db, db,
id: cusEnt.id, id: cusEnt.id,

View File

@@ -28,12 +28,10 @@ import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js";
const handleOneOffInvoicePaid = async ({ const handleOneOffInvoicePaid = async ({
db, db,
stripeInvoice, stripeInvoice,
logger,
}: { }: {
db: DrizzleCli; db: DrizzleCli;
stripeInvoice: Stripe.Invoice; stripeInvoice: Stripe.Invoice;
event: Stripe.Event; event: Stripe.Event;
logger: any;
}) => { }) => {
// Search for invoice // Search for invoice
const invoice = await InvoiceService.getByStripeId({ const invoice = await InvoiceService.getByStripeId({
@@ -285,7 +283,6 @@ export const handleInvoicePaid = async ({
db, db,
stripeInvoice: invoice, stripeInvoice: invoice,
event, event,
logger,
}); });
} }
}; };

View File

@@ -34,29 +34,28 @@ export const handleInvoicePaidDiscount = async ({
logger: any; logger: any;
}) => { }) => {
// Handle coupon // Handle coupon
const stripeCli = createStripeCli({ org, env }); const stripeCli = createStripeCli({ org, env, legacyVersion: true });
if (expandedInvoice.discounts.length === 0) { if (expandedInvoice.discounts.length === 0) return;
return;
}
const stripeCus = await stripeCli.customers.retrieve( const stripeCus = await stripeCli.customers.retrieve(
expandedInvoice.customer as string, expandedInvoice.customer as string,
); );
const legacyInvoice = await stripeCli.invoices.retrieve(expandedInvoice.id, {
expand: ["total_discount_amounts", "discounts.coupon"],
});
try { try {
const totalDiscountAmounts = expandedInvoice.total_discount_amounts; const totalDiscountAmounts = expandedInvoice.total_discount_amounts;
// Log coupon information for debugging // Log coupon information for debugging
for (const discount of expandedInvoice.discounts) { for (const discount of legacyInvoice.discounts) {
if (typeof discount === "string") { if (typeof discount === "string" || !("coupon" in discount)) continue;
continue;
}
const curCoupon = discount.source.coupon; const curCoupon = discount.coupon as Stripe.Coupon;
if (!curCoupon || typeof curCoupon === "string") { if (!curCoupon || typeof curCoupon === "string" || !curCoupon.amount_off)
continue; continue;
}
const rollSuffixIndex = curCoupon.id.indexOf("_roll_"); const rollSuffixIndex = curCoupon.id.indexOf("_roll_");
const couponId = const couponId =
@@ -76,9 +75,7 @@ export const handleInvoicePaidDiscount = async ({
(autumnReward.type === RewardType.InvoiceCredits || (autumnReward.type === RewardType.InvoiceCredits ||
autumnReward.type === RewardType.FreeProduct); autumnReward.type === RewardType.FreeProduct);
if (!shouldRollover) { if (!shouldRollover) continue;
continue;
}
// Get ID of coupon // Get ID of coupon
const originalCoupon = await stripeCli.coupons.retrieve(couponId, { const originalCoupon = await stripeCli.coupons.retrieve(couponId, {
@@ -150,13 +147,7 @@ export const handleInvoicePaidDiscount = async ({
}, },
}); });
const legacyStripeCli = createStripeCli({ await stripeCli.rawRequest(
org,
env,
legacyVersion: true,
});
await legacyStripeCli.rawRequest(
"POST", "POST",
`/v1/customers/${expandedInvoice.customer}`, `/v1/customers/${expandedInvoice.customer}`,
{ {

View File

@@ -1,5 +1,4 @@
import { import {
type AppEnv,
type Invoice, type Invoice,
InvoiceStatus, InvoiceStatus,
stripeToAtmnAmount, stripeToAtmnAmount,
@@ -13,6 +12,7 @@ import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { MetadataService } from "@/internal/metadata/MetadataService.js"; import { MetadataService } from "@/internal/metadata/MetadataService.js";
import { getFullStripeInvoice, invoiceToSubId } from "../stripeInvoiceUtils.js"; import { getFullStripeInvoice, invoiceToSubId } from "../stripeInvoiceUtils.js";
// biome-ignore lint/correctness/noUnusedVariables: Might be useful in the future
const handleInvoiceCheckoutVoided = async ({ const handleInvoiceCheckoutVoided = async ({
db, db,
stripeCli, stripeCli,
@@ -81,14 +81,10 @@ const handleInvoiceCheckoutVoided = async ({
}; };
export const handleInvoiceUpdated = async ({ export const handleInvoiceUpdated = async ({
env,
event, event,
stripeCli,
req, req,
}: { }: {
env: AppEnv;
event: Stripe.Event; event: Stripe.Event;
stripeCli: Stripe;
req: any; req: any;
}) => { }) => {
const invoiceObject = event.data.object as Stripe.Invoice; const invoiceObject = event.data.object as Stripe.Invoice;
@@ -97,13 +93,6 @@ export const handleInvoiceUpdated = async ({
stripeId: invoiceObject.id!, stripeId: invoiceObject.id!,
}); });
// const invoice = await getFullStripeInvoice({
// stripeCli,
// stripeId: invoiceObject.id!,
// });
const prevAttributes = event.data.previous_attributes as any;
const updates: Partial<Invoice> = {}; const updates: Partial<Invoice> = {};
if (invoiceObject.status === "void") { if (invoiceObject.status === "void") {

View File

@@ -1,10 +1,10 @@
import Stripe from "stripe"; import type Stripe from "stripe";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { import {
getFullStripeSub, getFullStripeSub,
subIsPrematurelyCanceled, subIsPrematurelyCanceled,
} from "../stripeSubUtils.js"; } from "../stripeSubUtils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { handleCusProductDeleted } from "./handleSubDeleted/handleCusProductDeleted.js"; import { handleCusProductDeleted } from "./handleSubDeleted/handleCusProductDeleted.js";
export const handleSubDeleted = async ({ export const handleSubDeleted = async ({
@@ -60,7 +60,7 @@ export const handleSubDeleted = async ({
} }
// Prematurely canceled if cancel_at_period_end is false or cancel_at is more than 20 seconds apart from current_period_end // Prematurely canceled if cancel_at_period_end is false or cancel_at is more than 20 seconds apart from current_period_end
let prematurelyCanceled = subIsPrematurelyCanceled(subscription); const prematurelyCanceled = subIsPrematurelyCanceled(subscription);
// const batchUpdate = []; // const batchUpdate = [];
for (const cusProduct of activeCusProducts) { for (const cusProduct of activeCusProducts) {

View File

@@ -1,7 +1,6 @@
import type { AppEnv, Organization } from "@autumn/shared"; import type { AppEnv, Organization } from "@autumn/shared";
import type Stripe from "stripe"; import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js"; import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
export const handleSubscriptionScheduleCanceled = async ({ export const handleSubscriptionScheduleCanceled = async ({
@@ -9,13 +8,11 @@ export const handleSubscriptionScheduleCanceled = async ({
schedule, schedule,
env, env,
org, org,
logger,
}: { }: {
db: DrizzleCli; db: DrizzleCli;
schedule: Stripe.SubscriptionSchedule; schedule: Stripe.SubscriptionSchedule;
org: Organization; org: Organization;
env: AppEnv; env: AppEnv;
logger: any;
}) => { }) => {
const cusProductsOnSchedule = await CusProductService.getByScheduleId({ const cusProductsOnSchedule = await CusProductService.getByScheduleId({
db, db,
@@ -25,59 +22,4 @@ export const handleSubscriptionScheduleCanceled = async ({
}); });
if (cusProductsOnSchedule.length === 0) return; if (cusProductsOnSchedule.length === 0) return;
for (const cusProduct of cusProductsOnSchedule) {
const stripeCli = createStripeCli({ org, env });
// if (cusProduct.status === CusProductStatus.Scheduled) {
// // let otherScheduledIds = cusProduct.scheduled_ids?.filter(
// // (id: string) => id !== schedule.id
// // );
// // for (const id of otherScheduledIds || []) {
// // try {
// // await stripeCli.subscriptionSchedules.cancel(id);
// // console.log(" - Cancelled scheduled id", id);
// // } catch (error) {
// // console.error("Failed to cancel subscription schedule:", id, error);
// // }
// // }
// await CusProductService.delete({
// db,
// cusProductId: cusProduct.id,
// });
// } else {
// // Here -> Should do something different, maybe... reactivate future product?
// await CusProductService.update({
// db,
// cusProductId: cusProduct.id,
// updates: {
// scheduled_ids: cusProduct.scheduled_ids?.filter(
// (id: string) => id !== schedule.id
// ),
// },
// });
// }
}
// // Delete from subscriptions
// try {
// let autumnSub = await SubService.getFromScheduleId({
// db,
// scheduleId: schedule.id,
// });
// if (autumnSub && !autumnSub.stripe_id) {
// await SubService.deleteFromScheduleId({
// db,
// scheduleId: schedule.id,
// });
// }
// } catch (error) {
// logger.error(
// `handleSubScheduleCanceled: failed to delete from subscriptions table`,
// error
// );
// }
}; };

View File

@@ -102,7 +102,7 @@ export const handleSchedulePhaseCompleted = async ({
// Maybe activate default product? // Maybe activate default product?
await deleteCachedApiCustomer({ await deleteCachedApiCustomer({
customerId: cusProduct.internal_customer_id || "", customerId: cusProduct.customer?.id || "",
orgId: org.id, orgId: org.id,
env, env,
source: "handleSchedulePhaseCompleted", source: "handleSchedulePhaseCompleted",

View File

@@ -1,6 +1,5 @@
import { AttachScenario, type FullCusProduct } from "@autumn/shared"; import { AttachScenario, type FullCusProduct } from "@autumn/shared";
import type Stripe from "stripe"; import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js"; import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js";
import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheUtils.js"; import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheUtils.js";
@@ -32,26 +31,6 @@ const isSubRenewed = ({
}; };
}; };
const updateCusProductRenewed = async ({
db,
sub,
}: {
db: DrizzleCli;
sub: Stripe.Subscription;
}) => {
if (sub.schedule) {
return;
}
await CusProductService.updateByStripeSubId({
db,
stripeSubId: sub.id,
updates: { canceled_at: null, canceled: false },
});
return;
};
export const handleSubRenewed = async ({ export const handleSubRenewed = async ({
req, req,
prevAttributes, prevAttributes,
@@ -131,5 +110,5 @@ export const handleSubRenewed = async ({
), ),
}); });
} }
} catch (error) {} } catch (_error) {}
}; };

View File

@@ -1,15 +1,14 @@
import {
AttachParams,
InsertCusProductParams,
} from "@/internal/customers/cusProducts/AttachParams.js";
import { import {
cusProductToEnts, cusProductToEnts,
cusProductToPrices, cusProductToPrices,
cusProductToProduct, cusProductToProduct,
type Entity,
type FullCusProduct,
type FullCustomer,
} from "@autumn/shared"; } from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js"; import type Stripe from "stripe";
import { Entity, FullCusProduct, FullCustomer } from "@autumn/shared"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import Stripe from "stripe"; import type { ExtendedRequest } from "@/utils/models/Request.js";
export const webhookToAttachParams = ({ export const webhookToAttachParams = ({
req, req,

View File

@@ -1,4 +1,3 @@
import { SupabaseClient } from "@supabase/supabase-js";
import { createSupabaseClient } from "../supabaseUtils.js"; import { createSupabaseClient } from "../supabaseUtils.js";
export const readFile = async ({ export const readFile = async ({

View File

@@ -1,38 +1,4 @@
import { createClient } from "@supabase/supabase-js"; import { createClient } from "@supabase/supabase-js";
import fetchRetry from "fetch-retry";
// Wrap the global fetch with fetch-retry
const fetchWithRetry = fetchRetry(fetch, {
retries: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // Exponential backoff starting at 1s, max 30s
retryOn: (attempt, error, response) => {
// Retry on gateway errors (502) and Cloudflare errors (520)
let shouldRetry = false;
try {
if (
error?.message?.includes("cloudflare") ||
error?.message?.includes("fetch failed")
) {
shouldRetry = true;
}
} catch (error) {}
if (
(response && (response.status === 502 || response.status === 520)) ||
shouldRetry
) {
console.warn(
`Retrying request... Attempt #${attempt + 1} - Status: ${
response?.status
}`,
);
return true;
}
return false;
},
});
export const createSupabaseClient = () => { export const createSupabaseClient = () => {
try { try {

View File

@@ -65,7 +65,9 @@ export const sendSvixEvent = safeSvix({
export const sendCustomSvixEvent = safeSvix({ export const sendCustomSvixEvent = safeSvix({
fn: async ({ fn: async ({
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be useful in the future
org, org,
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be useful in the future
env, env,
eventType, eventType,
data, data,

View File

@@ -137,6 +137,7 @@ export const listVercelPlansForOrg = async ({
org, org,
env, env,
metadata, metadata,
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be useful in the future
canCancel = true, canCancel = true,
}: { }: {
db: DrizzleCli; db: DrizzleCli;

View File

@@ -8,7 +8,7 @@ export const handleGetInstallation = createRoute({
handler: async (c) => { handler: async (c) => {
const ctx = c.get("ctx"); const ctx = c.get("ctx");
const { integrationConfigurationId } = c.req.param(); const { integrationConfigurationId } = c.req.param();
const { db, org, logger } = ctx; const { db, org } = ctx;
const customer = await CusService.getByVercelId({ const customer = await CusService.getByVercelId({
db, db,

View File

@@ -37,13 +37,7 @@ export const handleMarketplaceInvoicePaid = async ({
invoiceDate: string; invoiceDate: string;
}; };
}) => { }) => {
const { const { installationId, invoiceId, externalInvoiceId, invoiceDate } = payload;
installationId,
invoiceId,
externalInvoiceId,
invoiceTotal,
invoiceDate,
} = payload;
const stripeCli = createStripeCli({ org, env }); const stripeCli = createStripeCli({ org, env });
@@ -160,7 +154,6 @@ export const handleMarketplaceInvoicePaid = async ({
} }
if (isRenewal) { if (isRenewal) {
// Call sendUsageAndReset which handles all balance resets
const activeProduct = existingCusProducts[0]; const activeProduct = existingCusProducts[0];
await sendUsageAndReset({ await sendUsageAndReset({
@@ -169,7 +162,6 @@ export const handleMarketplaceInvoicePaid = async ({
org, org,
env, env,
invoice, invoice,
stripeSubs: [subscription],
logger, logger,
submitUsage: false, // Usage already submitted in invoice.created submitUsage: false, // Usage already submitted in invoice.created
resetBalance: true, // Payment confirmed - now safe to reset balance resetBalance: true, // Payment confirmed - now safe to reset balance

View File

@@ -26,13 +26,7 @@ export const handleMarketplaceInvoiceNotPaid = async ({
invoiceDate: string; invoiceDate: string;
}; };
}) => { }) => {
const { const { installationId, invoiceId, externalInvoiceId, invoiceDate } = payload;
installationId,
invoiceId,
externalInvoiceId,
invoiceTotal,
invoiceDate,
} = payload;
const stripeCli = createStripeCli({ org, env }); const stripeCli = createStripeCli({ org, env });

View File

@@ -146,7 +146,7 @@ export const vercelOidcAuthMiddleware = async (c: any, next: any) => {
let token: string; let token: string;
try { try {
token = getAuthorizationToken(authHeader); token = getAuthorizationToken(authHeader);
} catch (error) { } catch (_error) {
return c.json( return c.json(
{ error: "Unauthorized", code: "invalid_auth_header_format" }, { error: "Unauthorized", code: "invalid_auth_header_format" },
401, 401,
@@ -160,7 +160,7 @@ export const vercelOidcAuthMiddleware = async (c: any, next: any) => {
} catch (error: any) { } catch (error: any) {
return c.json( return c.json(
{ {
error: "Unauthorized" + error.message, error: `Unauthorized: ${error.message}`,
code: code:
error instanceof AuthError error instanceof AuthError
? "auth_failed" ? "auth_failed"

View File

@@ -265,7 +265,7 @@ export const getVercelAttachBody = ({
const attachParams: AttachParams = { const attachParams: AttachParams = {
stripeCli, stripeCli,
stripeCus: stripeCustomer, stripeCus: stripeCustomer,
now: Date.now(), now: now ?? Date.now(),
paymentMethod: customPaymentMethod, // Pass Vercel custom payment method paymentMethod: customPaymentMethod, // Pass Vercel custom payment method
org, org,
customer, customer,

View File

@@ -47,7 +47,7 @@ export const logVercelWebhook = ({
}; };
export const vercelLogMiddleware = async (c: Context<HonoEnv>, next: Next) => { export const vercelLogMiddleware = async (c: Context<HonoEnv>, next: Next) => {
const { db, logger, org } = c.get("ctx"); const { logger, org } = c.get("ctx");
const body = await c.req.json(); const body = await c.req.json();
logVercelWebhook({ logger, org, event: body }); logVercelWebhook({ logger, org, event: body });

View File

@@ -1,97 +0,0 @@
import { Request, Response } from "express";
import { Webhook } from "svix";
import { OrgService } from "@/internal/orgs/OrgService.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { AppEnv } from "autumn-js";
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
import { deleteStripeWebhook } from "@/internal/orgs/orgUtils.js";
import { eq } from "drizzle-orm";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { Organization, organizations } from "@autumn/shared";
// 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 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;
// }
// 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;
// };
// export const handleClerkWebhook = async (req: any, res: any) => {
// let event = await verifyClerkWebhook(req, res);
// if (!event) {
// return;
// }
// 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;
// case "organization.deleted":
// await handleOrgDeleted({
// db: req.db,
// eventData,
// });
// break;
// default:
// break;
// }
// } catch (error) {
// handleRequestError({
// req,
// error,
// res,
// action: "Handle Clerk Webhook",
// });
// return;
// }
// return void res.status(200).json({
// success: true,
// message: "Webhook received",
// });
// };

View File

@@ -27,12 +27,10 @@ const parseCustomerIdFromUrl = ({
const logResponse = async ({ const logResponse = async ({
ctx, ctx,
c, c,
method,
skipUrls, skipUrls,
}: { }: {
ctx: any; ctx: any;
c: Context<HonoEnv>; c: Context<HonoEnv>;
method: string;
skipUrls: string[]; skipUrls: string[];
}) => { }) => {
try { try {
@@ -116,7 +114,7 @@ export const analyticsMiddleware = async (c: Context<HonoEnv>, next: Next) => {
// Log response asynchronously without blocking (runs after response is sent) // Log response asynchronously without blocking (runs after response is sent)
Promise.resolve() Promise.resolve()
.then(() => logResponse({ ctx, c, method, skipUrls })) .then(() => logResponse({ ctx, c, skipUrls }))
.catch((error) => { .catch((error) => {
console.error("Failed to log response to logtail"); console.error("Failed to log response to logtail");
console.error(error); console.error(error);

View File

@@ -31,7 +31,6 @@ export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
}; };
const method = c.req.method; const method = c.req.method;
const path = c.req.path;
let body = null; let body = null;
if (method === "POST" || method === "PUT" || method === "PATCH") { if (method === "POST" || method === "PUT" || method === "PATCH") {

View File

@@ -189,7 +189,7 @@ const init = async () => {
app.use("/webhooks", webhooksRouter); app.use("/webhooks", webhooksRouter);
app.use(express.json()); app.use(express.json());
app.use(async (req: any, res: any, next: any) => { app.use(async (req: any, _res: any, next: any) => {
req.logger.info(`${req.method} ${req.originalUrl}`, { req.logger.info(`${req.method} ${req.originalUrl}`, {
context: { context: {
body: req.body, body: req.body,
@@ -228,7 +228,7 @@ if (process.env.NODE_ENV === "development") {
cluster.fork(); cluster.fork();
} }
cluster.on("exit", (worker, code, signal) => { cluster.on("exit", (worker, _code, _signal) => {
logger.error(`WORKER DIED: ${worker.process.pid}`); logger.error(`WORKER DIED: ${worker.process.pid}`);
cluster.fork(); cluster.fork();
}); });

View File

@@ -1,8 +1,8 @@
import { handleFrontendReqError } from "@/utils/errorUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { member, organizations, user } from "@autumn/shared"; import { member, organizations, user } from "@autumn/shared";
import { and, desc, eq, gt, gte, ilike, inArray, lt, or } from "drizzle-orm"; import { and, desc, eq, gt, gte, ilike, inArray, lt, or } from "drizzle-orm";
import { Router } from "express"; import { Router } from "express";
import { handleFrontendReqError } from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
export const adminRouter: Router = Router(); export const adminRouter: Router = Router();
@@ -12,7 +12,7 @@ adminRouter.get("/users", async (req: any, res: any) => {
try { try {
const { db } = req as ExtendedRequest; const { db } = req as ExtendedRequest;
let { sortKey, search, after, before } = req.query; let { search, after, before } = req.query;
if (after) { if (after) {
after = { after = {
@@ -136,9 +136,9 @@ adminRouter.get("/orgs", async (req: any, res: any) => {
.orderBy(desc(organizations.createdAt), desc(organizations.id)) .orderBy(desc(organizations.createdAt), desc(organizations.id))
.limit(21); .limit(21);
let orgIds = orgs.map((org) => org.id); const orgIds = orgs.map((org) => org.id);
let memberships = await db const memberships = await db
.select() .select()
.from(member) .from(member)
.leftJoin(user, eq(member.userId, user.id)) .leftJoin(user, eq(member.userId, user.id))

View File

@@ -1,14 +0,0 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { user } from "@autumn/shared";
import { count } from "drizzle-orm";
// Required stats (by interval):
// 1. User count
// 2. Retained count
// 3. Churned count
export const getUserCount = async ({ db }: { db: DrizzleCli }) => {
const userCount = await db.select({ count: count() }).from(user);
return userCount[0].count;
};

View File

@@ -1,5 +1,5 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { type ActionInsert, actions } from "@autumn/shared"; import { type ActionInsert, actions } from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
export class ActionService { export class ActionService {
static async insert(db: DrizzleCli, data: ActionInsert | ActionInsert[]) { static async insert(db: DrizzleCli, data: ActionInsert | ActionInsert[]) {

View File

@@ -73,7 +73,7 @@ export class AnalyticsService {
} }
static async getTopUser({ req }: { req: ExtendedRequest }) { static async getTopUser({ req }: { req: ExtendedRequest }) {
const { clickhouseClient, org, env, db } = req; const { clickhouseClient, org, env } = req;
const query = ` const query = `
SELECT SELECT
@@ -136,7 +136,7 @@ WHERE
req: ExtendedRequest; req: ExtendedRequest;
eventName?: string; eventName?: string;
}) { }) {
const { clickhouseClient, org, env, db } = req; const { clickhouseClient, org, env } = req;
const query = ` const query = `
SELECT SUM( SELECT SUM(
@@ -165,7 +165,7 @@ WHERE event_name = {eventName:String}
} }
static async getTotalCustomers({ req }: { req: ExtendedRequest }) { static async getTotalCustomers({ req }: { req: ExtendedRequest }) {
const { clickhouseClient, org, env, db } = req; const { clickhouseClient, org, env } = req;
const query = `SELECT COUNT(DISTINCT id) AS total_customers const query = `SELECT COUNT(DISTINCT id) AS total_customers
FROM customers FROM customers
WHERE org_id = {org_id:String} WHERE org_id = {org_id:String}
@@ -213,8 +213,6 @@ WHERE org_id = {org_id:String}
const getBCResults = const getBCResults =
isBillingCycle && !aggregateAll && customer isBillingCycle && !aggregateAll && customer
? ((await getBillingCycleStartDate( ? ((await getBillingCycleStartDate(
env,
org?.id,
customer, customer,
db, db,
intervalType as "1bc" | "3bc", intervalType as "1bc" | "3bc",
@@ -335,8 +333,6 @@ order by dr.period;
const getBCResults = const getBCResults =
isBillingCycle && !aggregateAll && customer isBillingCycle && !aggregateAll && customer
? ((await getBillingCycleStartDate( ? ((await getBillingCycleStartDate(
env,
org?.id,
customer, customer,
db, db,
intervalType as "1bc" | "3bc", intervalType as "1bc" | "3bc",
@@ -380,15 +376,6 @@ order by dr.period;
limit 10000 limit 10000
`; `;
const filledQuery = query
.replace("{organizationId:String}", org?.id ?? "")
.replace("{customerId:String}", params.customer_id ?? "")
.replace("{startDate:String}", finalStartDate)
.replace("{endDate:String}", finalEndDate)
.replace("{env:String}", env);
// console.log("filledQuery", filledQuery);
const result = await clickhouseClient.query({ const result = await clickhouseClient.query({
query: query, query: query,
query_params: { query_params: {

View File

@@ -1,4 +1,4 @@
import { ExtendedRequest } from "@/utils/models/Request.js"; import type { ExtendedRequest } from "@/utils/models/Request.js";
export class RevenueService { export class RevenueService {
static clickhouseAvailable = static clickhouseAvailable =

View File

@@ -11,7 +11,6 @@ import { routeHandler } from "@/utils/routerUtils.js";
const analyticsRouter = Router(); const analyticsRouter = Router();
const RangeEnum = z.enum(["24h", "7d", "30d", "90d", "last_cycle"]); const RangeEnum = z.enum(["24h", "7d", "30d", "90d", "last_cycle"]);
type Range = z.infer<typeof RangeEnum>;
analyticsRouter.post("", (req, res) => analyticsRouter.post("", (req, res) =>
routeHandler({ routeHandler({

View File

@@ -1,23 +1,17 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { import {
ErrCode, cusProductToProduct,
FullCustomer,
FullCusProduct,
CusProductStatus,
Subscription,
AppEnv,
FullProduct,
CustomerEntitlement,
FullCustomerEntitlement,
EntInterval, EntInterval,
type FullCusProduct,
type FullCustomer,
type FullCustomerEntitlement,
type FullProduct,
type Subscription,
} from "@autumn/shared"; } from "@autumn/shared";
import { cusProductToProduct } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js";
import { ACTIVE_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js"; import { ACTIVE_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
import { isFreeProduct } from "../products/productUtils.js"; import { isFreeProduct } from "../products/productUtils.js";
export async function getBillingCycleStartDate( export async function getBillingCycleStartDate(
env: AppEnv,
orgId: string,
customer?: FullCustomer, customer?: FullCustomer,
db?: DrizzleCli, db?: DrizzleCli,
intervalType?: "1bc" | "3bc", intervalType?: "1bc" | "3bc",
@@ -199,22 +193,26 @@ export function calculateStartDateFromInterval(
return formatDateToString( return formatDateToString(
new Date(nextResetAt! - 7 * 24 * 60 * 60 * 1000), new Date(nextResetAt! - 7 * 24 * 60 * 60 * 1000),
); );
case EntInterval.Month: case EntInterval.Month: {
const monthResetDate = new Date(nextResetAt!); const monthResetDate = new Date(nextResetAt!);
monthResetDate.setMonth(monthResetDate.getMonth() - 1); monthResetDate.setMonth(monthResetDate.getMonth() - 1);
return formatDateToString(monthResetDate); return formatDateToString(monthResetDate);
case EntInterval.Quarter: }
case EntInterval.Quarter: {
const quarterResetDate = new Date(nextResetAt!); const quarterResetDate = new Date(nextResetAt!);
quarterResetDate.setMonth(quarterResetDate.getMonth() - 3); quarterResetDate.setMonth(quarterResetDate.getMonth() - 3);
return formatDateToString(quarterResetDate); return formatDateToString(quarterResetDate);
case EntInterval.SemiAnnual: }
case EntInterval.SemiAnnual: {
const semiAnnualResetDate = new Date(nextResetAt!); const semiAnnualResetDate = new Date(nextResetAt!);
semiAnnualResetDate.setMonth(semiAnnualResetDate.getMonth() - 6); semiAnnualResetDate.setMonth(semiAnnualResetDate.getMonth() - 6);
return formatDateToString(semiAnnualResetDate); return formatDateToString(semiAnnualResetDate);
case EntInterval.Year: }
case EntInterval.Year: {
const yearResetDate = new Date(nextResetAt!); const yearResetDate = new Date(nextResetAt!);
yearResetDate.setFullYear(yearResetDate.getFullYear() - 1); yearResetDate.setFullYear(yearResetDate.getFullYear() - 1);
return formatDateToString(yearResetDate); return formatDateToString(yearResetDate);
}
default: default:
return null; return null;
} }

View File

@@ -178,8 +178,6 @@ export const handleProductsUpdated = async ({
// }); // });
// } // }
console.log("Sending svix event for products updated");
// 2. Send Svix event // 2. Send Svix event
await sendSvixEvent({ await sendSvixEvent({
org, org,

View File

@@ -74,7 +74,7 @@ analyticsRouter.get("/event_names", async (req: any, res: any) =>
); );
const getTopEvents = async ({ req }: { req: ExtendedRequest }) => { const getTopEvents = async ({ req }: { req: ExtendedRequest }) => {
const { org, env, features } = req; const { features } = req;
const topEventNamesRes = await AnalyticsService.getTopEventNames({ const topEventNamesRes = await AnalyticsService.getTopEventNames({
req, req,

View File

@@ -1,11 +0,0 @@
export interface FeatureCheckPreviewParams {
customerId: string;
featureId: string;
quantity: number;
}
export const getFeatureCheckPreview = async ({
customerId,
featureId,
quantity,
}: FeatureCheckPreviewParams) => {};

View File

@@ -98,8 +98,6 @@ export const syncItem = async ({
redisEntity = apiCustomer; redisEntity = apiCustomer;
} }
console.log("Redis entity: ", redisEntity);
// Get fresh customer from DB (no locking - let deduction handle it) // Get fresh customer from DB (no locking - let deduction handle it)
const fullCus = await CusService.getFull({ const fullCus = await CusService.getFull({
db, db,

View File

@@ -2,6 +2,7 @@ import {
type ApiEntityV1, type ApiEntityV1,
addToExpand, addToExpand,
CusExpand, CusExpand,
type EntityLegacyData,
type FullCustomer, type FullCustomer,
filterEntityLevelCusProducts, filterEntityLevelCusProducts,
filterOutEntitiesFromCusProducts, filterOutEntitiesFromCusProducts,
@@ -58,14 +59,17 @@ export const setCachedApiCustomer = async ({
}); });
// Build entities first // Build entities first
const entityBatch: { entityId: string; entityData: ApiEntityV1 }[] = []; const entityBatch: {
entityId: string;
entityData: ApiEntityV1 & { legacyData: EntityLegacyData };
}[] = [];
const entityFullCus = { const entityFullCus = {
...fullCus, ...fullCus,
customer_products: entityLevelCusProducts, customer_products: entityLevelCusProducts,
}; };
for (const entity of fullCus.entities) { for (const entity of fullCus.entities) {
const { apiEntity } = await getApiEntityBase({ const { apiEntity, legacyData: entityLegacyData } = await getApiEntityBase({
ctx: ctxWithExpand, ctx: ctxWithExpand,
fullCus: entityFullCus, fullCus: entityFullCus,
entity, entity,
@@ -74,17 +78,27 @@ export const setCachedApiCustomer = async ({
entityBatch.push({ entityBatch.push({
entityId: entity.id, entityId: entity.id,
entityData: apiEntity, entityData: {
...apiEntity,
legacyData: entityLegacyData,
},
}); });
} }
// Then write to Redis // Then write to Redis
const masterApiCustomerData = { const masterApiCustomerData = {
...masterApiCustomer, ...masterApiCustomer,
entities: fullCus.entities, entities: fullCus.entities.filter((e) => e.id !== null),
legacyData, legacyData,
}; };
if (masterApiCustomerData.id === null) return;
// console.log(
// `Setting cached api customer ${customerId}, masterApiCustomerData: `,
// masterApiCustomerData,
// );
await tryRedisWrite(async () => { await tryRedisWrite(async () => {
await redis.eval( await redis.eval(
SET_CUSTOMER_SCRIPT, SET_CUSTOMER_SCRIPT,
@@ -95,11 +109,15 @@ export const setCachedApiCustomer = async ({
customerId, customerId,
); );
const filteredEntityBatch = entityBatch.filter(
(e) => e.entityData.id !== null,
);
if (entityBatch.length > 0) { if (entityBatch.length > 0) {
await redis.eval( await redis.eval(
SET_ENTITIES_BATCH_SCRIPT, SET_ENTITIES_BATCH_SCRIPT,
0, 0,
JSON.stringify(entityBatch), JSON.stringify(filteredEntityBatch),
org.id, org.id,
env, env,
); );

View File

@@ -10,6 +10,7 @@ import {
import type Stripe from "stripe"; import type Stripe from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { getOriginalCouponId } from "../../../rewards/rewardUtils";
export const getCusRewards = async ({ export const getCusRewards = async ({
org, org,
@@ -35,16 +36,17 @@ export const getCusRewards = async ({
const stripeCli = createStripeCli({ const stripeCli = createStripeCli({
org, org,
env, env,
legacyVersion: true,
}); });
const [stripeCus, stripeSubs] = await Promise.all([ const [stripeCus, stripeSubs] = await Promise.all([
stripeCli.customers.retrieve( stripeCli.customers.retrieve(fullCus.processor?.id, {
fullCus.processor?.id, expand: ["discount.coupon"],
) as Promise<Stripe.Customer>, }) as Promise<Stripe.Customer>,
getStripeSubs({ getStripeSubs({
stripeCli, stripeCli,
subIds, subIds,
expand: ["discounts", "discounts.source.coupon"], expand: ["discounts", "discounts.coupon"],
}), }),
]); ]);
@@ -59,14 +61,12 @@ export const getCusRewards = async ({
const rewards = { const rewards = {
discounts: stripeDiscounts discounts: stripeDiscounts
.map((d) => { .map((d) => {
if (typeof d.source.coupon === "string") { if (!("coupon" in d) || typeof d.coupon === "string") {
return null; return null;
} }
const coupon = d.source.coupon; const coupon = d.coupon as Stripe.Coupon;
if (!coupon) { const couponId = getOriginalCouponId(coupon.id);
return null;
}
let duration_type: CouponDurationType; let duration_type: CouponDurationType;
let duration_value = 0; let duration_value = 0;
@@ -81,7 +81,7 @@ export const getCusRewards = async ({
duration_type = CouponDurationType.OneOff; duration_type = CouponDurationType.OneOff;
} }
return { return {
id: coupon.id, id: couponId,
name: coupon.name ?? "", name: coupon.name ?? "",
type: coupon.amount_off type: coupon.amount_off
? RewardType.FixedDiscount ? RewardType.FixedDiscount

View File

@@ -158,7 +158,7 @@ export const handleCreateOtp = async (req: any, res: any) =>
res, res,
action: "Create OTP", action: "Create OTP",
handler: async () => { handler: async () => {
const { orgId, env, db } = req; const { orgId } = req;
// Check if there's already an OTP to use // Check if there's already an OTP to use
const maybeCacheKey = `orgOTPExists:${orgId}`; const maybeCacheKey = `orgOTPExists:${orgId}`;
@@ -208,7 +208,10 @@ export const handleGetOtp = async (req: any, res: any) =>
const { db, env } = req; const { db, env } = req;
const { otp } = req.params; const { otp } = req.params;
const cacheKey = `otp:${otp}`; const cacheKey = `otp:${otp}`;
const cacheData = await CacheManager.getJson(cacheKey); const cacheData = await CacheManager.getJson<{
orgId: string;
stripeFlowAuthKey: string;
}>(cacheKey);
if (!cacheData) { if (!cacheData) {
res.status(404).json({ error: "OTP not found" }); res.status(404).json({ error: "OTP not found" });
return; return;
@@ -291,7 +294,7 @@ devRouter.post("/cli/stripe", async (req: any, res: any) => {
return; return;
} }
const cacheData = await CacheManager.getJson(key); const cacheData = await CacheManager.getJson<{ orgId: string }>(key);
if (!cacheData) { if (!cacheData) {
res.status(404).json({ message: "Key not found" }); res.status(404).json({ message: "Key not found" });
return; return;

View File

@@ -123,7 +123,7 @@ export const getCachedApiEntity = async ({
ctx, ctx,
entity, entity,
fullCus: fullCus, fullCus: fullCus,
withAutumnId: !skipCache, withAutumnId: true,
}); });
const { apiEntity: pureApiEntity } = await getApiEntityBase({ const { apiEntity: pureApiEntity } = await getApiEntityBase({

View File

@@ -96,6 +96,7 @@ export const createEntities = async ({
const clonedFullCus = structuredClone(fullCus); const clonedFullCus = structuredClone(fullCus);
clonedFullCus.entity = entity; clonedFullCus.entity = entity;
const apiEntity = await getApiEntity({ const apiEntity = await getApiEntity({
ctx, ctx,
customerId, customerId,

View File

@@ -96,7 +96,7 @@ export const handleCreatePlan = createRoute({
// body: CreateProductV2ParamsSchema, // body: CreateProductV2ParamsSchema,
versionedBody: { versionedBody: {
latest: CreatePlanParamsSchema, latest: CreatePlanParamsSchema,
[ApiVersion.V1_2]: CreateProductV2ParamsSchema, [ApiVersion.V1_Beta]: CreateProductV2ParamsSchema,
}, },
resource: AffectedResource.Product, resource: AffectedResource.Product,
handler: async (c) => { handler: async (c) => {

View File

@@ -39,11 +39,11 @@ import { handleUpdateProductDetails } from "./updateProductDetails.js";
export const handleUpdatePlan = createRoute({ export const handleUpdatePlan = createRoute({
versionedBody: { versionedBody: {
latest: UpdatePlanParamsSchema, latest: UpdatePlanParamsSchema,
[ApiVersion.V1_2]: UpdateProductV2ParamsSchema, [ApiVersion.V1_Beta]: UpdateProductV2ParamsSchema,
}, },
versionedQuery: { versionedQuery: {
latest: UpdatePlanQuerySchema, latest: UpdatePlanQuerySchema,
[ApiVersion.V1_2]: UpdateProductQuerySchema, [ApiVersion.V1_Beta]: UpdateProductQuerySchema,
}, },
resource: AffectedResource.Product, resource: AffectedResource.Product,
handler: async (c) => { handler: async (c) => {
@@ -57,6 +57,7 @@ export const handleUpdatePlan = createRoute({
// Convert to ProductV2 format only if client sent V2 Plan format // Convert to ProductV2 format only if client sent V2 Plan format
// V1.2 clients already send ProductV2, no conversion needed // V1.2 clients already send ProductV2, no conversion needed
const v1_2Body = ctx.apiVersion.gte(new ApiVersionClass(ApiVersion.V2_0)) const v1_2Body = ctx.apiVersion.gte(new ApiVersionClass(ApiVersion.V2_0))
? planToProductV2({ plan: body as ApiPlan, features: ctx.features }) ? planToProductV2({ plan: body as ApiPlan, features: ctx.features })
: (body as UpdateProductV2Params); : (body as UpdateProductV2Params);

View File

@@ -6,6 +6,7 @@ import { handleDeleteProduct as handleDeleteProductHono } from "./handlers/handl
import { handleGetPlan } from "./handlers/handleGetPlan.js"; import { handleGetPlan } from "./handlers/handleGetPlan.js";
import { handleGetPlanDeleteInfo } from "./handlers/handleGetPlanDeleteInfo.js"; import { handleGetPlanDeleteInfo } from "./handlers/handleGetPlanDeleteInfo.js";
import { handleListPlans } from "./handlers/handleListPlans.js"; import { handleListPlans } from "./handlers/handleListPlans.js";
import { handleMigrateProductV2 } from "./handlers/handleMigrateProductV2.js";
import { handlePlanHasCustomers } from "./handlers/handlePlanHasCustomers.js"; import { handlePlanHasCustomers } from "./handlers/handlePlanHasCustomers.js";
import { handleUpdatePlan } from "./handlers/handleUpdateProduct/handleUpdatePlan.js"; import { handleUpdatePlan } from "./handlers/handleUpdateProduct/handleUpdatePlan.js";
@@ -16,11 +17,15 @@ honoProductBetaRouter.get("", ...handleListPlans);
export const honoProductRouter = new Hono<HonoEnv>(); export const honoProductRouter = new Hono<HonoEnv>();
export const migrationRouter = new Hono<HonoEnv>(); export const migrationRouter = new Hono<HonoEnv>();
// Migrations
migrationRouter.post("/migrations", ...handleMigrateProductV2);
// CRUD // CRUD
honoProductRouter.get("", ...handleListPlans); honoProductRouter.get("", ...handleListPlans);
honoProductRouter.post("", ...handleCreatePlan); honoProductRouter.post("", ...handleCreatePlan);
honoProductRouter.get("/:product_id", ...handleGetPlan); honoProductRouter.get("/:product_id", ...handleGetPlan);
honoProductRouter.post("/:product_id", ...handleUpdatePlan); // will be deprecated honoProductRouter.post("/:product_id", ...handleUpdatePlan); // will be deprecated
honoProductRouter.patch("/:product_id", ...handleUpdatePlan); // will be deprecated
honoProductRouter.delete("/:product_id", ...handleDeleteProductHono); honoProductRouter.delete("/:product_id", ...handleDeleteProductHono);
// Others // Others

View File

@@ -48,7 +48,8 @@ export class ViewsService {
// Also save to a list for easy retrieval // Also save to a list for easy retrieval
const listKey = `saved_views_list:${orgId}:${env}`; const listKey = `saved_views_list:${orgId}:${env}`;
const existingViews = (await CacheManager.getJson(listKey)) || []; const existingViews =
(await CacheManager.getJson<string[]>(listKey)) || [];
existingViews.push(viewId); existingViews.push(viewId);
await CacheManager.setJson(listKey, existingViews, "forever"); // No TTL await CacheManager.setJson(listKey, existingViews, "forever"); // No TTL
@@ -74,12 +75,18 @@ export class ViewsService {
const env = req.env; const env = req.env;
const listKey = `saved_views_list:${orgId}:${env}`; const listKey = `saved_views_list:${orgId}:${env}`;
const viewIds = (await CacheManager.getJson(listKey)) || []; const viewIds = (await CacheManager.getJson<string[]>(listKey)) || [];
const views = []; const views = [];
for (const viewId of viewIds) { for (const viewId of viewIds) {
const key = `saved_views:${orgId}:${env}:${viewId}`; const key = `saved_views:${orgId}:${env}:${viewId}`;
const view = await CacheManager.getJson(key); const view = await CacheManager.getJson<{
id: string;
name: string;
filters: any;
created_at: string;
}>(key);
if (view) { if (view) {
views.push({ views.push({
id: view.id, id: view.id,
@@ -120,7 +127,8 @@ export class ViewsService {
// Remove from list // Remove from list
const listKey = `saved_views_list:${orgId}:${env}`; const listKey = `saved_views_list:${orgId}:${env}`;
const existingViews = (await CacheManager.getJson(listKey)) || []; const existingViews =
(await CacheManager.getJson<string[]>(listKey)) || [];
const updatedViews = existingViews.filter( const updatedViews = existingViews.filter(
(id: string) => id !== viewId, (id: string) => id !== viewId,
); );

View File

@@ -0,0 +1,29 @@
import { join } from "node:path";
import { config } from "dotenv";
export const loadLocalEnv = () => {
const processDir = process.cwd();
const serverDir = processDir.includes("server")
? processDir
: join(processDir, "server");
// Determine which env file to load based on ENV_FILE environment variable
// Defaults to .env if not specified
const envFileName = process.env.ENV_FILE || ".env";
const envPath = join(serverDir, envFileName);
// Load local .env file FIRST - these will take precedence over Infisical
const result = config({ path: envPath });
if (result.parsed) {
console.log(
`📄 Loading ${Object.keys(result.parsed).length} variables from ${envFileName}`,
);
for (const [key, value] of Object.entries(result.parsed)) {
process.env[key] = value;
}
} else {
console.log(
` No ${envFileName} file found (using only Infisical secrets)`,
);
}
};

View File

@@ -5,17 +5,6 @@ dotenv.config();
import { AppEnv } from "@autumn/shared"; import { AppEnv } from "@autumn/shared";
import { clearOrg, setupOrg } from "@tests/utils/setup.js"; import { clearOrg, setupOrg } from "@tests/utils/setup.js";
import { initDrizzle } from "@/db/initDrizzle.js"; import { initDrizzle } from "@/db/initDrizzle.js";
import {
advanceProducts,
attachProducts,
creditSystems,
entityProducts,
features,
oneTimeProducts,
products,
referralPrograms,
rewards,
} from "./global.js";
const ORG_SLUG = process.env.TESTS_ORG!; const ORG_SLUG = process.env.TESTS_ORG!;
const DEFAULT_ENV = AppEnv.Sandbox; const DEFAULT_ENV = AppEnv.Sandbox;

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