Files
cfw-autumn/server/tests/utils/setup.ts
amianthus 5cb46bba96 Squashed commit of the following:
commit da2693567c
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 14:45:25 2025 +0100

    chore: ran biome format

commit db19928cea
Merge: 3005d452 d5d285a2
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 13:25:11 2025 +0100

    Merge branch 'staging' of https://github.com/useautumn/autumn into staging

commit 3005d45252
Merge: 5cb89756 784f7f92
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 13:25:01 2025 +0100

    Merge branch 'main' into staging

commit d5d285a25b
Merge: 894f943f b3952a27
Author: John Yeo <51376134+johnyeocx@users.noreply.github.com>
Date:   Mon Sep 22 13:19:48 2025 +0100

    Merge pull request #224 from muhammadusmandev/fix/create-product-prevent-empty-name-submission

    Fix: Create product prevent API call if product name field is empty and invalid characters

commit 894f943f31
Merge: 5cb89756 59aaf3c4
Author: John Yeo <51376134+johnyeocx@users.noreply.github.com>
Date:   Mon Sep 22 13:19:15 2025 +0100

    Merge pull request #223 from sidgaikwad/Fixes/Update-logic-reward-program

    Fixes/update logic reward program

commit 59aaf3c430
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 13:17:56 2025 +0100

    Fixes/Update-logic-reward-program

commit 5f9eb4f02d
Merge: 7b1b2cd2 5cb89756
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 13:17:46 2025 +0100

    Merge branch 'staging' into Fixes/Update-logic-reward-program

commit 7b1b2cd278
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 13:12:39 2025 +0100

    fix: show error message for update reward program

commit 5cb897560d
Merge: 79cf0f58 94dd5ba5
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 13:06:11 2025 +0100

    Merge branch 'staging' of https://github.com/useautumn/autumn into staging

commit 79cf0f5839
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 13:06:09 2025 +0100

    fix: update feature modal scrolls

commit 94dd5ba5b1
Merge: 9715e04e 609bd24b
Author: John Yeo <51376134+johnyeocx@users.noreply.github.com>
Date:   Mon Sep 22 12:58:51 2025 +0100

    Merge pull request #215 from shreyash1231/fix/Disabled-Create-Customer-Button-Until-Id-Or-Email-is-Filled-To-Prevent-API-CALL

    Fix/disabled create customer button until id or email is filled to prevent api call

commit 9715e04edd
Merge: 97190711 fe96c87a
Author: John Yeo <51376134+johnyeocx@users.noreply.github.com>
Date:   Mon Sep 22 12:44:40 2025 +0100

    Merge pull request #213 from SirTenzin/fix/event-timezone

    fix: 🐛 timezones not localised

commit 9719071149
Merge: 9c1862b4 7e201dae
Author: John Yeo <51376134+johnyeocx@users.noreply.github.com>
Date:   Mon Sep 22 12:44:28 2025 +0100

    Merge pull request #217 from SirTenzin/fix/trmnl-values

    fix: 🐛 only counted row instead of value

commit 9c1862b405
Merge: 6040c103 4fd25cc1
Author: John Yeo <51376134+johnyeocx@users.noreply.github.com>
Date:   Mon Sep 22 12:43:36 2025 +0100

    Merge pull request #216 from SirTenzin/feat/reward-migrations

    fix: 🐛 reward price_ids migrations

commit 4fd25cc161
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 12:43:06 2025 +0100

    fix: ent rollover null / undefined difference

commit 784f7f9290
Merge: dd0e9f1f 6040c103
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 10:40:49 2025 +0100

    fix: referral program coupon reward, apply to redeemer

commit dd0e9f1fd7
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 10:06:27 2025 +0100

    fix: referral utils apply to both coupon

commit e40e5a06f9
Author: John Yeo <johnyeocx@gmail.com>
Date:   Mon Sep 22 09:53:38 2025 +0100

    chore: fixing reward migrations

commit e161cbec83
Merge: e8ff791a 6040c103
Author: John Yeo <johnyeocx@gmail.com>
Date:   Sun Sep 21 18:24:24 2025 +0100

    Merge branch 'staging' into feat/reward-migrations

commit b3952a2702
Author: Muhammad Usman <muhammadusmandev@yahoo.com>
Date:   Sat Sep 20 08:28:35 2025 -0700

    Fix: Create product prevent API call if product name field is empty and not valid

commit 1f609eb592
Author: sidgaikwad <gaikwadsiddharth039@gmail.com>
Date:   Sat Sep 20 00:53:46 2025 +0530

    changes for the update logic for passing proper id

commit 86222adfa9
Author: sidgaikwad <gaikwadsiddharth039@gmail.com>
Date:   Sat Sep 20 00:20:44 2025 +0530

    `feat(rewardPrograms): Update RewardProgramService to support idOrInternalId and add update endpoint`

commit ab655107fc
Author: sidgaikwad <gaikwadsiddharth039@gmail.com>
Date:   Fri Sep 19 23:21:05 2025 +0530

    changes to have view for the updaterewardprogram and also the service for it

commit e8ff791ace
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Fri Sep 19 12:58:48 2025 +0100

    fix: 🐛 dont use unmatched prices

commit e2528194fd
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Fri Sep 19 12:53:06 2025 +0100

    feat: 🎸 toast to alert of any unmigrated prices

commit 7e201dae41
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Tue Sep 16 14:47:04 2025 +0100

    fix: 🐛 only counted row instead of value

commit 590a2ae86f
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Tue Sep 16 14:23:26 2025 +0100

    refactor: 💡 clean up

commit d529d6adb7
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Tue Sep 16 13:52:35 2025 +0100

    refactor: 💡 types

commit d6e9802cdc
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Tue Sep 16 13:52:28 2025 +0100

    feat: 🎸 reward migrations

commit 609bd24b60
Author: shreyash1231 <chandwadkarshreyash@gmail.com>
Date:   Tue Sep 16 17:08:33 2025 +0530

    fix/Disabled-Create-Customer-Button-Until-Id-Or-Email-is-Filled-To-Prevent-API-CALL

commit d13505621c
Author: shreyash1231 <chandwadkarshreyash@gmail.com>
Date:   Tue Sep 16 16:41:45 2025 +0530

    Disabled-Create-Customer-Button-Until-Name-Id-Or-Email-isfilled-to-Prevent-API-CALL

commit c26bd73627
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Mon Sep 15 15:25:47 2025 +0100

    refactor: 💡 type sugar :)

commit 77790cb176
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Mon Sep 15 15:11:05 2025 +0100

    fix: 🐛 applying to all

commit fe96c87a7a
Author: amianthus <49116958+SirTenzin@users.noreply.github.com>
Date:   Mon Sep 15 14:47:16 2025 +0100

    fix: 🐛 timezones not localised
2025-09-22 13:49:19 +00: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 { deactivateStripeMeters } from "@/external/stripe/stripeProductUtils.js";
import { createStripeCli } from "@/external/stripe/utils.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.config?.filters[0].value[0];
}
}
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();
};