fix: coupons on new stripe version

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,97 +0,0 @@
import { Request, Response } from "express";
import { Webhook } from "svix";
import { OrgService } from "@/internal/orgs/OrgService.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { AppEnv } from "autumn-js";
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
import { deleteStripeWebhook } from "@/internal/orgs/orgUtils.js";
import { eq } from "drizzle-orm";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { Organization, organizations } from "@autumn/shared";
// const verifyClerkWebhook = async (req: Request, res: Response) => {
// const wh = new Webhook(process.env.CLERK_SIGNING_SECRET!);
// const headers = req.headers;
// const payload = req.body;
// const svix_id = headers["svix-id"];
// const svix_timestamp = headers["svix-timestamp"];
// const svix_signature = headers["svix-signature"];
// if (!svix_id || !svix_timestamp || !svix_signature) {
// res.status(400).json({
// success: false,
// message: "Error: Missing svix headers",
// });
// return;
// }
// let evt: any;
// try {
// evt = wh.verify(payload, {
// "svix-id": svix_id as string,
// "svix-timestamp": svix_timestamp as string,
// "svix-signature": svix_signature as string,
// });
// } catch (err) {
// console.log("Error: Could not verify webhook");
// res.status(400).json({
// success: false,
// message: "Error: Could not verify webhook",
// });
// return;
// }
// return evt;
// };
// export const handleClerkWebhook = async (req: any, res: any) => {
// let event = await verifyClerkWebhook(req, res);
// if (!event) {
// return;
// }
// const eventType = event.type;
// const eventData = event.data;
// try {
// switch (eventType) {
// case "organization.created":
// await saveOrgToDB({
// db: req.db,
// id: eventData.id,
// slug: eventData.slug,
// createdAt: eventData.created_at,
// });
// break;
// case "organization.deleted":
// await handleOrgDeleted({
// db: req.db,
// eventData,
// });
// break;
// default:
// break;
// }
// } catch (error) {
// handleRequestError({
// req,
// error,
// res,
// action: "Handle Clerk Webhook",
// });
// return;
// }
// return void res.status(200).json({
// success: true,
// message: "Webhook received",
// });
// };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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