fix: coupons on new stripe version
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
|
||||
"setup": "node scripts/setup/setup.js",
|
||||
"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",
|
||||
"replicate": "bun scripts/db/replicate.ts",
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { loadLocalEnv } from "@server/utils/envUtils.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
/**
|
||||
* Recursively finds all test files in a directory
|
||||
*/
|
||||
@@ -267,46 +270,17 @@ async function runTest() {
|
||||
const frameworkLabel = framework === "bun" ? "Bun" : "Mocha";
|
||||
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
|
||||
const child =
|
||||
framework === "bun"
|
||||
? spawn(
|
||||
"infisical",
|
||||
[
|
||||
"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" },
|
||||
},
|
||||
);
|
||||
const child = spawn("bun", ["test", "--timeout", "0", testFile.relative], {
|
||||
cwd: serverDir,
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, NODE_ENV: "production" },
|
||||
});
|
||||
|
||||
// Store the process group ID
|
||||
const pgid = child.pid;
|
||||
@@ -363,3 +337,25 @@ async function 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" },
|
||||
// },
|
||||
// );
|
||||
|
||||
@@ -14,28 +14,28 @@ fi
|
||||
|
||||
# Run tests using TypeScript runner with compact mode
|
||||
# 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 \
|
||||
'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 \
|
||||
'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 \
|
||||
# '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 \
|
||||
|
||||
@@ -14,15 +14,15 @@ fi
|
||||
|
||||
|
||||
|
||||
# BUN_PARALLEL_COMPACT \
|
||||
# 'server/tests/attach/migrations' \
|
||||
# 'server/tests/attach/others' \
|
||||
# 'server/tests/attach/newVersion' \
|
||||
# 'server/tests/attach/upgradeOld' \
|
||||
# 'server/tests/attach/updateEnts' \
|
||||
# 'server/tests/advanced/check' \
|
||||
# 'server/tests/attach/prepaid' \
|
||||
# 'server/tests/interval/upgrade' \
|
||||
# 'server/tests/interval/multiSub' \
|
||||
# --max=6
|
||||
BUN_PARALLEL_COMPACT \
|
||||
'server/tests/attach/migrations' \
|
||||
'server/tests/attach/others' \
|
||||
'server/tests/attach/newVersion' \
|
||||
'server/tests/attach/upgradeOld' \
|
||||
'server/tests/attach/updateEnts' \
|
||||
'server/tests/advanced/check' \
|
||||
'server/tests/attach/prepaid' \
|
||||
'server/tests/interval/upgrade' \
|
||||
'server/tests/interval/multiSub' \
|
||||
--max=6
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ BUN_PARALLEL_COMPACT \
|
||||
--max=6
|
||||
|
||||
|
||||
# BUN_PARALLEL_COMPACT \
|
||||
# 'server/tests/advanced/usage'
|
||||
# 'server/tests/crud/plan'
|
||||
BUN_PARALLEL_COMPACT \
|
||||
'server/tests/advanced/usage'
|
||||
# 'server/tests/crud/plan'
|
||||
|
||||
# 'server/tests/advanced/referrals/paid' \
|
||||
@@ -1,11 +1,14 @@
|
||||
#!/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 chalk from "chalk";
|
||||
import dotenv from "dotenv";
|
||||
import { readdir } from "fs/promises";
|
||||
import pLimit from "p-limit";
|
||||
import { basename, resolve } from "path";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
// Load environment variables from server/.env
|
||||
dotenv.config({ path: resolve(process.cwd(), "server", ".env") });
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"email": "email dev -p 3001",
|
||||
"start": "bun src/index.ts",
|
||||
"d": "ENV_FILE=.env infisical run --env=dev -- bun dev",
|
||||
"p": "ENV_FILE=.env.prod infisical run --env=prod -- bun dev",
|
||||
"w": "ENV_FILE=.env infisical run --env=dev -- bun workers:dev",
|
||||
"c": "ENV_FILE=.env infisical run --env=dev -- bun cron",
|
||||
"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",
|
||||
|
||||
"start": "bun src/index.ts",
|
||||
"workers": "bun src/workers.ts",
|
||||
"cron": "bun src/cron.ts",
|
||||
"check": "bun src/check.ts",
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
filename="$1"
|
||||
|
||||
|
||||
# Check if the file path contains "shell"
|
||||
|
||||
|
||||
if [[ "$filename" == *"shell"* ]]; then
|
||||
"$filename" "${@:2}"
|
||||
@@ -15,7 +13,7 @@ elif [[ "$filename" == *"/tests/"* ]]; then
|
||||
# Remove .ts extension if present
|
||||
path_after_tests="${path_after_tests%.ts}"
|
||||
# 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
|
||||
"$filename"
|
||||
|
||||
@@ -41,6 +41,7 @@ for _, entityWrapper in ipairs(entities) do
|
||||
created_at = entityData.created_at,
|
||||
env = entityData.env,
|
||||
subscriptions = entityData.subscriptions,
|
||||
legacyData = entityData.legacyData,
|
||||
_balanceFeatureIds = balanceFeatureIds
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ local baseEntity = {
|
||||
created_at = entityData.created_at,
|
||||
env = entityData.env,
|
||||
subscriptions = entityData.subscriptions,
|
||||
legacyData = entityData.legacyData,
|
||||
_balanceFeatureIds = balanceFeatureIds
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { resetCustomerEntitlement } from "./cron/cronUtils.js";
|
||||
import { runProductCron } from "./cron/productCron/runProductCron.js";
|
||||
import { initDrizzle } from "./db/initDrizzle.js";
|
||||
import { CusEntService } from "./internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { OrgService } from "./internal/orgs/OrgService.js";
|
||||
import { notNullish } from "./utils/genUtils.js";
|
||||
|
||||
dotenv.config();
|
||||
@@ -26,8 +25,6 @@ export const cronTask = async () => {
|
||||
batchSize: 500,
|
||||
});
|
||||
|
||||
const cacheEnabledOrgs = await OrgService.getCacheEnabledOrgs({ db });
|
||||
|
||||
const batchSize = 100;
|
||||
for (let i = 0; i < cusEnts.length; i += batchSize) {
|
||||
const batch = cusEnts.slice(i, i + batchSize);
|
||||
@@ -37,7 +34,6 @@ export const cronTask = async () => {
|
||||
resetCustomerEntitlement({
|
||||
db,
|
||||
cusEnt: cusEnt,
|
||||
cacheEnabledOrgs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,11 +94,9 @@ const checkSubAnchor = async ({
|
||||
const handleShortDurationCusEnt = async ({
|
||||
db,
|
||||
cusEnt,
|
||||
cacheEnabledOrgs,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
cusEnt: ResetCusEnt;
|
||||
cacheEnabledOrgs: any[];
|
||||
}) => {
|
||||
const ent = cusEnt.entitlement as FullEntitlement;
|
||||
|
||||
@@ -155,11 +153,9 @@ const shortDurations = [EntInterval.Minute, EntInterval.Hour, EntInterval.Day];
|
||||
export const resetCustomerEntitlement = async ({
|
||||
db,
|
||||
cusEnt,
|
||||
cacheEnabledOrgs,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
cusEnt: ResetCusEnt;
|
||||
cacheEnabledOrgs: any[];
|
||||
}) => {
|
||||
try {
|
||||
const ent = cusEnt.entitlement as FullEntitlement;
|
||||
@@ -171,7 +167,6 @@ export const resetCustomerEntitlement = async ({
|
||||
return await handleShortDurationCusEnt({
|
||||
db,
|
||||
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({
|
||||
db,
|
||||
orgId: cusEnt.customer.org_id,
|
||||
|
||||
@@ -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";
|
||||
|
||||
export interface RelationPath {
|
||||
|
||||
@@ -34,7 +34,6 @@ export function generateArrayAggSQL({
|
||||
alias,
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
distinct = false,
|
||||
}: ArrayAggregationConfig): SQL {
|
||||
const tableAlias = alias || getTableAlias(table);
|
||||
@@ -81,34 +80,33 @@ export function generateRowSubquerySQL({
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SQL for many-to-many join through junction table
|
||||
* Example:
|
||||
* SELECT json_agg(o)
|
||||
* FROM member m
|
||||
* INNER JOIN organizations o ON o.id = m.organization_id
|
||||
* WHERE m.user_id = ${userId}
|
||||
*/
|
||||
export function generateJunctionJoinSQL({
|
||||
junctionTable,
|
||||
fromField,
|
||||
toField,
|
||||
fromTable,
|
||||
toTable,
|
||||
fromId,
|
||||
}: JunctionJoinConfig): SQL {
|
||||
const junctionAlias = getTableAlias(junctionTable);
|
||||
const toAlias = getTableAlias(toTable);
|
||||
const junctionTableName = getTableName(junctionTable);
|
||||
const toTableName = getTableName(toTable);
|
||||
// /**
|
||||
// * Generate SQL for many-to-many join through junction table
|
||||
// * Example:
|
||||
// * SELECT json_agg(o)
|
||||
// * FROM member m
|
||||
// * INNER JOIN organizations o ON o.id = m.organization_id
|
||||
// * WHERE m.user_id = ${userId}
|
||||
// */
|
||||
// export function generateJunctionJoinSQL({
|
||||
// junctionTable,
|
||||
// fromField,
|
||||
// toField,
|
||||
// toTable,
|
||||
// fromId,
|
||||
// }: JunctionJoinConfig): SQL {
|
||||
// const junctionAlias = getTableAlias(junctionTable);
|
||||
// const toAlias = getTableAlias(toTable);
|
||||
// const junctionTableName = getTableName(junctionTable);
|
||||
// const toTableName = getTableName(toTable);
|
||||
|
||||
return sql`
|
||||
FROM ${sql.identifier(junctionTableName)} ${sql.identifier(junctionAlias)}
|
||||
INNER JOIN ${sql.identifier(toTableName)} ${sql.identifier(toAlias)}
|
||||
ON ${sql.identifier(toAlias)}.${sql.identifier("id")} = ${sql.identifier(junctionAlias)}.${sql.identifier(toField)}
|
||||
WHERE ${sql.identifier(junctionAlias)}.${sql.identifier(fromField)} = ${fromId}
|
||||
`;
|
||||
}
|
||||
// return sql`
|
||||
// FROM ${sql.identifier(junctionTableName)} ${sql.identifier(junctionAlias)}
|
||||
// INNER JOIN ${sql.identifier(toTableName)} ${sql.identifier(toAlias)}
|
||||
// ON ${sql.identifier(toAlias)}.${sql.identifier("id")} = ${sql.identifier(junctionAlias)}.${sql.identifier(toField)}
|
||||
// WHERE ${sql.identifier(junctionAlias)}.${sql.identifier(fromField)} = ${fromId}
|
||||
// `;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Generate SQL for limiting results per parent using window functions
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { type SQL, sql } from "drizzle-orm";
|
||||
import type { PgTable } from "drizzle-orm/pg-core";
|
||||
import type { CTEConfig } from "../buildCte.js";
|
||||
import {
|
||||
buildRelationGraph,
|
||||
getTableName,
|
||||
parseJoinCondition,
|
||||
type RelationNode,
|
||||
} from "./relationGraph.js";
|
||||
import { buildRelationGraph, type RelationNode } from "./relationGraph.js";
|
||||
|
||||
/**
|
||||
* Build the optimized query using JOIN + GROUP BY strategy
|
||||
@@ -26,7 +21,6 @@ export function buildJoinGroupByQuery({
|
||||
}) => SQL | undefined;
|
||||
}): SQL {
|
||||
// Build relation graph
|
||||
const rootTable = getSourceTable(config.from);
|
||||
const graph = buildRelationGraph({
|
||||
config,
|
||||
relations,
|
||||
@@ -34,7 +28,7 @@ export function buildJoinGroupByQuery({
|
||||
});
|
||||
|
||||
// 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
|
||||
const mainQuery = buildMainQuery({ graph, config });
|
||||
@@ -181,10 +175,8 @@ function addNestedJoins({
|
||||
*/
|
||||
function buildAggregationCTEs({
|
||||
graph,
|
||||
rootTable,
|
||||
}: {
|
||||
graph: RelationNode;
|
||||
rootTable: PgTable;
|
||||
}): Array<{ name: string; definition: SQL }> {
|
||||
const ctes: Array<{ name: string; definition: SQL }> = [];
|
||||
|
||||
@@ -301,13 +293,3 @@ function buildAggregationCTEs({
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 { CTEConfig } from "../buildCte.js";
|
||||
import { CTEBuilder } from "../buildCte.js";
|
||||
@@ -80,12 +80,10 @@ export function parseJoinCondition({
|
||||
*/
|
||||
export function buildRelationGraph({
|
||||
config,
|
||||
parentTable,
|
||||
relations,
|
||||
extractJoinCondition,
|
||||
}: {
|
||||
config: CTEConfig;
|
||||
parentTable?: PgTable;
|
||||
relations: Record<string, any>;
|
||||
extractJoinCondition: (params: {
|
||||
parentTable: PgTable;
|
||||
@@ -128,7 +126,6 @@ export function buildRelationGraph({
|
||||
// Recursively build nested nodes
|
||||
const nestedNode = buildRelationGraph({
|
||||
config: nested,
|
||||
parentTable: table,
|
||||
relations,
|
||||
extractJoinCondition,
|
||||
});
|
||||
|
||||
@@ -36,11 +36,7 @@ export function inferMode(config: ModeDetectionConfig): CTEMode {
|
||||
|
||||
// 5. Plural field name? → array (entities, organizations, products)
|
||||
// Exclude words ending in 'ss' (address, process, etc.)
|
||||
if (
|
||||
config.fieldName &&
|
||||
config.fieldName.endsWith("s") &&
|
||||
!config.fieldName.endsWith("ss")
|
||||
) {
|
||||
if (config.fieldName?.endsWith("s") && !config.fieldName.endsWith("ss")) {
|
||||
return "array";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getTableColumns, sql, SQL } from "drizzle-orm";
|
||||
import { PgTable } from "drizzle-orm/pg-core";
|
||||
import { getTableColumns, type SQL, sql } from "drizzle-orm";
|
||||
import type { PgTable } from "drizzle-orm/pg-core";
|
||||
|
||||
export const buildConflictUpdateColumns = <T extends PgTable>(
|
||||
table: T,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClickHouseClient, createClient } from "@clickhouse/client";
|
||||
import { type ClickHouseClient, createClient } from "@clickhouse/client";
|
||||
|
||||
export const clickhouseClient: ClickHouseClient = createClient({
|
||||
url: process.env.CLICKHOUSE_URL!,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Writable } from "node:stream";
|
||||
import pino from "pino";
|
||||
import { Writable } from "stream";
|
||||
|
||||
// Custom log formatter for Bun compatibility
|
||||
const createDevLogStream = () => {
|
||||
@@ -53,7 +53,7 @@ const createDevLogStream = () => {
|
||||
};
|
||||
|
||||
return new Writable({
|
||||
write(chunk, encoding, callback) {
|
||||
write(chunk, _encoding, callback) {
|
||||
try {
|
||||
const log = JSON.parse(chunk.toString());
|
||||
const timestamp = new Date(log.time)
|
||||
|
||||
36
server/src/external/autumn/autumnCli.ts
vendored
36
server/src/external/autumn/autumnCli.ts
vendored
@@ -128,6 +128,40 @@ export class AutumnInt {
|
||||
|
||||
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(
|
||||
path: string,
|
||||
@@ -418,7 +452,7 @@ export class AutumnInt {
|
||||
// if (product.items && typeof product.items === "object") {
|
||||
// product.items = Object.values(product.items);
|
||||
// }
|
||||
const data = await this.post(`/products/${productId}`, product);
|
||||
const data = await this.patch(`/products/${productId}`, product);
|
||||
return data;
|
||||
},
|
||||
|
||||
|
||||
4
server/src/external/autumn/autumnUtils.ts
vendored
4
server/src/external/autumn/autumnUtils.ts
vendored
@@ -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 RecaseError from "@/utils/errorUtils.js";
|
||||
import { Autumn } from "autumn-js";
|
||||
|
||||
export enum FeatureId {
|
||||
Products = "products",
|
||||
|
||||
@@ -5,7 +5,7 @@ import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
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 headers = req.headers;
|
||||
@@ -49,7 +49,7 @@ autumnWebhookRouter.post(
|
||||
express.raw({ type: "application/json" }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const evt = await verifyAutumnWebhook(req, res);
|
||||
const evt = await verifyAutumnWebhook(req);
|
||||
console.log("Received webhook from autumn");
|
||||
const { type, data } = evt;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { ClickHouseClient, QueryParams } from "@clickhouse/client";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { ClickHouseClient, QueryParams } from "@clickhouse/client";
|
||||
import { clickhouseClient } from "../../db/initClickHouse.js";
|
||||
|
||||
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() {
|
||||
if (!this.client) {
|
||||
throw new Error("ClickHouse client not initialized");
|
||||
|
||||
31
server/src/external/infisical/initInfisical.ts
vendored
31
server/src/external/infisical/initInfisical.ts
vendored
@@ -1,34 +1,5 @@
|
||||
import { join } from "node:path";
|
||||
import { InfisicalSDK } from "@infisical/sdk";
|
||||
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)`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
import { loadLocalEnv } from "@/utils/envUtils.js";
|
||||
/**
|
||||
* Initialize Infisical and load secrets into process.env
|
||||
* This allows all existing code using process.env to work seamlessly
|
||||
|
||||
1
server/src/external/redis/initRedis.ts
vendored
1
server/src/external/redis/initRedis.ts
vendored
@@ -25,6 +25,7 @@ const redis = new Redis(regionalCacheUrl || process.env.CACHE_URL, {
|
||||
tls: caText ? { ca: caText } : undefined,
|
||||
});
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might uncomment this back in in the future
|
||||
redis.on("error", (error) => {
|
||||
// logger.error(`redis (cache) error: ${error.message}`);
|
||||
});
|
||||
|
||||
2
server/src/external/redis/loadCaCert.ts
vendored
2
server/src/external/redis/loadCaCert.ts
vendored
@@ -19,7 +19,7 @@ export const loadCaCert = async ({
|
||||
const ca = Bun.file(caPath || `/etc/secrets/${type}-cert.pem`);
|
||||
const caText = await ca.text();
|
||||
|
||||
return undefined;
|
||||
return caText;
|
||||
} catch (_error) {
|
||||
return;
|
||||
}
|
||||
|
||||
2
server/src/external/resend/resendUtils.ts
vendored
2
server/src/external/resend/resendUtils.ts
vendored
@@ -24,7 +24,7 @@ export const sendTextEmail = async ({
|
||||
|
||||
try {
|
||||
logger.info(`Sending email to ${to} with subject ${subject}`);
|
||||
const { data, error } = await resend.emails.send({
|
||||
const { error } = await resend.emails.send({
|
||||
from: from,
|
||||
to: to,
|
||||
subject: subject,
|
||||
|
||||
@@ -20,7 +20,6 @@ import { billingIntervalToStripe } from "../stripePriceUtils.js";
|
||||
import { priceToInArrearTiers } from "./createStripeInArrear.js";
|
||||
|
||||
export interface StripeMeteredPriceParams {
|
||||
db: DrizzleCli;
|
||||
stripeCli: Stripe;
|
||||
price: Price;
|
||||
entitlements: EntitlementWithFeature[];
|
||||
@@ -29,7 +28,6 @@ export interface StripeMeteredPriceParams {
|
||||
}
|
||||
|
||||
export const createStripeMeteredPrice = async ({
|
||||
db,
|
||||
stripeCli,
|
||||
price,
|
||||
entitlements,
|
||||
@@ -225,7 +223,6 @@ export const createStripeArrearProrated = async ({
|
||||
// CREATE PLACEHOLDER PRICE FOR INARREAR PRORATED PRICING
|
||||
if (billingType === BillingType.InArrearProrated) {
|
||||
const placeholderPrice = await createStripeMeteredPrice({
|
||||
db,
|
||||
stripeCli,
|
||||
price,
|
||||
entitlements,
|
||||
|
||||
@@ -178,7 +178,6 @@ export const createStripePriceIFNotExist = async ({
|
||||
} else if (!config.stripe_placeholder_price_id) {
|
||||
logger.info(`Creating stripe placeholder price`);
|
||||
const placeholderPrice = await createStripeMeteredPrice({
|
||||
db,
|
||||
stripeCli,
|
||||
price,
|
||||
entitlements,
|
||||
|
||||
@@ -201,8 +201,6 @@ export const handleStripeWebhookEvent = async ({
|
||||
|
||||
case "invoice.updated":
|
||||
await handleInvoiceUpdated({
|
||||
stripeCli,
|
||||
env,
|
||||
event,
|
||||
req: ctx as unknown as ExtendedRequest,
|
||||
});
|
||||
@@ -251,7 +249,6 @@ export const handleStripeWebhookEvent = async ({
|
||||
org,
|
||||
env,
|
||||
schedule: canceledSchedule,
|
||||
logger,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
const classifyStripePaymentMethod = (paymentMethod: Stripe.PaymentMethod) => {
|
||||
let cardPaymentMethods = [];
|
||||
};
|
||||
|
||||
// Note: us_bank_account -> ACH
|
||||
// customer_balance -> Bank Account
|
||||
@@ -1,5 +1,4 @@
|
||||
import { UsagePriceConfig } from "@autumn/shared";
|
||||
import { Price } from "@autumn/shared";
|
||||
import type { Price, UsagePriceConfig } from "@autumn/shared";
|
||||
|
||||
export const priceToInArrearProrated = ({
|
||||
price,
|
||||
@@ -11,9 +10,9 @@ export const priceToInArrearProrated = ({
|
||||
existingUsage: number;
|
||||
}) => {
|
||||
const config = price.config as UsagePriceConfig;
|
||||
let quantity = existingUsage || 0;
|
||||
const quantity = existingUsage || 0;
|
||||
|
||||
if (quantity == 0 && isCheckout) {
|
||||
if (quantity === 0 && isCheckout) {
|
||||
return {
|
||||
price: config.stripe_placeholder_price_id,
|
||||
};
|
||||
|
||||
@@ -124,7 +124,6 @@ export const priceToStripeItem = ({
|
||||
price,
|
||||
options,
|
||||
isCheckout,
|
||||
relatedEnt,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,8 @@ export const priceToOneOffAndTiered = ({
|
||||
stripeProductId: string;
|
||||
}) => {
|
||||
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();
|
||||
// let overage = quantity * config.billing_units! - relatedEnt.allowance!;
|
||||
|
||||
// if (overage <= 0) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
const amount = getPriceForOverage(price, overage);
|
||||
if (!config.stripe_product_id) {
|
||||
@@ -53,12 +48,10 @@ export const priceToOneOffAndTiered = ({
|
||||
|
||||
export const priceToUsageInAdvance = ({
|
||||
price,
|
||||
relatedEnt,
|
||||
options,
|
||||
isCheckout,
|
||||
}: {
|
||||
price: Price;
|
||||
relatedEnt: EntitlementWithFeature;
|
||||
options: FeatureOptions | undefined | null;
|
||||
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
|
||||
const adjustable = notNullish(options?.adjustable_quantity)
|
||||
? options!.adjustable_quantity
|
||||
: nullish(optionsQuantity)
|
||||
? true
|
||||
: false;
|
||||
: nullish(optionsQuantity);
|
||||
|
||||
if (optionsQuantity === 0 && isCheckout) {
|
||||
// 1. If quantity is 0 and is checkout, skip over line item
|
||||
@@ -81,12 +72,6 @@ export const priceToUsageInAdvance = ({
|
||||
finalQuantity = 1;
|
||||
}
|
||||
|
||||
// Divide final quantity by billing units...?
|
||||
|
||||
// let minimum = new Decimal(relatedEnt.allowance!)
|
||||
// .div(config.billing_units || 1)
|
||||
// .toNumber();
|
||||
|
||||
const adjustableQuantity =
|
||||
isCheckout && adjustable
|
||||
? {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Stripe } from "stripe";
|
||||
import type { Stripe } from "stripe";
|
||||
|
||||
export const deleteCouponFromSub = async ({
|
||||
stripeCli,
|
||||
@@ -12,23 +12,14 @@ export const deleteCouponFromSub = async ({
|
||||
logger: any;
|
||||
}) => {
|
||||
try {
|
||||
let stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
|
||||
|
||||
let newDiscounts = stripeSub.discounts
|
||||
?.filter((d: any) => d !== discountId)
|
||||
.map((d: any) => ({
|
||||
discount: d,
|
||||
}));
|
||||
const stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
|
||||
|
||||
if (stripeSub.discounts.some((d: any) => d === discountId)) {
|
||||
await stripeCli.subscriptions.deleteDiscount(stripeSubId);
|
||||
// console.log("DELETED DISCOUNT FROM SUB", stripeSubId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// if (!error.message.includes("no active discount for subscription")) {
|
||||
logger.error(`Failed to delete discount from subscription ${stripeSubId}`);
|
||||
logger.error(error.message);
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,7 +37,7 @@ export const deleteCouponFromCus = async ({
|
||||
logger: any;
|
||||
}) => {
|
||||
try {
|
||||
let stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
|
||||
const stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
|
||||
if (stripeSub.discounts.some((d: any) => d === discountId)) {
|
||||
await stripeCli.subscriptions.deleteDiscount(stripeSubId);
|
||||
}
|
||||
@@ -56,7 +47,7 @@ export const deleteCouponFromCus = async ({
|
||||
}
|
||||
|
||||
try {
|
||||
let stripeCus = (await stripeCli.customers.retrieve(
|
||||
const stripeCus = (await stripeCli.customers.retrieve(
|
||||
stripeCusId,
|
||||
)) as Stripe.Customer;
|
||||
if (stripeCus.discount?.id === discountId) {
|
||||
|
||||
4
server/src/external/stripe/stripeCusUtils.ts
vendored
4
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -22,7 +22,7 @@ export const getStripeCus = async ({
|
||||
try {
|
||||
const stripeCus = await stripeCli.customers.retrieve(stripeId);
|
||||
return stripeCus as Stripe.Customer;
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -57,7 +57,7 @@ export const createStripeCusIfNotExists = async ({
|
||||
} else {
|
||||
createNew = true;
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
createNew = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const stripeErrToCusMsg = (error: any) => {
|
||||
let code = error.code;
|
||||
let msg = error.message;
|
||||
};
|
||||
@@ -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,
|
||||
// });
|
||||
}
|
||||
};
|
||||
18
server/src/external/stripe/stripeMeterUtils.ts
vendored
18
server/src/external/stripe/stripeMeterUtils.ts
vendored
@@ -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 { BillingType, Price, UsagePriceConfig } from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const submitUsageToStripe = async ({
|
||||
price,
|
||||
@@ -21,10 +25,10 @@ export const submitUsageToStripe = async ({
|
||||
feature: Feature;
|
||||
logger: any;
|
||||
}) => {
|
||||
let config = price.config as UsagePriceConfig;
|
||||
let billingType = getBillingType(config);
|
||||
const config = price.config as UsagePriceConfig;
|
||||
const billingType = getBillingType(config);
|
||||
|
||||
if (billingType != BillingType.UsageInArrear) {
|
||||
if (billingType !== BillingType.UsageInArrear) {
|
||||
logger.warn(
|
||||
`Price ${price.id} is not usage in arrear type, can't send usage`,
|
||||
);
|
||||
|
||||
@@ -6,11 +6,7 @@ export const checkKeyValid = async (apiKey: string) => {
|
||||
const stripe = new Stripe(apiKey);
|
||||
|
||||
// Call customers.list
|
||||
const customers = await stripe.customers.list();
|
||||
|
||||
// const account = await stripe.accounts.retrieve();
|
||||
// console.log("Account", account);
|
||||
// return account;
|
||||
await stripe.customers.list();
|
||||
};
|
||||
|
||||
export const createWebhookEndpoint = async (
|
||||
|
||||
@@ -52,7 +52,7 @@ export const deleteStripeProduct = async (
|
||||
|
||||
try {
|
||||
await stripe.products.del(product.processor.id);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
throw new RecaseError({
|
||||
message: "Failed to delete stripe product",
|
||||
code: ErrCode.DeleteStripeProductFailed,
|
||||
@@ -136,7 +136,7 @@ export const deleteAllStripeProducts = async ({
|
||||
batch.map(async (p) => {
|
||||
try {
|
||||
await stripeCli.products.del(p.id);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
await stripeCli.products.update(p.id, {
|
||||
active: false,
|
||||
});
|
||||
|
||||
@@ -232,7 +232,6 @@ export const getStripeSubItems2 = async ({
|
||||
cusProducts,
|
||||
customer,
|
||||
internalEntityId,
|
||||
apiVersion,
|
||||
products,
|
||||
} = attachParams;
|
||||
|
||||
@@ -320,7 +319,7 @@ export const getStripeSubItems2 = async ({
|
||||
|
||||
export const sanitizeSubItems = (subItems: any[]) => {
|
||||
return subItems.map((si) => {
|
||||
const { autumnPrice, ...rest } = si;
|
||||
const { autumnPrice: _autumnPrice, ...rest } = si;
|
||||
return {
|
||||
...rest,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import {
|
||||
BillingInterval,
|
||||
type BillingInterval,
|
||||
BillingType,
|
||||
intervalsDifferent,
|
||||
Organization,
|
||||
UsagePriceConfig,
|
||||
type Organization,
|
||||
type Price,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { Price } from "@autumn/shared";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import { billingIntervalToStripe } from "../../stripePriceUtils.js";
|
||||
|
||||
export const getArrearItems = ({
|
||||
@@ -21,9 +20,9 @@ export const getArrearItems = ({
|
||||
intervalCount: number;
|
||||
org: Organization;
|
||||
}) => {
|
||||
let placeholderItems: any[] = [];
|
||||
const placeholderItems: any[] = [];
|
||||
for (const price of prices) {
|
||||
let billingType = getBillingType(price.config!);
|
||||
const billingType = getBillingType(price.config!);
|
||||
if (
|
||||
intervalsDifferent({
|
||||
intervalA: {
|
||||
@@ -36,8 +35,8 @@ export const getArrearItems = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
if (billingType == BillingType.UsageInArrear) {
|
||||
let config = price.config! as UsagePriceConfig;
|
||||
if (billingType === BillingType.UsageInArrear) {
|
||||
const config = price.config! as UsagePriceConfig;
|
||||
placeholderItems.push({
|
||||
price_data: {
|
||||
product: config.stripe_product_id!,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import Stripe from "stripe";
|
||||
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 = ({
|
||||
tiers,
|
||||
@@ -40,12 +39,12 @@ export const getSubItemAmount = ({
|
||||
}: {
|
||||
subItem: Stripe.SubscriptionItem;
|
||||
}) => {
|
||||
let price = subItem.price;
|
||||
const price = subItem.price;
|
||||
|
||||
const quantity = subItem.quantity || 0;
|
||||
|
||||
if (price.billing_scheme == "tiered") {
|
||||
let tieredAmount = calculateTieredAmount({
|
||||
if (price.billing_scheme === "tiered") {
|
||||
const tieredAmount = calculateTieredAmount({
|
||||
tiers: price.tiers!,
|
||||
quantity,
|
||||
});
|
||||
@@ -53,7 +52,7 @@ export const getSubItemAmount = ({
|
||||
return tieredAmount;
|
||||
}
|
||||
|
||||
if (price.billing_scheme == "per_unit") {
|
||||
if (price.billing_scheme === "per_unit") {
|
||||
if (price.unit_amount_decimal) {
|
||||
return new Decimal(price.unit_amount_decimal).mul(quantity).toNumber();
|
||||
} else {
|
||||
|
||||
@@ -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;
|
||||
// });
|
||||
// };
|
||||
@@ -69,7 +69,6 @@ export const createProrationInvoice = async ({
|
||||
}) => {
|
||||
const { stripeCli, customer, paymentMethod } = attachParams;
|
||||
|
||||
const proratedItems = [];
|
||||
// How to retrieve upcoming invoice items?
|
||||
const items = await stripeCli.invoiceItems.list({
|
||||
customer: customer.processor.id,
|
||||
|
||||
1
server/src/external/stripe/stripeWebhooks.ts
vendored
1
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -20,7 +20,6 @@ stripeWebhookRouter.post(
|
||||
let event: Stripe.Event;
|
||||
|
||||
const { orgId, env } = request.params;
|
||||
const { db } = request;
|
||||
|
||||
let org: Organization;
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ export const handleCheckoutSessionCompleted = async ({
|
||||
if (attachParams.setupPayment) {
|
||||
await handleSetupCheckout({
|
||||
req,
|
||||
db,
|
||||
attachParams,
|
||||
});
|
||||
return;
|
||||
@@ -107,18 +106,15 @@ export const handleCheckoutSessionCompleted = async ({
|
||||
db,
|
||||
subscription: checkoutSub,
|
||||
attachParams,
|
||||
logger,
|
||||
});
|
||||
|
||||
// Create other subscriptions
|
||||
const { invoiceIds } = await handleRemainingSets({
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
checkoutSession,
|
||||
attachParams,
|
||||
checkoutSub,
|
||||
logger,
|
||||
});
|
||||
|
||||
const anchorToUnix = checkoutSub
|
||||
@@ -147,7 +143,7 @@ export const handleCheckoutSessionCompleted = async ({
|
||||
product,
|
||||
productOptions.entity_id || undefined,
|
||||
),
|
||||
subscriptionIds: checkoutSub ? [checkoutSub?.id!] : undefined,
|
||||
subscriptionIds: checkoutSub ? [checkoutSub.id] : undefined,
|
||||
anchorToUnix,
|
||||
scenario: AttachScenario.New,
|
||||
logger,
|
||||
@@ -160,7 +156,7 @@ export const handleCheckoutSessionCompleted = async ({
|
||||
await createFullCusProduct({
|
||||
db,
|
||||
attachParams: attachToInsertParams(attachParams, product),
|
||||
subscriptionIds: checkoutSub ? [checkoutSub?.id!] : undefined,
|
||||
subscriptionIds: checkoutSub ? [checkoutSub.id] : undefined,
|
||||
anchorToUnix,
|
||||
scenario: AttachScenario.New,
|
||||
logger,
|
||||
|
||||
@@ -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 {
|
||||
formatPrice,
|
||||
getBillingType,
|
||||
getPriceEntitlement,
|
||||
priceIsOneOffAndTiered,
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
import { BillingType, UsagePriceConfig } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import { findStripeItemForPrice } from "../../stripeSubUtils/stripeSubItemUtils.js";
|
||||
|
||||
export const getOptionsFromCheckoutSession = async ({
|
||||
@@ -18,7 +17,7 @@ export const getOptionsFromCheckoutSession = async ({
|
||||
}) => {
|
||||
const usageInAdvanceExists = attachParams.prices.some(
|
||||
(price) =>
|
||||
getBillingType(price.config as UsagePriceConfig) ==
|
||||
getBillingType(price.config as UsagePriceConfig) ===
|
||||
BillingType.UsageInAdvance,
|
||||
);
|
||||
|
||||
@@ -31,9 +30,9 @@ export const getOptionsFromCheckoutSession = async ({
|
||||
|
||||
// Should still work with old method?
|
||||
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({
|
||||
price,
|
||||
@@ -43,7 +42,7 @@ export const getOptionsFromCheckoutSession = async ({
|
||||
let quantity = 0;
|
||||
|
||||
if (lineItem) {
|
||||
let relatedEnt = getPriceEntitlement(price, ents);
|
||||
const relatedEnt = getPriceEntitlement(price, ents);
|
||||
|
||||
if (priceIsOneOffAndTiered(price, relatedEnt)) {
|
||||
// quantity = lineItem.quantity || 0;
|
||||
@@ -54,10 +53,10 @@ export const getOptionsFromCheckoutSession = async ({
|
||||
}
|
||||
|
||||
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({
|
||||
feature_id: config.feature_id,
|
||||
internal_feature_id: config.internal_feature_id,
|
||||
|
||||
@@ -16,13 +16,11 @@ export const handleCheckoutSub = async ({
|
||||
db,
|
||||
subscription,
|
||||
attachParams,
|
||||
logger,
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
db: DrizzleCli;
|
||||
subscription: Stripe.Subscription | null;
|
||||
attachParams: AttachParams;
|
||||
logger: any;
|
||||
}) => {
|
||||
const { org } = attachParams;
|
||||
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { ApiVersion, isUsagePrice, type Organization } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js";
|
||||
|
||||
export const handleRemainingSets = async ({
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
checkoutSession,
|
||||
attachParams,
|
||||
checkoutSub,
|
||||
logger,
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
checkoutSession: Stripe.Checkout.Session;
|
||||
attachParams: AttachParams;
|
||||
checkoutSub: Stripe.Subscription | null;
|
||||
logger: any;
|
||||
}) => {
|
||||
const itemSets = attachParams.itemSets;
|
||||
const remainingSets = itemSets ? itemSets.slice(1) : [];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AttachBranch } from "@autumn/shared";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js";
|
||||
import { handleOneOffFunction } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
|
||||
@@ -11,11 +10,9 @@ import { getCusPaymentMethod } from "../../stripeCusUtils.js";
|
||||
|
||||
export const handleSetupCheckout = async ({
|
||||
req,
|
||||
db,
|
||||
attachParams,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
db: DrizzleCli;
|
||||
attachParams: AttachParams;
|
||||
}) => {
|
||||
const logger = req.logger;
|
||||
|
||||
@@ -15,7 +15,6 @@ export const handleContUsePrices = async ({
|
||||
db,
|
||||
cusEnts,
|
||||
cusPrice,
|
||||
stripeCli,
|
||||
invoice,
|
||||
usageSub,
|
||||
logger,
|
||||
@@ -24,8 +23,6 @@ export const handleContUsePrices = async ({
|
||||
db: DrizzleCli;
|
||||
cusEnts: FullCustomerEntitlement[];
|
||||
cusPrice: FullCustomerPrice;
|
||||
stripeCli: Stripe;
|
||||
|
||||
invoice: Stripe.Invoice;
|
||||
usageSub: Stripe.Subscription;
|
||||
logger: any;
|
||||
|
||||
@@ -2,24 +2,18 @@ import {
|
||||
type AppEnv,
|
||||
BillingType,
|
||||
CusProductStatus,
|
||||
type Customer,
|
||||
type FullCusProduct,
|
||||
type FullCustomerEntitlement,
|
||||
type FullCustomerPrice,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.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 { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
|
||||
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.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 {
|
||||
getFullStripeInvoice,
|
||||
@@ -31,139 +25,6 @@ import { handleContUsePrices } from "./handleContUsePrices.js";
|
||||
import { handlePrepaidPrices } from "./handlePrepaidPrices.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 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)
|
||||
@@ -175,7 +36,6 @@ export const sendUsageAndReset = async ({
|
||||
org,
|
||||
env,
|
||||
invoice,
|
||||
stripeSubs,
|
||||
logger,
|
||||
submitUsage = true,
|
||||
resetBalance = true,
|
||||
@@ -185,7 +45,6 @@ export const sendUsageAndReset = async ({
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
invoice: Stripe.Invoice;
|
||||
stripeSubs: Stripe.Subscription[];
|
||||
logger: any;
|
||||
submitUsage?: boolean;
|
||||
resetBalance?: boolean;
|
||||
@@ -249,7 +108,6 @@ export const sendUsageAndReset = async ({
|
||||
if (billingType === BillingType.InArrearProrated) {
|
||||
const handledContUse = await handleContUsePrices({
|
||||
db,
|
||||
stripeCli,
|
||||
cusEnts,
|
||||
cusPrice,
|
||||
invoice,
|
||||
@@ -264,11 +122,9 @@ export const sendUsageAndReset = async ({
|
||||
if (billingType === BillingType.UsageInAdvance) {
|
||||
const handledPrepaid = await handlePrepaidPrices({
|
||||
db,
|
||||
stripeCli,
|
||||
cusPrice,
|
||||
cusProduct: activeProduct,
|
||||
usageSub: usageBasedSub,
|
||||
customer,
|
||||
invoice,
|
||||
logger,
|
||||
resetBalance,
|
||||
@@ -333,7 +189,7 @@ export const handleInvoiceCreated = async ({
|
||||
(p) => p.internal_entity_id,
|
||||
)?.internal_entity_id;
|
||||
|
||||
const features = await FeatureService.list({
|
||||
await FeatureService.list({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
@@ -389,7 +245,6 @@ export const handleInvoiceCreated = async ({
|
||||
activeProduct,
|
||||
org,
|
||||
env,
|
||||
stripeSubs,
|
||||
invoice,
|
||||
logger,
|
||||
submitUsage: true, // Always submit usage during invoice.created
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
type Customer,
|
||||
EntInterval,
|
||||
type FeatureOptions,
|
||||
type FullCusProduct,
|
||||
@@ -20,21 +19,17 @@ import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
|
||||
|
||||
export const handlePrepaidPrices = async ({
|
||||
db,
|
||||
stripeCli,
|
||||
cusProduct,
|
||||
cusPrice,
|
||||
usageSub,
|
||||
customer,
|
||||
invoice,
|
||||
logger,
|
||||
resetBalance = true,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
stripeCli: Stripe;
|
||||
cusProduct: FullCusProduct;
|
||||
cusPrice: FullCustomerPrice;
|
||||
usageSub: Stripe.Subscription;
|
||||
customer: Customer;
|
||||
invoice: Stripe.Invoice;
|
||||
logger: any;
|
||||
resetBalance?: boolean;
|
||||
@@ -60,7 +55,7 @@ export const handlePrepaidPrices = async ({
|
||||
|
||||
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 billingUnits = config.billing_units || 1;
|
||||
const newAllowance =
|
||||
@@ -106,7 +101,8 @@ export const handlePrepaidPrices = async ({
|
||||
});
|
||||
|
||||
if (ent.interval === EntInterval.Lifetime) {
|
||||
const difference = options?.quantity! - options?.upcoming_quantity!;
|
||||
const difference =
|
||||
(options?.quantity ?? 0) - (options?.upcoming_quantity ?? 0);
|
||||
await CusEntService.decrement({
|
||||
db,
|
||||
id: cusEnt.id,
|
||||
|
||||
@@ -28,12 +28,10 @@ import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js";
|
||||
const handleOneOffInvoicePaid = async ({
|
||||
db,
|
||||
stripeInvoice,
|
||||
logger,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
stripeInvoice: Stripe.Invoice;
|
||||
event: Stripe.Event;
|
||||
logger: any;
|
||||
}) => {
|
||||
// Search for invoice
|
||||
const invoice = await InvoiceService.getByStripeId({
|
||||
@@ -285,7 +283,6 @@ export const handleInvoicePaid = async ({
|
||||
db,
|
||||
stripeInvoice: invoice,
|
||||
event,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,29 +34,28 @@ export const handleInvoicePaidDiscount = async ({
|
||||
logger: any;
|
||||
}) => {
|
||||
// Handle coupon
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
if (expandedInvoice.discounts.length === 0) {
|
||||
return;
|
||||
}
|
||||
const stripeCli = createStripeCli({ org, env, legacyVersion: true });
|
||||
if (expandedInvoice.discounts.length === 0) return;
|
||||
|
||||
const stripeCus = await stripeCli.customers.retrieve(
|
||||
expandedInvoice.customer as string,
|
||||
);
|
||||
|
||||
const legacyInvoice = await stripeCli.invoices.retrieve(expandedInvoice.id, {
|
||||
expand: ["total_discount_amounts", "discounts.coupon"],
|
||||
});
|
||||
|
||||
try {
|
||||
const totalDiscountAmounts = expandedInvoice.total_discount_amounts;
|
||||
|
||||
// Log coupon information for debugging
|
||||
for (const discount of expandedInvoice.discounts) {
|
||||
if (typeof discount === "string") {
|
||||
continue;
|
||||
}
|
||||
for (const discount of legacyInvoice.discounts) {
|
||||
if (typeof discount === "string" || !("coupon" in discount)) 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;
|
||||
}
|
||||
|
||||
const rollSuffixIndex = curCoupon.id.indexOf("_roll_");
|
||||
const couponId =
|
||||
@@ -76,9 +75,7 @@ export const handleInvoicePaidDiscount = async ({
|
||||
(autumnReward.type === RewardType.InvoiceCredits ||
|
||||
autumnReward.type === RewardType.FreeProduct);
|
||||
|
||||
if (!shouldRollover) {
|
||||
continue;
|
||||
}
|
||||
if (!shouldRollover) continue;
|
||||
|
||||
// Get ID of coupon
|
||||
const originalCoupon = await stripeCli.coupons.retrieve(couponId, {
|
||||
@@ -150,13 +147,7 @@ export const handleInvoicePaidDiscount = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const legacyStripeCli = createStripeCli({
|
||||
org,
|
||||
env,
|
||||
legacyVersion: true,
|
||||
});
|
||||
|
||||
await legacyStripeCli.rawRequest(
|
||||
await stripeCli.rawRequest(
|
||||
"POST",
|
||||
`/v1/customers/${expandedInvoice.customer}`,
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
type Invoice,
|
||||
InvoiceStatus,
|
||||
stripeToAtmnAmount,
|
||||
@@ -13,6 +12,7 @@ import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
|
||||
import { MetadataService } from "@/internal/metadata/MetadataService.js";
|
||||
import { getFullStripeInvoice, invoiceToSubId } from "../stripeInvoiceUtils.js";
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: Might be useful in the future
|
||||
const handleInvoiceCheckoutVoided = async ({
|
||||
db,
|
||||
stripeCli,
|
||||
@@ -81,14 +81,10 @@ const handleInvoiceCheckoutVoided = async ({
|
||||
};
|
||||
|
||||
export const handleInvoiceUpdated = async ({
|
||||
env,
|
||||
event,
|
||||
stripeCli,
|
||||
req,
|
||||
}: {
|
||||
env: AppEnv;
|
||||
event: Stripe.Event;
|
||||
stripeCli: Stripe;
|
||||
req: any;
|
||||
}) => {
|
||||
const invoiceObject = event.data.object as Stripe.Invoice;
|
||||
@@ -97,13 +93,6 @@ export const handleInvoiceUpdated = async ({
|
||||
stripeId: invoiceObject.id!,
|
||||
});
|
||||
|
||||
// const invoice = await getFullStripeInvoice({
|
||||
// stripeCli,
|
||||
// stripeId: invoiceObject.id!,
|
||||
// });
|
||||
|
||||
const prevAttributes = event.data.previous_attributes as any;
|
||||
|
||||
const updates: Partial<Invoice> = {};
|
||||
|
||||
if (invoiceObject.status === "void") {
|
||||
|
||||
@@ -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 {
|
||||
getFullStripeSub,
|
||||
subIsPrematurelyCanceled,
|
||||
} from "../stripeSubUtils.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { handleCusProductDeleted } from "./handleSubDeleted/handleCusProductDeleted.js";
|
||||
|
||||
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
|
||||
let prematurelyCanceled = subIsPrematurelyCanceled(subscription);
|
||||
const prematurelyCanceled = subIsPrematurelyCanceled(subscription);
|
||||
|
||||
// const batchUpdate = [];
|
||||
for (const cusProduct of activeCusProducts) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { AppEnv, Organization } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
|
||||
export const handleSubscriptionScheduleCanceled = async ({
|
||||
@@ -9,13 +8,11 @@ export const handleSubscriptionScheduleCanceled = async ({
|
||||
schedule,
|
||||
env,
|
||||
org,
|
||||
logger,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
schedule: Stripe.SubscriptionSchedule;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
logger: any;
|
||||
}) => {
|
||||
const cusProductsOnSchedule = await CusProductService.getByScheduleId({
|
||||
db,
|
||||
@@ -25,59 +22,4 @@ export const handleSubscriptionScheduleCanceled = async ({
|
||||
});
|
||||
|
||||
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
|
||||
// );
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -102,7 +102,7 @@ export const handleSchedulePhaseCompleted = async ({
|
||||
|
||||
// Maybe activate default product?
|
||||
await deleteCachedApiCustomer({
|
||||
customerId: cusProduct.internal_customer_id || "",
|
||||
customerId: cusProduct.customer?.id || "",
|
||||
orgId: org.id,
|
||||
env,
|
||||
source: "handleSchedulePhaseCompleted",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AttachScenario, type FullCusProduct } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
|
||||
import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.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 ({
|
||||
req,
|
||||
prevAttributes,
|
||||
@@ -131,5 +110,5 @@ export const handleSubRenewed = async ({
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (_error) {}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import {
|
||||
AttachParams,
|
||||
InsertCusProductParams,
|
||||
} from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import {
|
||||
cusProductToEnts,
|
||||
cusProductToPrices,
|
||||
cusProductToProduct,
|
||||
type Entity,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
} from "@autumn/shared";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { Entity, FullCusProduct, FullCustomer } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import type Stripe from "stripe";
|
||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
|
||||
export const webhookToAttachParams = ({
|
||||
req,
|
||||
|
||||
1
server/src/external/supabase/storageUtils.ts
vendored
1
server/src/external/supabase/storageUtils.ts
vendored
@@ -1,4 +1,3 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { createSupabaseClient } from "../supabaseUtils.js";
|
||||
|
||||
export const readFile = async ({
|
||||
|
||||
34
server/src/external/supabaseUtils.ts
vendored
34
server/src/external/supabaseUtils.ts
vendored
@@ -1,38 +1,4 @@
|
||||
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 = () => {
|
||||
try {
|
||||
|
||||
2
server/src/external/svix/svixHelpers.ts
vendored
2
server/src/external/svix/svixHelpers.ts
vendored
@@ -65,7 +65,9 @@ export const sendSvixEvent = safeSvix({
|
||||
|
||||
export const sendCustomSvixEvent = safeSvix({
|
||||
fn: async ({
|
||||
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be useful in the future
|
||||
org,
|
||||
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be useful in the future
|
||||
env,
|
||||
eventType,
|
||||
data,
|
||||
|
||||
@@ -137,6 +137,7 @@ export const listVercelPlansForOrg = async ({
|
||||
org,
|
||||
env,
|
||||
metadata,
|
||||
// biome-ignore lint/correctness/noUnusedFunctionParameters: Might be useful in the future
|
||||
canCancel = true,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
|
||||
@@ -8,7 +8,7 @@ export const handleGetInstallation = createRoute({
|
||||
handler: async (c) => {
|
||||
const ctx = c.get("ctx");
|
||||
const { integrationConfigurationId } = c.req.param();
|
||||
const { db, org, logger } = ctx;
|
||||
const { db, org } = ctx;
|
||||
|
||||
const customer = await CusService.getByVercelId({
|
||||
db,
|
||||
|
||||
@@ -37,13 +37,7 @@ export const handleMarketplaceInvoicePaid = async ({
|
||||
invoiceDate: string;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
installationId,
|
||||
invoiceId,
|
||||
externalInvoiceId,
|
||||
invoiceTotal,
|
||||
invoiceDate,
|
||||
} = payload;
|
||||
const { installationId, invoiceId, externalInvoiceId, invoiceDate } = payload;
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
|
||||
@@ -160,7 +154,6 @@ export const handleMarketplaceInvoicePaid = async ({
|
||||
}
|
||||
|
||||
if (isRenewal) {
|
||||
// Call sendUsageAndReset which handles all balance resets
|
||||
const activeProduct = existingCusProducts[0];
|
||||
|
||||
await sendUsageAndReset({
|
||||
@@ -169,7 +162,6 @@ export const handleMarketplaceInvoicePaid = async ({
|
||||
org,
|
||||
env,
|
||||
invoice,
|
||||
stripeSubs: [subscription],
|
||||
logger,
|
||||
submitUsage: false, // Usage already submitted in invoice.created
|
||||
resetBalance: true, // Payment confirmed - now safe to reset balance
|
||||
|
||||
@@ -26,13 +26,7 @@ export const handleMarketplaceInvoiceNotPaid = async ({
|
||||
invoiceDate: string;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
installationId,
|
||||
invoiceId,
|
||||
externalInvoiceId,
|
||||
invoiceTotal,
|
||||
invoiceDate,
|
||||
} = payload;
|
||||
const { installationId, invoiceId, externalInvoiceId, invoiceDate } = payload;
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export const vercelOidcAuthMiddleware = async (c: any, next: any) => {
|
||||
let token: string;
|
||||
try {
|
||||
token = getAuthorizationToken(authHeader);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return c.json(
|
||||
{ error: "Unauthorized", code: "invalid_auth_header_format" },
|
||||
401,
|
||||
@@ -160,7 +160,7 @@ export const vercelOidcAuthMiddleware = async (c: any, next: any) => {
|
||||
} catch (error: any) {
|
||||
return c.json(
|
||||
{
|
||||
error: "Unauthorized" + error.message,
|
||||
error: `Unauthorized: ${error.message}`,
|
||||
code:
|
||||
error instanceof AuthError
|
||||
? "auth_failed"
|
||||
|
||||
@@ -265,7 +265,7 @@ export const getVercelAttachBody = ({
|
||||
const attachParams: AttachParams = {
|
||||
stripeCli,
|
||||
stripeCus: stripeCustomer,
|
||||
now: Date.now(),
|
||||
now: now ?? Date.now(),
|
||||
paymentMethod: customPaymentMethod, // Pass Vercel custom payment method
|
||||
org,
|
||||
customer,
|
||||
|
||||
@@ -47,7 +47,7 @@ export const logVercelWebhook = ({
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
logVercelWebhook({ logger, org, event: body });
|
||||
|
||||
97
server/src/external/webhooks/clerkWebhooks.ts
vendored
97
server/src/external/webhooks/clerkWebhooks.ts
vendored
@@ -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",
|
||||
// });
|
||||
// };
|
||||
@@ -27,12 +27,10 @@ const parseCustomerIdFromUrl = ({
|
||||
const logResponse = async ({
|
||||
ctx,
|
||||
c,
|
||||
method,
|
||||
skipUrls,
|
||||
}: {
|
||||
ctx: any;
|
||||
c: Context<HonoEnv>;
|
||||
method: string;
|
||||
skipUrls: string[];
|
||||
}) => {
|
||||
try {
|
||||
@@ -116,7 +114,7 @@ export const analyticsMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
||||
|
||||
// Log response asynchronously without blocking (runs after response is sent)
|
||||
Promise.resolve()
|
||||
.then(() => logResponse({ ctx, c, method, skipUrls }))
|
||||
.then(() => logResponse({ ctx, c, skipUrls }))
|
||||
.catch((error) => {
|
||||
console.error("Failed to log response to logtail");
|
||||
console.error(error);
|
||||
|
||||
@@ -31,7 +31,6 @@ export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
||||
};
|
||||
|
||||
const method = c.req.method;
|
||||
const path = c.req.path;
|
||||
|
||||
let body = null;
|
||||
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
||||
|
||||
@@ -189,7 +189,7 @@ const init = async () => {
|
||||
app.use("/webhooks", webhooksRouter);
|
||||
|
||||
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}`, {
|
||||
context: {
|
||||
body: req.body,
|
||||
@@ -228,7 +228,7 @@ if (process.env.NODE_ENV === "development") {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
cluster.on("exit", (worker, code, signal) => {
|
||||
cluster.on("exit", (worker, _code, _signal) => {
|
||||
logger.error(`WORKER DIED: ${worker.process.pid}`);
|
||||
cluster.fork();
|
||||
});
|
||||
|
||||
@@ -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 { and, desc, eq, gt, gte, ilike, inArray, lt, or } from "drizzle-orm";
|
||||
import { Router } from "express";
|
||||
import { handleFrontendReqError } from "@/utils/errorUtils.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
|
||||
export const adminRouter: Router = Router();
|
||||
|
||||
@@ -12,7 +12,7 @@ adminRouter.get("/users", async (req: any, res: any) => {
|
||||
try {
|
||||
const { db } = req as ExtendedRequest;
|
||||
|
||||
let { sortKey, search, after, before } = req.query;
|
||||
let { search, after, before } = req.query;
|
||||
|
||||
if (after) {
|
||||
after = {
|
||||
@@ -136,9 +136,9 @@ adminRouter.get("/orgs", async (req: any, res: any) => {
|
||||
.orderBy(desc(organizations.createdAt), desc(organizations.id))
|
||||
.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()
|
||||
.from(member)
|
||||
.leftJoin(user, eq(member.userId, user.id))
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { type ActionInsert, actions } from "@autumn/shared";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
|
||||
export class ActionService {
|
||||
static async insert(db: DrizzleCli, data: ActionInsert | ActionInsert[]) {
|
||||
|
||||
@@ -73,7 +73,7 @@ export class AnalyticsService {
|
||||
}
|
||||
|
||||
static async getTopUser({ req }: { req: ExtendedRequest }) {
|
||||
const { clickhouseClient, org, env, db } = req;
|
||||
const { clickhouseClient, org, env } = req;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
@@ -136,7 +136,7 @@ WHERE
|
||||
req: ExtendedRequest;
|
||||
eventName?: string;
|
||||
}) {
|
||||
const { clickhouseClient, org, env, db } = req;
|
||||
const { clickhouseClient, org, env } = req;
|
||||
|
||||
const query = `
|
||||
SELECT SUM(
|
||||
@@ -165,7 +165,7 @@ WHERE event_name = {eventName:String}
|
||||
}
|
||||
|
||||
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
|
||||
FROM customers
|
||||
WHERE org_id = {org_id:String}
|
||||
@@ -213,8 +213,6 @@ WHERE org_id = {org_id:String}
|
||||
const getBCResults =
|
||||
isBillingCycle && !aggregateAll && customer
|
||||
? ((await getBillingCycleStartDate(
|
||||
env,
|
||||
org?.id,
|
||||
customer,
|
||||
db,
|
||||
intervalType as "1bc" | "3bc",
|
||||
@@ -335,8 +333,6 @@ order by dr.period;
|
||||
const getBCResults =
|
||||
isBillingCycle && !aggregateAll && customer
|
||||
? ((await getBillingCycleStartDate(
|
||||
env,
|
||||
org?.id,
|
||||
customer,
|
||||
db,
|
||||
intervalType as "1bc" | "3bc",
|
||||
@@ -380,15 +376,6 @@ order by dr.period;
|
||||
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({
|
||||
query: query,
|
||||
query_params: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
|
||||
export class RevenueService {
|
||||
static clickhouseAvailable =
|
||||
|
||||
@@ -11,7 +11,6 @@ import { routeHandler } from "@/utils/routerUtils.js";
|
||||
const analyticsRouter = Router();
|
||||
|
||||
const RangeEnum = z.enum(["24h", "7d", "30d", "90d", "last_cycle"]);
|
||||
type Range = z.infer<typeof RangeEnum>;
|
||||
|
||||
analyticsRouter.post("", (req, res) =>
|
||||
routeHandler({
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import {
|
||||
ErrCode,
|
||||
FullCustomer,
|
||||
FullCusProduct,
|
||||
CusProductStatus,
|
||||
Subscription,
|
||||
AppEnv,
|
||||
FullProduct,
|
||||
CustomerEntitlement,
|
||||
FullCustomerEntitlement,
|
||||
cusProductToProduct,
|
||||
EntInterval,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
type FullCustomerEntitlement,
|
||||
type FullProduct,
|
||||
type Subscription,
|
||||
} 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 { isFreeProduct } from "../products/productUtils.js";
|
||||
|
||||
export async function getBillingCycleStartDate(
|
||||
env: AppEnv,
|
||||
orgId: string,
|
||||
customer?: FullCustomer,
|
||||
db?: DrizzleCli,
|
||||
intervalType?: "1bc" | "3bc",
|
||||
@@ -199,22 +193,26 @@ export function calculateStartDateFromInterval(
|
||||
return formatDateToString(
|
||||
new Date(nextResetAt! - 7 * 24 * 60 * 60 * 1000),
|
||||
);
|
||||
case EntInterval.Month:
|
||||
case EntInterval.Month: {
|
||||
const monthResetDate = new Date(nextResetAt!);
|
||||
monthResetDate.setMonth(monthResetDate.getMonth() - 1);
|
||||
return formatDateToString(monthResetDate);
|
||||
case EntInterval.Quarter:
|
||||
}
|
||||
case EntInterval.Quarter: {
|
||||
const quarterResetDate = new Date(nextResetAt!);
|
||||
quarterResetDate.setMonth(quarterResetDate.getMonth() - 3);
|
||||
return formatDateToString(quarterResetDate);
|
||||
case EntInterval.SemiAnnual:
|
||||
}
|
||||
case EntInterval.SemiAnnual: {
|
||||
const semiAnnualResetDate = new Date(nextResetAt!);
|
||||
semiAnnualResetDate.setMonth(semiAnnualResetDate.getMonth() - 6);
|
||||
return formatDateToString(semiAnnualResetDate);
|
||||
case EntInterval.Year:
|
||||
}
|
||||
case EntInterval.Year: {
|
||||
const yearResetDate = new Date(nextResetAt!);
|
||||
yearResetDate.setFullYear(yearResetDate.getFullYear() - 1);
|
||||
return formatDateToString(yearResetDate);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -178,8 +178,6 @@ export const handleProductsUpdated = async ({
|
||||
// });
|
||||
// }
|
||||
|
||||
console.log("Sending svix event for products updated");
|
||||
|
||||
// 2. Send Svix event
|
||||
await sendSvixEvent({
|
||||
org,
|
||||
|
||||
@@ -74,7 +74,7 @@ analyticsRouter.get("/event_names", async (req: any, res: any) =>
|
||||
);
|
||||
|
||||
const getTopEvents = async ({ req }: { req: ExtendedRequest }) => {
|
||||
const { org, env, features } = req;
|
||||
const { features } = req;
|
||||
|
||||
const topEventNamesRes = await AnalyticsService.getTopEventNames({
|
||||
req,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export interface FeatureCheckPreviewParams {
|
||||
customerId: string;
|
||||
featureId: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export const getFeatureCheckPreview = async ({
|
||||
customerId,
|
||||
featureId,
|
||||
quantity,
|
||||
}: FeatureCheckPreviewParams) => {};
|
||||
@@ -98,8 +98,6 @@ export const syncItem = async ({
|
||||
redisEntity = apiCustomer;
|
||||
}
|
||||
|
||||
console.log("Redis entity: ", redisEntity);
|
||||
|
||||
// Get fresh customer from DB (no locking - let deduction handle it)
|
||||
const fullCus = await CusService.getFull({
|
||||
db,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
type ApiEntityV1,
|
||||
addToExpand,
|
||||
CusExpand,
|
||||
type EntityLegacyData,
|
||||
type FullCustomer,
|
||||
filterEntityLevelCusProducts,
|
||||
filterOutEntitiesFromCusProducts,
|
||||
@@ -58,14 +59,17 @@ export const setCachedApiCustomer = async ({
|
||||
});
|
||||
|
||||
// Build entities first
|
||||
const entityBatch: { entityId: string; entityData: ApiEntityV1 }[] = [];
|
||||
const entityBatch: {
|
||||
entityId: string;
|
||||
entityData: ApiEntityV1 & { legacyData: EntityLegacyData };
|
||||
}[] = [];
|
||||
const entityFullCus = {
|
||||
...fullCus,
|
||||
customer_products: entityLevelCusProducts,
|
||||
};
|
||||
|
||||
for (const entity of fullCus.entities) {
|
||||
const { apiEntity } = await getApiEntityBase({
|
||||
const { apiEntity, legacyData: entityLegacyData } = await getApiEntityBase({
|
||||
ctx: ctxWithExpand,
|
||||
fullCus: entityFullCus,
|
||||
entity,
|
||||
@@ -74,17 +78,27 @@ export const setCachedApiCustomer = async ({
|
||||
|
||||
entityBatch.push({
|
||||
entityId: entity.id,
|
||||
entityData: apiEntity,
|
||||
entityData: {
|
||||
...apiEntity,
|
||||
legacyData: entityLegacyData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Then write to Redis
|
||||
const masterApiCustomerData = {
|
||||
...masterApiCustomer,
|
||||
entities: fullCus.entities,
|
||||
entities: fullCus.entities.filter((e) => e.id !== null),
|
||||
legacyData,
|
||||
};
|
||||
|
||||
if (masterApiCustomerData.id === null) return;
|
||||
|
||||
// console.log(
|
||||
// `Setting cached api customer ${customerId}, masterApiCustomerData: `,
|
||||
// masterApiCustomerData,
|
||||
// );
|
||||
|
||||
await tryRedisWrite(async () => {
|
||||
await redis.eval(
|
||||
SET_CUSTOMER_SCRIPT,
|
||||
@@ -95,11 +109,15 @@ export const setCachedApiCustomer = async ({
|
||||
customerId,
|
||||
);
|
||||
|
||||
const filteredEntityBatch = entityBatch.filter(
|
||||
(e) => e.entityData.id !== null,
|
||||
);
|
||||
|
||||
if (entityBatch.length > 0) {
|
||||
await redis.eval(
|
||||
SET_ENTITIES_BATCH_SCRIPT,
|
||||
0,
|
||||
JSON.stringify(entityBatch),
|
||||
JSON.stringify(filteredEntityBatch),
|
||||
org.id,
|
||||
env,
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type Stripe from "stripe";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { getOriginalCouponId } from "../../../rewards/rewardUtils";
|
||||
|
||||
export const getCusRewards = async ({
|
||||
org,
|
||||
@@ -35,16 +36,17 @@ export const getCusRewards = async ({
|
||||
const stripeCli = createStripeCli({
|
||||
org,
|
||||
env,
|
||||
legacyVersion: true,
|
||||
});
|
||||
|
||||
const [stripeCus, stripeSubs] = await Promise.all([
|
||||
stripeCli.customers.retrieve(
|
||||
fullCus.processor?.id,
|
||||
) as Promise<Stripe.Customer>,
|
||||
stripeCli.customers.retrieve(fullCus.processor?.id, {
|
||||
expand: ["discount.coupon"],
|
||||
}) as Promise<Stripe.Customer>,
|
||||
getStripeSubs({
|
||||
stripeCli,
|
||||
subIds,
|
||||
expand: ["discounts", "discounts.source.coupon"],
|
||||
expand: ["discounts", "discounts.coupon"],
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -59,14 +61,12 @@ export const getCusRewards = async ({
|
||||
const rewards = {
|
||||
discounts: stripeDiscounts
|
||||
.map((d) => {
|
||||
if (typeof d.source.coupon === "string") {
|
||||
if (!("coupon" in d) || typeof d.coupon === "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const coupon = d.source.coupon;
|
||||
if (!coupon) {
|
||||
return null;
|
||||
}
|
||||
const coupon = d.coupon as Stripe.Coupon;
|
||||
const couponId = getOriginalCouponId(coupon.id);
|
||||
|
||||
let duration_type: CouponDurationType;
|
||||
let duration_value = 0;
|
||||
@@ -81,7 +81,7 @@ export const getCusRewards = async ({
|
||||
duration_type = CouponDurationType.OneOff;
|
||||
}
|
||||
return {
|
||||
id: coupon.id,
|
||||
id: couponId,
|
||||
name: coupon.name ?? "",
|
||||
type: coupon.amount_off
|
||||
? RewardType.FixedDiscount
|
||||
|
||||
@@ -158,7 +158,7 @@ export const handleCreateOtp = async (req: any, res: any) =>
|
||||
res,
|
||||
action: "Create OTP",
|
||||
handler: async () => {
|
||||
const { orgId, env, db } = req;
|
||||
const { orgId } = req;
|
||||
|
||||
// Check if there's already an OTP to use
|
||||
const maybeCacheKey = `orgOTPExists:${orgId}`;
|
||||
@@ -208,7 +208,10 @@ export const handleGetOtp = async (req: any, res: any) =>
|
||||
const { db, env } = req;
|
||||
const { otp } = req.params;
|
||||
const cacheKey = `otp:${otp}`;
|
||||
const cacheData = await CacheManager.getJson(cacheKey);
|
||||
const cacheData = await CacheManager.getJson<{
|
||||
orgId: string;
|
||||
stripeFlowAuthKey: string;
|
||||
}>(cacheKey);
|
||||
if (!cacheData) {
|
||||
res.status(404).json({ error: "OTP not found" });
|
||||
return;
|
||||
@@ -291,7 +294,7 @@ devRouter.post("/cli/stripe", async (req: any, res: any) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheData = await CacheManager.getJson(key);
|
||||
const cacheData = await CacheManager.getJson<{ orgId: string }>(key);
|
||||
if (!cacheData) {
|
||||
res.status(404).json({ message: "Key not found" });
|
||||
return;
|
||||
|
||||
@@ -123,7 +123,7 @@ export const getCachedApiEntity = async ({
|
||||
ctx,
|
||||
entity,
|
||||
fullCus: fullCus,
|
||||
withAutumnId: !skipCache,
|
||||
withAutumnId: true,
|
||||
});
|
||||
|
||||
const { apiEntity: pureApiEntity } = await getApiEntityBase({
|
||||
|
||||
@@ -96,6 +96,7 @@ export const createEntities = async ({
|
||||
|
||||
const clonedFullCus = structuredClone(fullCus);
|
||||
clonedFullCus.entity = entity;
|
||||
|
||||
const apiEntity = await getApiEntity({
|
||||
ctx,
|
||||
customerId,
|
||||
|
||||
@@ -96,7 +96,7 @@ export const handleCreatePlan = createRoute({
|
||||
// body: CreateProductV2ParamsSchema,
|
||||
versionedBody: {
|
||||
latest: CreatePlanParamsSchema,
|
||||
[ApiVersion.V1_2]: CreateProductV2ParamsSchema,
|
||||
[ApiVersion.V1_Beta]: CreateProductV2ParamsSchema,
|
||||
},
|
||||
resource: AffectedResource.Product,
|
||||
handler: async (c) => {
|
||||
|
||||
@@ -39,11 +39,11 @@ import { handleUpdateProductDetails } from "./updateProductDetails.js";
|
||||
export const handleUpdatePlan = createRoute({
|
||||
versionedBody: {
|
||||
latest: UpdatePlanParamsSchema,
|
||||
[ApiVersion.V1_2]: UpdateProductV2ParamsSchema,
|
||||
[ApiVersion.V1_Beta]: UpdateProductV2ParamsSchema,
|
||||
},
|
||||
versionedQuery: {
|
||||
latest: UpdatePlanQuerySchema,
|
||||
[ApiVersion.V1_2]: UpdateProductQuerySchema,
|
||||
[ApiVersion.V1_Beta]: UpdateProductQuerySchema,
|
||||
},
|
||||
resource: AffectedResource.Product,
|
||||
handler: async (c) => {
|
||||
@@ -57,6 +57,7 @@ export const handleUpdatePlan = createRoute({
|
||||
|
||||
// Convert to ProductV2 format only if client sent V2 Plan format
|
||||
// V1.2 clients already send ProductV2, no conversion needed
|
||||
|
||||
const v1_2Body = ctx.apiVersion.gte(new ApiVersionClass(ApiVersion.V2_0))
|
||||
? planToProductV2({ plan: body as ApiPlan, features: ctx.features })
|
||||
: (body as UpdateProductV2Params);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { handleDeleteProduct as handleDeleteProductHono } from "./handlers/handl
|
||||
import { handleGetPlan } from "./handlers/handleGetPlan.js";
|
||||
import { handleGetPlanDeleteInfo } from "./handlers/handleGetPlanDeleteInfo.js";
|
||||
import { handleListPlans } from "./handlers/handleListPlans.js";
|
||||
import { handleMigrateProductV2 } from "./handlers/handleMigrateProductV2.js";
|
||||
import { handlePlanHasCustomers } from "./handlers/handlePlanHasCustomers.js";
|
||||
import { handleUpdatePlan } from "./handlers/handleUpdateProduct/handleUpdatePlan.js";
|
||||
|
||||
@@ -16,11 +17,15 @@ honoProductBetaRouter.get("", ...handleListPlans);
|
||||
export const honoProductRouter = new Hono<HonoEnv>();
|
||||
export const migrationRouter = new Hono<HonoEnv>();
|
||||
|
||||
// Migrations
|
||||
migrationRouter.post("/migrations", ...handleMigrateProductV2);
|
||||
|
||||
// CRUD
|
||||
honoProductRouter.get("", ...handleListPlans);
|
||||
honoProductRouter.post("", ...handleCreatePlan);
|
||||
honoProductRouter.get("/:product_id", ...handleGetPlan);
|
||||
honoProductRouter.post("/:product_id", ...handleUpdatePlan); // will be deprecated
|
||||
honoProductRouter.patch("/:product_id", ...handleUpdatePlan); // will be deprecated
|
||||
honoProductRouter.delete("/:product_id", ...handleDeleteProductHono);
|
||||
|
||||
// Others
|
||||
|
||||
@@ -48,7 +48,8 @@ export class ViewsService {
|
||||
|
||||
// Also save to a list for easy retrieval
|
||||
const listKey = `saved_views_list:${orgId}:${env}`;
|
||||
const existingViews = (await CacheManager.getJson(listKey)) || [];
|
||||
const existingViews =
|
||||
(await CacheManager.getJson<string[]>(listKey)) || [];
|
||||
existingViews.push(viewId);
|
||||
await CacheManager.setJson(listKey, existingViews, "forever"); // No TTL
|
||||
|
||||
@@ -74,12 +75,18 @@ export class ViewsService {
|
||||
const env = req.env;
|
||||
|
||||
const listKey = `saved_views_list:${orgId}:${env}`;
|
||||
const viewIds = (await CacheManager.getJson(listKey)) || [];
|
||||
const viewIds = (await CacheManager.getJson<string[]>(listKey)) || [];
|
||||
|
||||
const views = [];
|
||||
for (const viewId of viewIds) {
|
||||
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) {
|
||||
views.push({
|
||||
id: view.id,
|
||||
@@ -120,7 +127,8 @@ export class ViewsService {
|
||||
|
||||
// Remove from list
|
||||
const listKey = `saved_views_list:${orgId}:${env}`;
|
||||
const existingViews = (await CacheManager.getJson(listKey)) || [];
|
||||
const existingViews =
|
||||
(await CacheManager.getJson<string[]>(listKey)) || [];
|
||||
const updatedViews = existingViews.filter(
|
||||
(id: string) => id !== viewId,
|
||||
);
|
||||
|
||||
29
server/src/utils/envUtils.ts
Normal file
29
server/src/utils/envUtils.ts
Normal 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)`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -5,17 +5,6 @@ dotenv.config();
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { clearOrg, setupOrg } from "@tests/utils/setup.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 DEFAULT_ENV = AppEnv.Sandbox;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user