fix: scan script
This commit is contained in:
405
server/src/scan/runScan.ts
Normal file
405
server/src/scan/runScan.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import assert from "node:assert";
|
||||
import {
|
||||
AppEnv,
|
||||
CusProductStatus,
|
||||
cusProductToPrices,
|
||||
type Entity,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import type Stripe from "stripe";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
||||
import { EntityService } from "@/internal/api/entities/EntityService.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { getRelatedCusPrice } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
|
||||
import { checkCusSubCorrect } from "@/utils/checkUtils/checkCustomerCorrect.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
getAllEntities,
|
||||
getAllFullCustomers,
|
||||
} from "@/utils/scriptUtils/getAll/getAllAutumnCustomers.js";
|
||||
import {
|
||||
getAllStripeSchedules,
|
||||
getAllStripeSubscriptions,
|
||||
} from "@/utils/scriptUtils/getAll/getAllStripeSubs.js";
|
||||
|
||||
const { db } = initDrizzle({ maxConnections: 5 });
|
||||
|
||||
const orgSlugs = process.env.ORG_SLUGS!.split(",");
|
||||
const skipEmails = process.env.SKIP_EMAILS!.split(",");
|
||||
const skipIds = [
|
||||
"cus_2tXCCwC6iyiftgA6ndSo1Ubb2dx",
|
||||
"DxG668K7uDd0Vahk54YWjvCGVgf2",
|
||||
];
|
||||
|
||||
// orgSlugs = ["athenahq"];
|
||||
const customerId = null;
|
||||
|
||||
const getSingleCustomer = async ({
|
||||
stripeCli,
|
||||
customerId,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
customerId: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const customers = [
|
||||
await CusService.getFull({
|
||||
db,
|
||||
idOrInternalId: customerId,
|
||||
orgId,
|
||||
env,
|
||||
}),
|
||||
];
|
||||
|
||||
const stripeCusId = customers[0].processor?.id;
|
||||
const stripeSubs = stripeCusId
|
||||
? (
|
||||
await stripeCli.subscriptions.list({
|
||||
customer: stripeCusId,
|
||||
expand: ["data.discounts.coupon"],
|
||||
})
|
||||
).data
|
||||
: [];
|
||||
|
||||
// const stripeSubs = await getStripeSubs({
|
||||
// stripeCli,
|
||||
// subIds: customers[0].customer_products.flatMap(
|
||||
// (cp) => cp.subscription_ids || []
|
||||
// ),
|
||||
// });
|
||||
|
||||
let scheduleIds = customers[0].customer_products.flatMap(
|
||||
(cp) => cp.scheduled_ids || [],
|
||||
);
|
||||
|
||||
scheduleIds = Array.from(new Set(scheduleIds));
|
||||
|
||||
const stripeSchedules = await getStripeSchedules({
|
||||
stripeCli,
|
||||
scheduleIds,
|
||||
});
|
||||
|
||||
const entities = await EntityService.list({
|
||||
db,
|
||||
internalCustomerId: customers[0].internal_id,
|
||||
});
|
||||
|
||||
return { customers, stripeSubs, stripeSchedules, entities };
|
||||
};
|
||||
|
||||
const checkCustomerCorrect = async ({
|
||||
fullCus,
|
||||
subs,
|
||||
schedules,
|
||||
org,
|
||||
entities,
|
||||
}: {
|
||||
fullCus: FullCustomer;
|
||||
subs: Stripe.Subscription[];
|
||||
schedules: Stripe.SubscriptionSchedule[];
|
||||
org: Organization;
|
||||
entities: Entity[];
|
||||
}) => {
|
||||
if (skipIds.includes(fullCus.internal_id!)) return;
|
||||
|
||||
if (skipEmails.some((skipEmail) => skipEmail === fullCus.email)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fullCus.entities = entities.filter(
|
||||
(entity) => entity.internal_customer_id === fullCus.internal_id,
|
||||
);
|
||||
|
||||
// console.log(`Checking ${fullCus.email} (${fullCus.id})`);
|
||||
const cusProducts = fullCus.customer_products;
|
||||
|
||||
await checkCusSubCorrect({
|
||||
db,
|
||||
fullCus,
|
||||
subs,
|
||||
schedules,
|
||||
org,
|
||||
env: AppEnv.Live,
|
||||
});
|
||||
|
||||
for (const cusProduct of cusProducts) {
|
||||
if (!cusProduct.subscription_ids) continue;
|
||||
|
||||
if (cusProduct.status === CusProductStatus.Scheduled) {
|
||||
// Check if there's a main product elsewhere
|
||||
const mainCusProd = cusProducts.find(
|
||||
(cp: FullCusProduct) =>
|
||||
cp.product.group === cusProduct.product.group &&
|
||||
cp.id !== cusProduct.id &&
|
||||
cp.status !== CusProductStatus.Scheduled &&
|
||||
(cusProduct.internal_entity_id
|
||||
? cusProduct.internal_entity_id === cp.internal_entity_id
|
||||
: true),
|
||||
);
|
||||
|
||||
assert(
|
||||
mainCusProd,
|
||||
`Found scheduled cus product with no main product (${cusProduct.product.name})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!cusProduct.product.is_add_on &&
|
||||
cusProduct.status !== CusProductStatus.Scheduled
|
||||
) {
|
||||
const group = cusProduct.product.group;
|
||||
const otherCusProd = cusProducts.find(
|
||||
(cp: FullCusProduct) =>
|
||||
cp.product.group === group &&
|
||||
cp.id !== cusProduct.id &&
|
||||
!cp.product.is_add_on &&
|
||||
cp.status !== CusProductStatus.Scheduled &&
|
||||
cp.internal_entity_id === cusProduct.internal_entity_id,
|
||||
);
|
||||
|
||||
assert(
|
||||
!otherCusProd,
|
||||
`found two cus products from the same group: ${otherCusProd?.product.name} and ${cusProduct.product.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const stripeSubs = subs.filter((sub: any) =>
|
||||
cusProduct.subscription_ids!.some((id: string) => id === sub.id),
|
||||
);
|
||||
|
||||
assert(
|
||||
stripeSubs.length === cusProduct.subscription_ids!.length,
|
||||
"number of stripe subs should be the same as number of subscription ids",
|
||||
);
|
||||
|
||||
// let subItems = stripeSubs.flatMap((sub: any) => sub.items.data);
|
||||
|
||||
const prices = cusProductToPrices({ cusProduct });
|
||||
|
||||
if (
|
||||
isOneOff(prices) ||
|
||||
isFreeProduct(prices) ||
|
||||
cusProduct.status === CusProductStatus.Scheduled
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const cusEnt of cusProduct.customer_entitlements) {
|
||||
const cusPrice = getRelatedCusPrice(cusEnt, cusProduct.customer_prices);
|
||||
|
||||
if (cusEnt.usage_allowed && !cusPrice) {
|
||||
assert.fail(
|
||||
`Feature ${cusEnt.feature_id} has usage allowed but no related cus price`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other checks to perform
|
||||
};
|
||||
|
||||
const checkCustomerHandleError = async ({
|
||||
fullCus,
|
||||
subs,
|
||||
org,
|
||||
schedules,
|
||||
entities,
|
||||
}: {
|
||||
fullCus: FullCustomer;
|
||||
subs: Stripe.Subscription[];
|
||||
org: Organization;
|
||||
schedules: Stripe.SubscriptionSchedule[];
|
||||
entities: Entity[];
|
||||
}) => {
|
||||
try {
|
||||
await checkCustomerCorrect({
|
||||
fullCus,
|
||||
subs,
|
||||
org,
|
||||
schedules,
|
||||
entities,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
id: fullCus.id,
|
||||
name: fullCus.name,
|
||||
email: fullCus.email,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const check = async () => {
|
||||
const env = AppEnv.Live;
|
||||
const sb = createSupabaseClient();
|
||||
|
||||
const today = new Date().toISOString().slice(0, 16);
|
||||
|
||||
for (const slug of orgSlugs) {
|
||||
const org = await OrgService.getBySlug({
|
||||
db,
|
||||
slug,
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
console.log(`Org ${slug} not found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileName = `errors/${today}-${org.slug}.json`;
|
||||
|
||||
const stripeCli = createStripeCli({
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
console.log("--------------------------------");
|
||||
console.log(`Running error check for ${org.name}`);
|
||||
|
||||
let customers: FullCustomer[] = [];
|
||||
let stripeSubs: Stripe.Subscription[] = [];
|
||||
let stripeSchedules: Stripe.SubscriptionSchedule[] = [];
|
||||
let entities: Entity[] = [];
|
||||
|
||||
if (customerId) {
|
||||
const res = await getSingleCustomer({
|
||||
stripeCli,
|
||||
customerId,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
customers = res.customers;
|
||||
stripeSubs = res.stripeSubs;
|
||||
entities = res.entities;
|
||||
} else {
|
||||
const [customersRes, stripeSubsRes, stripeSchedulesRes, entitiesRes] =
|
||||
await Promise.all([
|
||||
getAllFullCustomers({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
}),
|
||||
getAllStripeSubscriptions({
|
||||
stripeCli,
|
||||
waitForSeconds: 1,
|
||||
}),
|
||||
getAllStripeSchedules({
|
||||
stripeCli,
|
||||
waitForSeconds: 1,
|
||||
}),
|
||||
getAllEntities({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
}),
|
||||
]);
|
||||
|
||||
customers = customersRes;
|
||||
stripeSubs = stripeSubsRes.subscriptions;
|
||||
stripeSchedules = stripeSchedulesRes.schedules;
|
||||
entities = entitiesRes;
|
||||
}
|
||||
|
||||
const batchSize = 1;
|
||||
const allErrors = [];
|
||||
for (let i = 0; i < customers.length; i += batchSize) {
|
||||
const batch = customers.slice(i, i + batchSize);
|
||||
|
||||
const batchCheck: any = [];
|
||||
for (const customer of batch) {
|
||||
batchCheck.push(
|
||||
checkCustomerHandleError({
|
||||
fullCus: customer,
|
||||
subs: stripeSubs,
|
||||
schedules: stripeSchedules,
|
||||
org,
|
||||
entities,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let results = await Promise.all(batchCheck);
|
||||
results = results.filter(notNullish);
|
||||
allErrors.push(...results);
|
||||
}
|
||||
|
||||
console.log(`Found ${allErrors.length} errors`);
|
||||
|
||||
if (allErrors.length > 0 && customers.length > 1) {
|
||||
await sb.storage
|
||||
.from("autumn")
|
||||
.upload(fileName, JSON.stringify(allErrors, null, 2));
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
const slackBody = {
|
||||
text: `Error check for ${org.name}`,
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Error check for ${org.name}*: found ${allErrors.length} errors\nSee results at ${process.env.SUPABASE_URL}/storage/v1/object/public/autumn/${fileName}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fetch(process.env.SLACK_WEBHOOK_URL!, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(slackBody),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(allErrors);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`COMPLETED ERROR CHECK FOR ${new Date().toISOString().slice(0, 16)}`,
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const slackBody = {
|
||||
text: `Completed error check for ${new Date().toISOString().slice(0, 16)}`,
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `Error check completed for ${new Date().toISOString().slice(0, 16)}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fetch(process.env.SLACK_WEBHOOK_URL!, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(slackBody),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
check()
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in New Issue
Block a user