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