Files
cfw-autumn/server/tests/utils/setup.ts
2025-10-17 13:38:35 +01:00

491 lines
12 KiB
TypeScript

import {
AppEnv,
type CreateReward,
type Feature,
FeatureType,
type FullProduct,
type Organization,
type Price,
PriceType,
type RewardProgram,
RewardType,
} from "@autumn/shared";
import axios from "axios";
import type Stripe from "stripe";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features as v2Features } from "tests/setup/v2Features.js";
import { initDrizzle } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { CacheType } from "@/external/caching/cacheActions.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { deactivateStripeMeters } from "@/external/stripe/stripeProductUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
import { deleteAllStripeTestClocks } from "./stripeUtils.js";
export const getAxiosInstance = (
apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!,
) => {
return axios.create({
baseURL: "http://localhost:8080",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
};
export const getPublicAxiosInstance = ({
withBearer,
pkey = process.env.UNIT_TEST_AUTUMN_PUBLIC_KEY!,
}: {
withBearer: boolean;
pkey?: string;
}) => {
const headers = withBearer
? {
Authorization: `Bearer ${pkey}`,
}
: {
"x-publishable-key": pkey,
};
return axios.create({
baseURL: "http://localhost:8080",
headers: headers,
});
};
export const clearOrg = async ({
orgSlug,
env,
}: {
orgSlug: string;
env?: AppEnv;
}) => {
if (env !== AppEnv.Sandbox) {
console.error("Cannot clear non-sandbox orgs");
process.exit(1);
}
const autumn = new AutumnInt();
if (process.env.STRIPE_TEST_KEY) {
console.log(`Reconnecting stripe...`);
try {
await autumn.stripe.delete();
} catch (error) {}
try {
await autumn.stripe.connect({
secret_key: process.env.STRIPE_TEST_KEY!,
success_url: "https://useautumn.com",
default_currency: "usd",
});
} catch (error: any) {
console.error("Error reconnecting stripe", error.message);
process.exit(1);
}
}
const { db, client } = initDrizzle();
const org = await OrgService.getBySlug({ db, slug: orgSlug });
await Promise.all([
CacheManager.invalidate({
action: CacheType.SecretKey,
value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!),
}),
CacheManager.invalidate({
action: CacheType.PublicKey,
value: process.env.UNIT_TEST_AUTUMN_PUBLIC_KEY!,
}),
]);
await CacheManager.disconnect();
if (!org) {
throw new Error(`Org ${orgSlug} not found`);
}
if (!(org.slug === "unit-test-org" || org.slug === "ci_cd")) {
console.error("Cannot clear non-unit-test-orgs");
process.exit(1);
}
const orgId = org.id;
// 1. Delete all customers
await CusService.deleteByOrgId({ db, orgId, env });
console.log(" ✅ Deleted customers");
const stripeCli = createStripeCli({ org, env: env! });
const stripeCustomers = await stripeCli.customers.list({
limit: 100,
});
const deleteCustomer = async (customer: Stripe.Customer) => {
try {
await stripeCli.customers.del(customer.id);
} catch (error) {
console.error("Error deleting stripe customer", customer.id);
}
};
const cusBatchSize = 5;
for (let i = 0; i < stripeCustomers.data.length; i += cusBatchSize) {
const batch = stripeCustomers.data.slice(i, i + cusBatchSize);
const batchDeleteCustomers = [];
for (const customer of batch) {
batchDeleteCustomers.push(deleteCustomer(customer));
}
await Promise.all(batchDeleteCustomers);
console.log(
` ✅ Deleted ${i + batch.length}/${
stripeCustomers.data.length
} Stripe customers`,
);
}
console.log(" ✅ Deleted Stripe customers");
// 2. Delete all products
await ProductService.deleteByOrgId({ db, orgId, env });
console.log(" ✅ Deleted products");
const stripeProducts = await stripeCli.products.list({
limit: 100,
active: true,
});
const batchSize = 5;
const removeStripeProduct = async (product: Stripe.Product) => {
try {
await stripeCli.products.del(product.id);
} catch (error) {
await stripeCli.products.update(product.id, {
active: false,
});
}
};
for (let i = 0; i < stripeProducts.data.length; i += batchSize) {
const batch = stripeProducts.data.slice(i, i + batchSize);
const batchDeleteProducts = [];
for (const product of batch) {
batchDeleteProducts.push(removeStripeProduct(product));
}
await Promise.all(batchDeleteProducts);
// await timeout(800);
console.log(
` ✅ Deleted ${i + batch.length}/${
stripeProducts.data.length
} Stripe products`,
);
}
console.log(" ✅ Deleted Stripe products");
await deleteAllStripeTestClocks({ stripeCli });
console.log(" ✅ Deleted Stripe test clocks");
// Delete all stripe meters
await deactivateStripeMeters({ org, env });
console.log(" ✅ Deactivated Stripe meters");
// Batch delete coupons
const batchDeleteCoupons = [];
await RewardService.deleteByOrgId({ db, orgId, env });
const stripeCoupons = await stripeCli.coupons.list({
limit: 100,
});
for (const coupon of stripeCoupons.data) {
batchDeleteCoupons.push(stripeCli.coupons.del(coupon.id));
}
await Promise.all(batchDeleteCoupons);
console.log(" ✅ Deleted Stripe coupons");
await FeatureService.deleteByOrgId({ db, orgId, env });
console.log(`✅ Cleared org ${orgSlug} (${env})`);
await client.end();
return org;
};
export const setupOrg = async ({
orgId,
env,
features,
products,
rewards,
rewardTriggers,
}: {
orgId: string;
env: AppEnv;
features: Record<string, Feature & { eventName: string }>;
products: Record<string, FullProduct | any>;
rewards: Record<string, any>;
rewardTriggers: Record<string, RewardProgram>;
}) => {
const axiosInstance = getAxiosInstance();
const { client, db } = initDrizzle();
const autumn = new AutumnInt();
const insertFeatures = [];
for (const feature of Object.values(features)) {
insertFeatures.push(axiosInstance.post("/v1/internal_features", feature));
}
await Promise.all(insertFeatures);
await FeatureService.insert({
db,
data: Object.values(v2Features),
logger: console,
});
// const allFeatures = await FeatureService.list({ db, orgId, env });
let org: Organization | null = null;
let newFeatures: Feature[] = [];
try {
org = await OrgService.get({ db, orgId });
await OrgService.update({
db,
orgId,
updates: {
config: {
...org.config,
bill_upgrade_immediately: true,
},
},
});
newFeatures = (await FeatureService.list({ db, orgId, env })).filter((f) =>
Object.keys(features).includes(f.id),
);
} catch (error) {
console.error("Error updating org", error);
}
for (const feature of newFeatures!) {
features[feature.id].internal_id = feature.internal_id;
if (feature.type === FeatureType.Metered) {
features[feature.id].eventName = feature.event_names?.[0] || feature.id;
}
}
console.log("✅ Inserted features");
// 2. Create products
const insertProducts = [];
const productValues = Object.values(products);
const batchSize = 5;
for (
let batchStart = 0;
batchStart < productValues.length;
batchStart += batchSize
) {
const batch = productValues.slice(batchStart, batchStart + batchSize);
const batchPromises = [];
for (const product of batch) {
const insertProduct = async () => {
await autumn.products.create({
id: product.id,
name: product.name,
group: product.group,
is_add_on: product.is_add_on,
is_default: product.is_default,
});
const prices = product.prices.map((p: any) => ({
...p,
config: {
...p.config,
internal_feature_id: newFeatures!.find(
(f) => f.id === (p.config as any)?.feature_id,
)?.internal_id,
},
}));
const entitlements = Object.values(product.entitlements).map(
(ent: any) => ({
...ent,
internal_feature_id: newFeatures!.find(
(f) => f.id === ent.feature_id,
)?.internal_id,
}),
);
const entWithFeatures = entitlements.map((ent) => ({
...ent,
feature: newFeatures!.find((f) => f.id === ent.feature_id),
}));
const items = mapToProductItems({
prices,
entitlements: entWithFeatures,
allowFeatureMatch: true,
features: newFeatures!,
});
try {
await axiosInstance.post(`/v1/products/${product.id}`, {
// prices: prices,
// entitlements: entitlements,
items,
free_trial: product.free_trial,
});
} catch (error) {
console.log("Product:", product.name);
console.error("Error creating product prices / ents");
console.log("Items", items);
}
return;
};
batchPromises.push(insertProduct());
}
// Wait for the current batch to complete before proceeding to the next
await Promise.all(batchPromises);
insertProducts.push(...batchPromises);
}
await Promise.all(insertProducts);
console.log("✅ Inserted products");
if (process.env.MOCHA_PARALLEL === "true") {
console.log("MOCHA RUNNING IN PARALLEL");
await AutumnCli.initStripeProducts();
console.log("✅ Initialized stripe products / prices");
} else {
console.log("MOCHA RUNNING IN SERIAL");
}
// return;
// Fetch all products
const { list: allProducts } = await AutumnCli.getProducts();
const productIds = allProducts.map((p: any) => p.id);
// Insert coupons
const insertCoupons = [];
for (const reward of Object.values(rewards)) {
const createReward = async () => {
let priceIds = [];
const rewardData: any = {
id: reward.id,
name: reward.name,
promo_codes: [
{
code: reward.id,
},
],
type: reward.type,
};
if (reward.type === RewardType.FreeProduct) {
rewardData.free_product_id = reward.free_product_id;
rewardData.free_product_config = reward.free_product_config;
} else {
if (reward.only_usage_prices) {
const filteredProducts = allProducts.filter(
(product: FullProduct) => {
if (reward.product_ids) {
return reward.product_ids.includes(product.id);
} else return true;
},
);
priceIds = filteredProducts.flatMap((product: FullProduct) =>
product.prices
.filter((price: Price) => price.config!.type === PriceType.Usage)
.map((price) => {
return price.id;
}),
);
} else if (reward.product_ids) {
priceIds = allProducts
.filter((product: FullProduct) =>
reward.product_ids.includes(product.id),
)
.flatMap((product: FullProduct) =>
product.prices.map((price) => price.id),
);
}
rewardData.discount_config = {
discount_value: reward.discount_config.discount_value,
duration_type: reward.discount_config.duration_type,
duration_value: reward.discount_config.duration_value,
apply_to_all: reward.discount_config.apply_to_all,
price_ids: priceIds,
};
}
const newReward: CreateReward & { internal_id: string } = {
internal_id: reward.id,
id: reward.id,
name: reward.name,
promo_codes: [
{
code: reward.id,
},
],
type: reward.type,
discount_config: rewardData.discount_config,
free_product_id: rewardData.free_product_id,
free_product_config:
rewardData.free_product_config?.duration_type &&
rewardData.free_product_config?.duration_value
? rewardData.free_product_config
: undefined,
};
const rewardRes = await autumn.rewards.create(newReward);
return {
id: reward.id,
rewardRes,
};
};
console.log("Creating reward", reward.id);
insertCoupons.push(createReward());
}
await Promise.all(insertCoupons);
console.log("✅ Inserted coupons");
// CREATE REWARD TRIGGERS
const insertRewardTriggers = [];
const insertedRewards = await RewardService.list({ db, orgId, env });
for (const rewardTrigger of Object.values(rewardTriggers)) {
const rt = {
...rewardTrigger,
internal_reward_id: insertedRewards.find(
(r) => r.id === rewardTrigger.internal_reward_id,
)?.internal_id!,
};
insertRewardTriggers.push(autumn.rewardPrograms.create(rt));
}
await Promise.all(insertRewardTriggers);
console.log("✅ Inserted reward triggers");
await client.end();
};