fix: deleting products query includes org_id and env
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -95,4 +95,10 @@ server/test.sh
|
||||
# bun (keep main repo clean betweeen different npm flavours)
|
||||
bun.lock
|
||||
bun.lockb
|
||||
.dev.vars
|
||||
.dev.vars
|
||||
|
||||
supabase/
|
||||
|
||||
# To load supabase
|
||||
# 1. Start supabase locally
|
||||
# 2. Load files
|
||||
@@ -36,8 +36,17 @@ const { db, client } = initDrizzle({ maxConnections: 5 });
|
||||
|
||||
let orgSlugs = process.env.ORG_SLUGS!.split(",");
|
||||
const skipEmails = process.env.SKIP_EMAILS!.split(",");
|
||||
|
||||
// orgSlugs = ["athenahq"];
|
||||
// "athenahq",
|
||||
// orgSlugs = ["sweep-ai-1739822644", "zero"];
|
||||
orgSlugs = [
|
||||
// "circlemind-zero"
|
||||
// 'zero',
|
||||
// 'sweep-ai-1739822644',
|
||||
// 'alex',
|
||||
// 'supermemory',
|
||||
// 'lingo',
|
||||
// 'lucid',
|
||||
];
|
||||
|
||||
const getSingleCustomer = async ({
|
||||
stripeCli,
|
||||
|
||||
@@ -8,6 +8,9 @@ import { schemas as schema } from "@autumn/shared";
|
||||
export let client = postgres(process.env.DATABASE_URL!);
|
||||
export let db = drizzle(client, { schema });
|
||||
|
||||
// export const localClient = postgres("postgresql://postgres:postgres@localhost:54322/postgres")
|
||||
// export const localDb = drizzle(localClient, { schema });
|
||||
|
||||
export const initDrizzle = (params?: { maxConnections?: number }) => {
|
||||
let maxConnections = params?.maxConnections || 10;
|
||||
const client = postgres(process.env.DATABASE_URL!, {
|
||||
|
||||
7
server/src/external/autumn/autumnCli.ts
vendored
7
server/src/external/autumn/autumnCli.ts
vendored
@@ -42,15 +42,18 @@ export class AutumnInt {
|
||||
|
||||
constructor({
|
||||
apiKey,
|
||||
secretKey,
|
||||
baseUrl,
|
||||
version,
|
||||
}: {
|
||||
apiKey?: string;
|
||||
secretKey?: string;
|
||||
baseUrl?: string;
|
||||
version?: string | APIVersion;
|
||||
} = {}) {
|
||||
// this.apiKey = apiKey || process.env.AUTUMN_API_KEY || "";
|
||||
this.apiKey = apiKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || "";
|
||||
this.apiKey =
|
||||
apiKey || secretKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || "";
|
||||
|
||||
this.headers = {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
@@ -415,7 +418,7 @@ export class AutumnInt {
|
||||
},
|
||||
};
|
||||
|
||||
track = async (params: TrackParams) => {
|
||||
track = async (params: TrackParams & { timestamp?: number }) => {
|
||||
const data = await this.post(`/track`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ const handleInArrearProrated = async ({
|
||||
|
||||
let feature = cusEnt.entitlement.feature;
|
||||
logger.info(
|
||||
`Handling invoice.created for in arrear prorated, feature: ${feature.id}`,
|
||||
`Handling invoice.created for in arrear prorated, feature: ${feature.id}`
|
||||
);
|
||||
|
||||
let deletedEntities = await EntityService.list({
|
||||
@@ -93,12 +93,12 @@ const handleInArrearProrated = async ({
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`✨ Handling in arrear prorated, customer ${customer.name}, org: ${org.slug}`,
|
||||
`✨ Handling in arrear prorated, customer ${customer.name}, org: ${org.slug}`
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Deleting entities, feature ${feature.id}, customer ${customer.id}, org ${org.slug}`,
|
||||
deletedEntities,
|
||||
deletedEntities
|
||||
);
|
||||
|
||||
// Get linked cus ents
|
||||
@@ -112,7 +112,7 @@ const handleInArrearProrated = async ({
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Linked cus ent: ${linkedCusEnt.feature_id}, isLinked: ${isLinked}`,
|
||||
`Linked cus ent: ${linkedCusEnt.feature_id}, isLinked: ${isLinked}`
|
||||
);
|
||||
|
||||
// Delete cus ent ids
|
||||
@@ -136,7 +136,7 @@ const handleInArrearProrated = async ({
|
||||
console.log(`Updated ${updated.length} cus ents`);
|
||||
|
||||
logger.info(
|
||||
`Feature: ${feature.id}, customer: ${customer.id}, deleted entities from cus ent`,
|
||||
`Feature: ${feature.id}, customer: ${customer.id}, deleted entities from cus ent`
|
||||
);
|
||||
linkedCusEnt.entities = newEntities;
|
||||
}
|
||||
@@ -150,7 +150,7 @@ const handleInArrearProrated = async ({
|
||||
logger.info(
|
||||
`Feature: ${feature.id}, Deleted ${
|
||||
deletedEntities.length
|
||||
}, entities: ${deletedEntities.map((e) => `${e.id}`).join(", ")}`,
|
||||
}, entities: ${deletedEntities.map((e) => `${e.id}`).join(", ")}`
|
||||
);
|
||||
|
||||
// Increase balance
|
||||
@@ -229,7 +229,7 @@ export const sendUsageAndReset = async ({
|
||||
|
||||
if (billingType == BillingType.UsageInArrear) {
|
||||
logger.info(
|
||||
`✨ Handling usage prices for ${customer.name || customer.id}, org: ${org.slug}`,
|
||||
`✨ Handling usage prices for ${customer.name || customer.id}, org: ${org.slug}`
|
||||
);
|
||||
|
||||
await handleUsagePrices({
|
||||
@@ -306,13 +306,13 @@ export const handleInvoiceCreated = async ({
|
||||
|
||||
if (activeProducts.length == 0) {
|
||||
logger.warn(
|
||||
`Stripe invoice.created -- no active products found (${org.slug})`,
|
||||
`Stripe invoice.created -- no active products found (${org.slug})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let internalEntityId = activeProducts.find(
|
||||
(p) => p.internal_entity_id,
|
||||
(p) => p.internal_entity_id
|
||||
)?.internal_entity_id;
|
||||
|
||||
let features = await FeatureService.list({
|
||||
@@ -330,7 +330,7 @@ export const handleInvoiceCreated = async ({
|
||||
});
|
||||
|
||||
let feature = features.find(
|
||||
(f) => f.internal_id == entity?.internal_feature_id,
|
||||
(f) => f.internal_id == entity?.internal_feature_id
|
||||
);
|
||||
|
||||
let entDetails = "";
|
||||
|
||||
@@ -28,7 +28,7 @@ analyticsRouter.get("/event_names", async (req: any, res: any) =>
|
||||
const { db, org, env, features } = req;
|
||||
const { interval, event_names, customer_id } = req.body;
|
||||
|
||||
const result = await queryWithCache({
|
||||
let result = await queryWithCache({
|
||||
action: CacheType.TopEvents,
|
||||
key: `${org.id}_${env}`,
|
||||
fn: async () => {
|
||||
@@ -40,6 +40,12 @@ analyticsRouter.get("/event_names", async (req: any, res: any) =>
|
||||
},
|
||||
});
|
||||
|
||||
// const topEventNamesRes = await AnalyticsService.getTopEventNames({
|
||||
// req,
|
||||
// });
|
||||
|
||||
// let result = topEventNamesRes?.eventNames;
|
||||
|
||||
let featureIds: string[] = [];
|
||||
let eventNames: string[] = [];
|
||||
|
||||
@@ -73,18 +79,24 @@ analyticsRouter.get("/event_names", async (req: any, res: any) =>
|
||||
const getTopEvents = async ({ req }: { req: ExtendedRequest }) => {
|
||||
const { org, env, features } = req;
|
||||
|
||||
const result = await queryWithCache({
|
||||
action: CacheType.TopEvents,
|
||||
key: `${org.id}_${env}`,
|
||||
fn: async () => {
|
||||
const res = await AnalyticsService.getTopEventNames({
|
||||
req,
|
||||
});
|
||||
// const result = await queryWithCache({
|
||||
// action: CacheType.TopEvents,
|
||||
// key: `${org.id}_${env}`,
|
||||
// fn: async () => {
|
||||
// const res = await AnalyticsService.getTopEventNames({
|
||||
// req,
|
||||
// });
|
||||
|
||||
return res?.eventNames;
|
||||
},
|
||||
// return res?.eventNames;
|
||||
// },
|
||||
// });
|
||||
|
||||
const topEventNamesRes = await AnalyticsService.getTopEventNames({
|
||||
req,
|
||||
});
|
||||
|
||||
let result = topEventNamesRes?.eventNames;
|
||||
|
||||
let featureIds: string[] = [];
|
||||
let eventNames: string[] = [];
|
||||
|
||||
@@ -122,6 +134,7 @@ analyticsRouter.post("/events", async (req: any, res: any) =>
|
||||
let topEvents:
|
||||
| { featureIds: string[]; eventNames: string[] }
|
||||
| undefined = undefined;
|
||||
|
||||
if (!event_names || event_names.length === 0) {
|
||||
topEvents = await getTopEvents({ req });
|
||||
event_names = [...topEvents.eventNames, ...topEvents.featureIds];
|
||||
|
||||
@@ -566,10 +566,14 @@ export class CusProductService {
|
||||
db,
|
||||
productId,
|
||||
internalProductId,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
productId?: string;
|
||||
internalProductId?: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) {
|
||||
if (productId) {
|
||||
let res = await db
|
||||
@@ -577,10 +581,24 @@ export class CusProductService {
|
||||
internal_id: products.internal_id,
|
||||
})
|
||||
.from(products)
|
||||
.where(eq(products.id, productId));
|
||||
.where(
|
||||
and(
|
||||
eq(products.id, productId),
|
||||
eq(products.org_id, orgId),
|
||||
eq(products.env, env)
|
||||
)
|
||||
);
|
||||
|
||||
let internalProductIds = res.map((r) => r.internal_id);
|
||||
|
||||
if (internalProductIds.length > 100) {
|
||||
throw new RecaseError({
|
||||
message: "Something went wrong... please try again later.",
|
||||
code: ErrCode.ProductHasCustomers,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(customerProducts)
|
||||
.where(
|
||||
|
||||
@@ -69,6 +69,12 @@ export const featuresToObject = ({
|
||||
|
||||
// console.log(`Feature ${featureId} list:`, relatedEnts);
|
||||
|
||||
let hasRollovers = relatedEnts.some((e) => notNullish(e.rollovers));
|
||||
let rollovers = hasRollovers
|
||||
? (relatedEnts
|
||||
.flatMap((e) => e.rollovers)
|
||||
.filter(notNullish) as CusRollover[])
|
||||
: undefined;
|
||||
let cusFeature: CusEntResponseV2 = {
|
||||
id: featureId,
|
||||
name: feature.name,
|
||||
@@ -99,9 +105,7 @@ export const featuresToObject = ({
|
||||
}))
|
||||
: undefined,
|
||||
|
||||
rollovers: relatedEnts
|
||||
.flatMap((e) => e.rollovers)
|
||||
.filter(notNullish) as CusRollover[],
|
||||
rollovers,
|
||||
};
|
||||
|
||||
featureObject[featureId] = cusFeature;
|
||||
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { autoCreateEntity } from "@/internal/entities/handlers/handleCreateEntity/autoCreateEntity.js";
|
||||
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
|
||||
import {
|
||||
deleteCusCache,
|
||||
refreshCusCache,
|
||||
} from "../cusCache/updateCachedCus.js";
|
||||
import { getCusWithCache } from "../cusCache/getCusWithCache.js";
|
||||
|
||||
export const getOrCreateCustomer = async ({
|
||||
@@ -107,6 +110,13 @@ export const getOrCreateCustomer = async ({
|
||||
expand,
|
||||
withSubs: true,
|
||||
});
|
||||
|
||||
await deleteCusCache({
|
||||
db,
|
||||
customerId: customer.id!,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error?.data?.code == "23505") {
|
||||
customer = await CusService.getFull({
|
||||
|
||||
@@ -69,8 +69,12 @@ export const handleDeleteProduct = (req: any, res: any) =>
|
||||
|
||||
await CusProductService.deleteByProduct({
|
||||
db,
|
||||
productId: all_versions ? productId : undefined,
|
||||
internalProductId: all_versions ? undefined : product.internal_id,
|
||||
productId: deleteAllVersions ? productId : undefined,
|
||||
internalProductId: deleteAllVersions
|
||||
? undefined
|
||||
: product.internal_id,
|
||||
orgId,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
112
server/src/utils/scriptUtils/getAll/getAllCusProds.ts
Normal file
112
server/src/utils/scriptUtils/getAll/getAllCusProds.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AppEnv, FullCusProduct } from "@autumn/shared";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
let cusProductsQuery = ({
|
||||
lastProductId,
|
||||
internalProductId,
|
||||
pageSize = 250,
|
||||
}: {
|
||||
lastProductId?: string;
|
||||
internalProductId?: string;
|
||||
pageSize?: number;
|
||||
}) => {
|
||||
// const withStatusFilter = () => {
|
||||
// return inStatuses
|
||||
// ? sql`AND cp.status = ANY(ARRAY[${sql.join(
|
||||
// inStatuses.map((status) => sql`${status}`),
|
||||
// sql`, `,
|
||||
// )}])`
|
||||
// : sql``;
|
||||
// };
|
||||
|
||||
|
||||
|
||||
return sql`
|
||||
SELECT
|
||||
cp.*,
|
||||
row_to_json(prod) AS product,
|
||||
|
||||
-- Spread customer_prices fields + add price field
|
||||
COALESCE(
|
||||
json_agg(DISTINCT (
|
||||
to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))
|
||||
)) FILTER (WHERE cpr.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_prices,
|
||||
|
||||
-- Spread customer_entitlements fields + add entitlement and replaceables
|
||||
COALESCE(
|
||||
json_agg(DISTINCT (
|
||||
to_jsonb(ce.*) || jsonb_build_object(
|
||||
'entitlement', (
|
||||
SELECT row_to_json(ent_with_feature)
|
||||
FROM (
|
||||
SELECT e.*, row_to_json(f) AS feature
|
||||
FROM entitlements e
|
||||
JOIN features f ON e.internal_feature_id = f.internal_id
|
||||
WHERE e.id = ce.entitlement_id
|
||||
) AS ent_with_feature
|
||||
),
|
||||
'replaceables', (
|
||||
SELECT COALESCE(
|
||||
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
|
||||
'[]'::json
|
||||
)
|
||||
FROM replaceables r
|
||||
WHERE r.cus_ent_id = ce.id
|
||||
)
|
||||
)
|
||||
)) FILTER (WHERE ce.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS customer_entitlements,
|
||||
|
||||
-- free_trial
|
||||
(
|
||||
SELECT row_to_json(ft)
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = cp.free_trial_id
|
||||
) AS free_trial
|
||||
|
||||
FROM customer_products cp
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
LEFT JOIN customer_prices cpr ON cpr.customer_product_id = cp.id
|
||||
LEFT JOIN prices p ON cpr.price_id = p.id
|
||||
LEFT JOIN customer_entitlements ce ON ce.customer_product_id = cp.id
|
||||
WHERE cp.internal_product_id = ${internalProductId}
|
||||
${lastProductId ? sql`AND cp.id < ${lastProductId}` : sql``}
|
||||
GROUP BY cp.id, prod.*
|
||||
ORDER BY cp.id DESC
|
||||
LIMIT ${pageSize}
|
||||
`;
|
||||
};
|
||||
|
||||
export const getAllFullCusProducts = async ({
|
||||
db,
|
||||
internalProductId,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
internalProductId: string;
|
||||
}) => {
|
||||
let lastProductId = "";
|
||||
let allData: any[] = [];
|
||||
let pageSize = 500;
|
||||
|
||||
while (true) {
|
||||
const data = await db.execute(
|
||||
cusProductsQuery({
|
||||
lastProductId,
|
||||
pageSize,
|
||||
internalProductId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (data.length === 0) break;
|
||||
|
||||
console.log(`Fetched ${data.length} customer products`);
|
||||
allData.push(...data);
|
||||
lastProductId = data[data.length - 1].id as string;
|
||||
}
|
||||
|
||||
return allData as FullCusProduct[];
|
||||
};
|
||||
@@ -81,11 +81,11 @@ describe(`${chalk.yellowBright("basic1: Testing attach free product")}`, () => {
|
||||
|
||||
const entitled = (await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
features.metered1.id
|
||||
)) as any;
|
||||
|
||||
const metered1Balance = entitled.balances.find(
|
||||
(balance: any) => balance.feature_id === features.metered1.id,
|
||||
(balance: any) => balance.feature_id === features.metered1.id
|
||||
);
|
||||
|
||||
expect(entitled.allowed).to.be.true;
|
||||
|
||||
@@ -30,7 +30,7 @@ const checkEntitledOnProduct = async ({
|
||||
AutumnCli.sendEvent({
|
||||
customerId: customerId,
|
||||
eventName: features.metered1.eventName,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const checkEntitledOnProduct = async ({
|
||||
const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -70,7 +70,7 @@ const checkEntitledOnProduct = async ({
|
||||
AutumnCli.sendEvent({
|
||||
customerId: customerId,
|
||||
eventName: features.metered1.eventName,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(batchUpdates2);
|
||||
@@ -103,7 +103,7 @@ const checkEntitledOnProduct = async ({
|
||||
|
||||
const testCase = "others5";
|
||||
describe(`${chalk.yellowBright(
|
||||
"others5: Testing /events and /entitled, for pro, one time top up",
|
||||
"others5: Testing /events and /entitled, for pro, one time top up"
|
||||
)}`, () => {
|
||||
const customerId = testCase;
|
||||
|
||||
@@ -175,7 +175,7 @@ describe(`${chalk.yellowBright(
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright(
|
||||
"others5: Testing /entitled & /events, for pro with overage",
|
||||
"others5: Testing /entitled & /events, for pro with overage"
|
||||
)}`, () => {
|
||||
const customerId = testCase;
|
||||
|
||||
@@ -213,7 +213,7 @@ describe(`${chalk.yellowBright(
|
||||
const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
expect(allowed).to.be.true;
|
||||
@@ -226,12 +226,12 @@ describe(`${chalk.yellowBright(
|
||||
AutumnCli.sendEvent({
|
||||
customerId: customerId,
|
||||
eventName: features.metered1.eventName,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(batchUpdates);
|
||||
await timeout(5000);
|
||||
await timeout(10000);
|
||||
|
||||
const { allowed: allowed2, balanceObj: balanceObj2 }: any =
|
||||
await AutumnCli.entitled(customerId, features.metered1.id, true);
|
||||
|
||||
@@ -54,7 +54,11 @@ export const DeleteProductDialog = ({
|
||||
const handleDelete = async () => {
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
await ProductService.deleteProduct(axiosInstance, product.id);
|
||||
await ProductService.deleteProduct(
|
||||
axiosInstance,
|
||||
product.id,
|
||||
deleteAllVersions
|
||||
);
|
||||
await mutate();
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user