feat: caching customer to speed up check and get customer
This commit is contained in:
@@ -25,6 +25,8 @@ import { UTCDate } from "@date-fns/utc";
|
|||||||
import { type DrizzleCli, initDrizzle } from "./db/initDrizzle.js";
|
import { type DrizzleCli, initDrizzle } from "./db/initDrizzle.js";
|
||||||
|
|
||||||
import { CusPriceService } from "./internal/customers/cusProducts/cusPrices/CusPriceService.js";
|
import { CusPriceService } from "./internal/customers/cusProducts/cusPrices/CusPriceService.js";
|
||||||
|
import { CusService } from "./internal/customers/CusService.js";
|
||||||
|
import { refreshCusCache } from "./internal/customers/cusCache/updateCachedCus.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -208,6 +210,19 @@ const resetCustomerEntitlement = async ({
|
|||||||
format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss")
|
format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss")
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let customer = await CusService.getByInternalId({
|
||||||
|
db,
|
||||||
|
internalId: cusEnt.internal_customer_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (customer) {
|
||||||
|
await refreshCusCache({
|
||||||
|
customerId: customer.id!,
|
||||||
|
orgId: customer.org_id,
|
||||||
|
env: customer.env,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(
|
console.log(
|
||||||
`Failed to reset ${cusEnt.id} | ${cusEnt.customer_id} | ${cusEnt.feature_id}, error: ${error}`
|
`Failed to reset ${cusEnt.id} | ${cusEnt.customer_id} | ${cusEnt.feature_id}, error: ${error}`
|
||||||
|
|||||||
80
server/src/external/stripe/stripeWebhooks.ts
vendored
80
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -3,7 +3,7 @@ import stripe, { Stripe } from "stripe";
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||||
import { AuthType, LoggerAction, Organization } from "@autumn/shared";
|
import { AppEnv, AuthType, LoggerAction, Organization } from "@autumn/shared";
|
||||||
|
|
||||||
import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js";
|
import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js";
|
||||||
import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js";
|
import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js";
|
||||||
@@ -18,6 +18,9 @@ import { handleSubscriptionScheduleCanceled } from "./webhookHandlers/handleSubS
|
|||||||
import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js";
|
import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js";
|
||||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||||
import { createStripeCli } from "./utils.js";
|
import { createStripeCli } from "./utils.js";
|
||||||
|
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||||
|
import { CusService } from "@/internal/customers/CusService.js";
|
||||||
|
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
|
|
||||||
export const stripeWebhookRouter: Router = express.Router();
|
export const stripeWebhookRouter: Router = express.Router();
|
||||||
|
|
||||||
@@ -238,7 +241,82 @@ stripeWebhookRouter.post(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handleStripeWebhookRefresh({
|
||||||
|
eventType: event.type,
|
||||||
|
data: event.data,
|
||||||
|
db,
|
||||||
|
org,
|
||||||
|
env,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Stripe webhook, error refreshing cache!`, { error });
|
||||||
|
}
|
||||||
|
|
||||||
// DO NOT DELETE -- RESPONSIBLE FOR SENDING SUCCESSFUL RESPONSE TO STRIPE...
|
// DO NOT DELETE -- RESPONSIBLE FOR SENDING SUCCESSFUL RESPONSE TO STRIPE...
|
||||||
response.status(200).send();
|
response.status(200).send();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const coreEvents = [
|
||||||
|
"customer.subscription.created",
|
||||||
|
"customer.subscription.updated",
|
||||||
|
"customer.subscription.deleted",
|
||||||
|
"invoice.paid",
|
||||||
|
"invoice.created",
|
||||||
|
"invoice.finalized",
|
||||||
|
"subscription_schedule.canceled",
|
||||||
|
"checkout.session.completed",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const handleStripeWebhookRefresh = async ({
|
||||||
|
eventType,
|
||||||
|
data,
|
||||||
|
db,
|
||||||
|
org,
|
||||||
|
env,
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
eventType: string;
|
||||||
|
data: any;
|
||||||
|
db: DrizzleCli;
|
||||||
|
org: Organization;
|
||||||
|
env: AppEnv;
|
||||||
|
logger: any;
|
||||||
|
}) => {
|
||||||
|
if (coreEvents.includes(eventType)) {
|
||||||
|
let stripeCusId = data.object.customer;
|
||||||
|
if (!stripeCusId) {
|
||||||
|
logger.warn(
|
||||||
|
`stripe webhook cache refresh, object doesn't contain customer id`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
eventType,
|
||||||
|
object: data.object,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cus = await CusService.getByStripeId({
|
||||||
|
db,
|
||||||
|
stripeId: stripeCusId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cus) {
|
||||||
|
logger.warn(
|
||||||
|
`Searched for customer by stripe id, but not found: ${stripeCusId}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// logger.info(`Deleting cache for customer ${cus.id}`);
|
||||||
|
await deleteCusCache({
|
||||||
|
customerId: cus.id!,
|
||||||
|
orgId: org.id,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const handleCheckoutSessionCompleted = async ({
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Handling checkout.completed: autumn metadata:",
|
"Handling checkout.completed: autumn metadata:",
|
||||||
checkoutSession.metadata?.autumn_metadata_id,
|
checkoutSession.metadata?.autumn_metadata_id
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkoutSub =
|
const checkoutSub =
|
||||||
@@ -139,7 +139,7 @@ export const handleCheckoutSessionCompleted = async ({
|
|||||||
attachParams,
|
attachParams,
|
||||||
invoiceId,
|
invoiceId,
|
||||||
logger,
|
logger,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { internalFeatureRouter } from "../features/internalFeatureRouter.js";
|
|||||||
import { analyticsRouter } from "../analytics/analyticsRouter.js";
|
import { analyticsRouter } from "../analytics/analyticsRouter.js";
|
||||||
import { handleConnectStripe } from "../orgs/handlers/handleConnectStripe.js";
|
import { handleConnectStripe } from "../orgs/handlers/handleConnectStripe.js";
|
||||||
import { handleDeleteStripe } from "../orgs/handlers/handleDeleteStripe.js";
|
import { handleDeleteStripe } from "../orgs/handlers/handleDeleteStripe.js";
|
||||||
import { refreshCusCache } from "../customers/cusCache/updateCachedCus.js";
|
|
||||||
import { refreshCacheMiddleware } from "@/middleware/refreshCacheMiddleware.js";
|
import { refreshCacheMiddleware } from "@/middleware/refreshCacheMiddleware.js";
|
||||||
|
|
||||||
const apiRouter: Router = Router();
|
const apiRouter: Router = Router();
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export const getEntityResponse = async ({
|
|||||||
withAutumnId = false,
|
withAutumnId = false,
|
||||||
apiVersion,
|
apiVersion,
|
||||||
features,
|
features,
|
||||||
|
logger,
|
||||||
|
skipCache = false,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
entityIds: string[];
|
entityIds: string[];
|
||||||
@@ -46,6 +48,8 @@ export const getEntityResponse = async ({
|
|||||||
withAutumnId?: boolean;
|
withAutumnId?: boolean;
|
||||||
apiVersion: number;
|
apiVersion: number;
|
||||||
features: Feature[];
|
features: Feature[];
|
||||||
|
logger: any;
|
||||||
|
skipCache?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
// let customer = await CusService.getFull({
|
// let customer = await CusService.getFull({
|
||||||
// db,
|
// db,
|
||||||
@@ -63,6 +67,8 @@ export const getEntityResponse = async ({
|
|||||||
env,
|
env,
|
||||||
expand,
|
expand,
|
||||||
entityId,
|
entityId,
|
||||||
|
logger,
|
||||||
|
skipCache,
|
||||||
});
|
});
|
||||||
|
|
||||||
let entities = customer.entities.filter((e: Entity) =>
|
let entities = customer.entities.filter((e: Entity) =>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const handleGetEntity = async (req: any, res: any) =>
|
|||||||
entityId,
|
entityId,
|
||||||
apiVersion,
|
apiVersion,
|
||||||
features,
|
features,
|
||||||
|
logger,
|
||||||
});
|
});
|
||||||
// const end = performance.now();
|
// const end = performance.now();
|
||||||
// logger.info(`getEntityResponse took ${(end - start).toFixed(2)}ms`);
|
// logger.info(`getEntityResponse took ${(end - start).toFixed(2)}ms`);
|
||||||
@@ -53,7 +54,7 @@ export const handleGetEntity = async (req: any, res: any) =>
|
|||||||
logger,
|
logger,
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const getFeatureAndCreditSystems = ({
|
|||||||
const { features } = req;
|
const { features } = req;
|
||||||
|
|
||||||
const feature: Feature | undefined = features.find(
|
const feature: Feature | undefined = features.find(
|
||||||
(feature: Feature) => feature.id === featureId,
|
(feature: Feature) => feature.id === featureId
|
||||||
);
|
);
|
||||||
|
|
||||||
const creditSystems = getCreditSystemsFromFeature({
|
const creditSystems = getCreditSystemsFromFeature({
|
||||||
@@ -59,6 +59,7 @@ export const getCheckData = async ({ req }: { req: any }) => {
|
|||||||
inStatuses,
|
inStatuses,
|
||||||
entityId: entity_id,
|
entityId: entity_id,
|
||||||
entityData: req.body.entity_data,
|
entityData: req.body.entity_data,
|
||||||
|
withCache: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
@@ -82,7 +83,7 @@ export const getCheckData = async ({ req }: { req: any }) => {
|
|||||||
cusEnt,
|
cusEnt,
|
||||||
entity: customer.entity!,
|
entity: customer.entity!,
|
||||||
features: allFeatures,
|
features: allFeatures,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { handleAttachErrors } from "./attachUtils/handleAttachErrors.js";
|
|||||||
import { checkStripeConnections, createStripePrices } from "./attachRouter.js";
|
import { checkStripeConnections, createStripePrices } from "./attachRouter.js";
|
||||||
import { insertCustomItems } from "./attachUtils/insertCustomItems.js";
|
import { insertCustomItems } from "./attachUtils/insertCustomItems.js";
|
||||||
import { runAttachFunction } from "./attachUtils/getAttachFunction.js";
|
import { runAttachFunction } from "./attachUtils/getAttachFunction.js";
|
||||||
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
|
|
||||||
|
|
||||||
export const handleAttach = async (req: any, res: any) =>
|
export const handleAttach = async (req: any, res: any) =>
|
||||||
routeHandler({
|
routeHandler({
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const buildBaseCusCacheKey = ({
|
|||||||
env: string;
|
env: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (entityId) {
|
if (entityId) {
|
||||||
return `customer:${idOrInternalId}_${orgId}_${env}:${entityId}`;
|
return `customer:${idOrInternalId}_${orgId}_${env}:entity_${entityId}`;
|
||||||
} else {
|
} else {
|
||||||
return `customer:${idOrInternalId}_${orgId}_${env}`;
|
return `customer:${idOrInternalId}_${orgId}_${env}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { AppEnv, CusExpand, EntityExpand, FullCustomer } from "@autumn/shared";
|
import { AppEnv, CusExpand, EntityExpand, FullCustomer } from "@autumn/shared";
|
||||||
import { ACTIVE_STATUSES } from "../cusProducts/CusProductService.js";
|
import { RELEVANT_STATUSES } from "../cusProducts/CusProductService.js";
|
||||||
import { db } from "@/db/initDrizzle.js";
|
import { db } from "@/db/initDrizzle.js";
|
||||||
import { CusService } from "../CusService.js";
|
import { CusService } from "../CusService.js";
|
||||||
import { buildBaseCusCacheKey } from "./cusCacheUtils.js";
|
import { buildBaseCusCacheKey } from "./cusCacheUtils.js";
|
||||||
import { initUpstash } from "./upstashUtils.js";
|
import { initUpstash } from "./upstashUtils.js";
|
||||||
|
import { notNullish } from "@/utils/genUtils.js";
|
||||||
|
|
||||||
export const getCusWithCache = async ({
|
export const getCusWithCache = async ({
|
||||||
idOrInternalId,
|
idOrInternalId,
|
||||||
@@ -14,6 +15,7 @@ export const getCusWithCache = async ({
|
|||||||
allowNotFound = true,
|
allowNotFound = true,
|
||||||
skipCache = false,
|
skipCache = false,
|
||||||
skipGet = false,
|
skipGet = false,
|
||||||
|
logger,
|
||||||
}: {
|
}: {
|
||||||
idOrInternalId: string;
|
idOrInternalId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -25,33 +27,38 @@ export const getCusWithCache = async ({
|
|||||||
allowNotFound?: boolean;
|
allowNotFound?: boolean;
|
||||||
skipCache?: boolean;
|
skipCache?: boolean;
|
||||||
skipGet?: boolean;
|
skipGet?: boolean;
|
||||||
|
logger: any;
|
||||||
}): Promise<FullCustomer> => {
|
}): Promise<FullCustomer> => {
|
||||||
const statuses = ACTIVE_STATUSES;
|
const statuses = RELEVANT_STATUSES;
|
||||||
const withEntities = true;
|
const withEntities = true;
|
||||||
const withSubs = true;
|
const withSubs = true;
|
||||||
|
|
||||||
const upstash = await initUpstash();
|
const upstash = await initUpstash();
|
||||||
if (!upstash) skipCache = true;
|
if (!upstash) skipCache = true;
|
||||||
|
|
||||||
const baseKey = buildBaseCusCacheKey({
|
let cacheKey = buildBaseCusCacheKey({
|
||||||
idOrInternalId,
|
idOrInternalId,
|
||||||
orgId,
|
orgId,
|
||||||
env,
|
env,
|
||||||
entityId,
|
entityId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cacheKey = `${baseKey}:${expand.join(",")}`;
|
if (expand.length > 0) {
|
||||||
|
cacheKey = `${cacheKey}:expand_${expand.join(",")}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (!skipCache && !skipGet) {
|
if (!skipCache && !skipGet) {
|
||||||
try {
|
try {
|
||||||
const cached = await upstash!.get(cacheKey);
|
const cached = await upstash!.get(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
console.log(`Cache hit: ${cacheKey}`);
|
logger.info(`Cache hit: ${cacheKey}`);
|
||||||
|
logger.info("Cached:", cached);
|
||||||
return cached as FullCustomer;
|
return cached as FullCustomer;
|
||||||
} else {
|
} else {
|
||||||
console.log(`Cache miss: ${cacheKey}`);
|
// logger.info(`Cache miss: ${cacheKey}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
logger.error(`Failed to get cache: ${cacheKey}`, { error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +77,12 @@ export const getCusWithCache = async ({
|
|||||||
|
|
||||||
if (entityId && !customer.entity) skipCache = true;
|
if (entityId && !customer.entity) skipCache = true;
|
||||||
|
|
||||||
if (!skipCache) {
|
if (!skipCache && notNullish(customer)) {
|
||||||
try {
|
try {
|
||||||
await upstash!.set(cacheKey, customer);
|
await upstash!.set(cacheKey, customer);
|
||||||
await upstash!.expire(cacheKey, 1000); // Expire after 60 seconds
|
await upstash!.expire(cacheKey, 300); // Expire after 5 minutes...
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
logger.error(`Failed to set cache: ${cacheKey}`, { error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,20 +24,56 @@ export const refreshCusCache = async ({
|
|||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
|
|
||||||
const list = await upstash.keys(`${baseKey}:*`);
|
const list = await upstash.keys(`${baseKey}*`);
|
||||||
|
|
||||||
for (const key of list) {
|
for (const key of list) {
|
||||||
const keyName = key;
|
const keyName = key;
|
||||||
let params = keyName.split(":");
|
let params = keyName.split(":");
|
||||||
let expand = params ? params[params.length - 1].split(",") : [];
|
let expandParam = params.find((p) => p.startsWith("expand_"));
|
||||||
|
let expand = expandParam
|
||||||
|
? expandParam.replace("expand_", "").split(",")
|
||||||
|
: [];
|
||||||
|
|
||||||
|
let entityIdParam = params.find((p) => p.startsWith("entity_"));
|
||||||
|
let entityId = entityIdParam
|
||||||
|
? entityIdParam.replace("entity_", "")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
await getCusWithCache({
|
await getCusWithCache({
|
||||||
idOrInternalId: customerId,
|
idOrInternalId: customerId,
|
||||||
orgId,
|
orgId,
|
||||||
env,
|
env,
|
||||||
expand: expand as CusExpand[],
|
expand: expand as CusExpand[],
|
||||||
|
entityId,
|
||||||
skipGet: true,
|
skipGet: true,
|
||||||
|
logger: console,
|
||||||
});
|
});
|
||||||
console.log(`updated cache key: ${keyName}`);
|
console.log(`updated cache key: ${keyName}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteCusCache = async ({
|
||||||
|
customerId,
|
||||||
|
orgId,
|
||||||
|
env,
|
||||||
|
}: {
|
||||||
|
customerId: string;
|
||||||
|
orgId: string;
|
||||||
|
env: AppEnv;
|
||||||
|
}) => {
|
||||||
|
const upstash = await initUpstash();
|
||||||
|
if (!upstash) return;
|
||||||
|
|
||||||
|
const baseKey = buildBaseCusCacheKey({
|
||||||
|
idOrInternalId: customerId,
|
||||||
|
orgId,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = await upstash.keys(`${baseKey}*`);
|
||||||
|
|
||||||
|
for (const key of list) {
|
||||||
|
console.log("Deleting cache for key:", key);
|
||||||
|
await upstash.del(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -505,9 +505,11 @@ export const isTrialing = (cusProduct: FullCusProduct) => {
|
|||||||
export const getMainCusProduct = async ({
|
export const getMainCusProduct = async ({
|
||||||
db,
|
db,
|
||||||
internalCustomerId,
|
internalCustomerId,
|
||||||
|
productGroup,
|
||||||
}: {
|
}: {
|
||||||
db: DrizzleCli;
|
db: DrizzleCli;
|
||||||
internalCustomerId: string;
|
internalCustomerId: string;
|
||||||
|
productGroup?: string;
|
||||||
}) => {
|
}) => {
|
||||||
let cusProducts = await CusProductService.list({
|
let cusProducts = await CusProductService.list({
|
||||||
db,
|
db,
|
||||||
@@ -520,7 +522,9 @@ export const getMainCusProduct = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mainCusProduct = cusProducts.find(
|
let mainCusProduct = cusProducts.find(
|
||||||
(cusProduct: FullCusProduct) => !cusProduct.product.is_add_on
|
(cusProduct: FullCusProduct) =>
|
||||||
|
!cusProduct.product.is_add_on &&
|
||||||
|
(productGroup ? cusProduct.product.group === productGroup : true)
|
||||||
);
|
);
|
||||||
|
|
||||||
return mainCusProduct;
|
return mainCusProduct;
|
||||||
|
|||||||
@@ -58,16 +58,16 @@ cusRouter.post("/:customer_id", handleUpdateCustomer);
|
|||||||
|
|
||||||
// Update customer entitlement directly
|
// Update customer entitlement directly
|
||||||
cusRouter.post(
|
cusRouter.post(
|
||||||
"/customer_entitlements/:customer_entitlement_id",
|
"/:customer_id/entitlements/:customer_entitlement_id",
|
||||||
handleUpdateEntitlement
|
handleUpdateEntitlement
|
||||||
);
|
);
|
||||||
|
|
||||||
cusRouter.post("/:customer_id/balances", handleUpdateBalances);
|
cusRouter.post("/:customer_id/balances", handleUpdateBalances);
|
||||||
|
|
||||||
cusRouter.post(
|
// cusRouter.post(
|
||||||
"/customer_products/:customer_product_id",
|
// "/customer_products/:customer_product_id",
|
||||||
handleCusProductExpired
|
// handleCusProductExpired
|
||||||
);
|
// );
|
||||||
|
|
||||||
cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => {
|
cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { notNullish, nullish } from "@/utils/genUtils.js";
|
|||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
import { processInvoice } from "@/internal/invoices/InvoiceService.js";
|
import { processInvoice } from "@/internal/invoices/InvoiceService.js";
|
||||||
|
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
|
||||||
|
|
||||||
export const updateCustomerDetails = async ({
|
export const updateCustomerDetails = async ({
|
||||||
db,
|
db,
|
||||||
@@ -54,6 +55,12 @@ export const updateCustomerDetails = async ({
|
|||||||
update: updates,
|
update: updates,
|
||||||
});
|
});
|
||||||
customer = { ...customer, ...updates };
|
customer = { ...customer, ...updates };
|
||||||
|
|
||||||
|
await refreshCusCache({
|
||||||
|
customerId: customer.id!,
|
||||||
|
orgId: customer.org_id,
|
||||||
|
env: customer.env,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return customer;
|
return customer;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
|
|
||||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||||
import { autoCreateEntity } from "@/internal/entities/handlers/handleCreateEntity/autoCreateEntity.js";
|
import { autoCreateEntity } from "@/internal/entities/handlers/handleCreateEntity/autoCreateEntity.js";
|
||||||
|
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
|
||||||
|
import { getCusWithCache } from "../cusCache/getCusWithCache.js";
|
||||||
|
|
||||||
export const getOrCreateCustomer = async ({
|
export const getOrCreateCustomer = async ({
|
||||||
req,
|
req,
|
||||||
@@ -33,6 +35,7 @@ export const getOrCreateCustomer = async ({
|
|||||||
// Entity stuff
|
// Entity stuff
|
||||||
entityId,
|
entityId,
|
||||||
entityData,
|
entityData,
|
||||||
|
withCache = false,
|
||||||
}: {
|
}: {
|
||||||
req: ExtendedRequest;
|
req: ExtendedRequest;
|
||||||
customerId: string;
|
customerId: string;
|
||||||
@@ -43,6 +46,7 @@ export const getOrCreateCustomer = async ({
|
|||||||
expand?: CusExpand[];
|
expand?: CusExpand[];
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
entityData?: EntityData;
|
entityData?: EntityData;
|
||||||
|
withCache?: boolean;
|
||||||
}): Promise<FullCustomer> => {
|
}): Promise<FullCustomer> => {
|
||||||
let customer;
|
let customer;
|
||||||
|
|
||||||
@@ -53,18 +57,29 @@ export const getOrCreateCustomer = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!skipGet) {
|
if (!skipGet) {
|
||||||
customer = await CusService.getFull({
|
if (withCache) {
|
||||||
db,
|
customer = await getCusWithCache({
|
||||||
idOrInternalId: customerId,
|
idOrInternalId: customerId,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
env,
|
env,
|
||||||
inStatuses,
|
entityId,
|
||||||
withEntities,
|
expand: expand as CusExpand[],
|
||||||
entityId,
|
logger,
|
||||||
expand,
|
});
|
||||||
allowNotFound: true,
|
} else {
|
||||||
withSubs: true,
|
customer = await CusService.getFull({
|
||||||
});
|
db,
|
||||||
|
idOrInternalId: customerId,
|
||||||
|
orgId: org.id,
|
||||||
|
env,
|
||||||
|
inStatuses,
|
||||||
|
withEntities,
|
||||||
|
entityId,
|
||||||
|
expand,
|
||||||
|
allowNotFound: true,
|
||||||
|
withSubs: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
@@ -134,6 +149,12 @@ export const getOrCreateCustomer = async ({
|
|||||||
|
|
||||||
customer.entities = [...(customer.entities || []), newEntity];
|
customer.entities = [...(customer.entities || []), newEntity];
|
||||||
customer.entity = newEntity;
|
customer.entity = newEntity;
|
||||||
|
|
||||||
|
await refreshCusCache({
|
||||||
|
customerId: customer.id!,
|
||||||
|
orgId: customer.org_id,
|
||||||
|
env: customer.env,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return customer as FullCustomer;
|
return customer as FullCustomer;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
|||||||
import { routeHandler } from "@/utils/routerUtils.js";
|
import { routeHandler } from "@/utils/routerUtils.js";
|
||||||
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
|
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
|
|
||||||
|
|
||||||
export const deleteCusById = async ({
|
export const deleteCusById = async ({
|
||||||
db,
|
db,
|
||||||
@@ -91,12 +90,6 @@ export const handleDeleteCustomer = async (req: any, res: any) =>
|
|||||||
deleteInStripe: req.query.delete_in_stripe === "true",
|
deleteInStripe: req.query.delete_in_stripe === "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
await refreshCusCache({
|
|
||||||
customerId: req.params.customer_id,
|
|
||||||
orgId: org.id,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(data);
|
res.status(200).json(data);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ export const handleGetCustomer = async (req: any, res: any) =>
|
|||||||
env,
|
env,
|
||||||
expand: expandArray,
|
expand: expandArray,
|
||||||
allowNotFound: true,
|
allowNotFound: true,
|
||||||
|
logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
// const customer = await CusService.getFull({
|
// const customer = await CusService.getFull({
|
||||||
// db,
|
// db,
|
||||||
// idOrInternalId: customerId,
|
// idOrInternalId: customerId,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export const handlePostCustomerRequest = async (req: any, res: any) => {
|
|||||||
|
|
||||||
entityId: data.entity_id,
|
entityId: data.entity_id,
|
||||||
entityData: data.entity_data,
|
entityData: data.entity_data,
|
||||||
|
withCache: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let cusDetails = await getCustomerDetails({
|
let cusDetails = await getCustomerDetails({
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||||
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||||
import { notNullish } from "@/utils/genUtils.js";
|
import { notNullish } from "@/utils/genUtils.js";
|
||||||
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
|
|
||||||
|
|
||||||
const getCusFeaturesAndOrg = async (req: any, customerId: string) => {
|
const getCusFeaturesAndOrg = async (req: any, customerId: string) => {
|
||||||
// 1. Get customer
|
// 1. Get customer
|
||||||
@@ -265,12 +264,6 @@ export const handleUpdateBalances = async (req: any, res: any) => {
|
|||||||
}
|
}
|
||||||
await Promise.all(batchDeduct);
|
await Promise.all(batchDeduct);
|
||||||
|
|
||||||
await refreshCusCache({
|
|
||||||
customerId: cusId,
|
|
||||||
orgId: org.id,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(" ✅ Successfully updated balances");
|
logger.info(" ✅ Successfully updated balances");
|
||||||
|
|
||||||
res.status(200).json({ success: true });
|
res.status(200).json({ success: true });
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { parseCusExpand } from "../cusUtils/cusUtils.js";
|
|||||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||||
import { ExtendedResponse } from "@/utils/models/Request.js";
|
import { ExtendedResponse } from "@/utils/models/Request.js";
|
||||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||||
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
|
|
||||||
|
|
||||||
export const handleUpdateCustomer = async (req: any, res: any) =>
|
export const handleUpdateCustomer = async (req: any, res: any) =>
|
||||||
routeHandler({
|
routeHandler({
|
||||||
@@ -135,12 +134,6 @@ export const handleUpdateCustomer = async (req: any, res: any) =>
|
|||||||
reqApiVersion: req.apiVersion,
|
reqApiVersion: req.apiVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
await refreshCusCache({
|
|
||||||
customerId,
|
|
||||||
orgId,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(customerDetails);
|
res.status(200).json(customerDetails);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js";
|
|||||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
import { CusProductService } from "../cusProducts/CusProductService.js";
|
import { CusProductService } from "../cusProducts/CusProductService.js";
|
||||||
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
|
|
||||||
|
|
||||||
const getCusOrgAndCusPrice = async ({
|
const getCusOrgAndCusPrice = async ({
|
||||||
db,
|
db,
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ export const createEntities = async ({
|
|||||||
withAutumnId,
|
withAutumnId,
|
||||||
apiVersion: apiVersion!,
|
apiVersion: apiVersion!,
|
||||||
features,
|
features,
|
||||||
|
logger,
|
||||||
|
skipCache: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return entities;
|
return entities;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
|
|||||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||||
import { migrationToAttachParams } from "../migrationUtils/migrationToAttachParams.js";
|
import { migrationToAttachParams } from "../migrationUtils/migrationToAttachParams.js";
|
||||||
import { runMigrationAttach } from "../migrationUtils/runMigrationAttach.js";
|
import { runMigrationAttach } from "../migrationUtils/runMigrationAttach.js";
|
||||||
|
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||||
|
|
||||||
export const migrateCustomer = async ({
|
export const migrateCustomer = async ({
|
||||||
db,
|
db,
|
||||||
@@ -62,7 +63,7 @@ export const migrateCustomer = async ({
|
|||||||
|
|
||||||
const cusProducts = fullCus.customer_products;
|
const cusProducts = fullCus.customer_products;
|
||||||
const filteredCusProducts = cusProducts.filter(
|
const filteredCusProducts = cusProducts.filter(
|
||||||
(cp: FullCusProduct) => cp.product.internal_id == fromProduct.internal_id,
|
(cp: FullCusProduct) => cp.product.internal_id == fromProduct.internal_id
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const cusProduct of filteredCusProducts) {
|
for (const cusProduct of filteredCusProducts) {
|
||||||
@@ -78,12 +79,18 @@ export const migrateCustomer = async ({
|
|||||||
req,
|
req,
|
||||||
attachParams,
|
attachParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await deleteCusCache({
|
||||||
|
customerId,
|
||||||
|
orgId,
|
||||||
|
env,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Migration failed for customer ${customerId}, job id: ${migrationJob?.id}`,
|
`Migration failed for customer ${customerId}, job id: ${migrationJob?.id}`
|
||||||
);
|
);
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,117 @@
|
|||||||
const urls = [
|
import {
|
||||||
|
deleteCusCache,
|
||||||
|
refreshCusCache,
|
||||||
|
} from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||||
|
|
||||||
|
const cusPrefixedUrls = [
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/customers/:customer_id",
|
url: "/customers/:customer_id",
|
||||||
|
type: "delete",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/customers/:customer_id",
|
url: "/customers/:customer_id",
|
||||||
|
type: "delete",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/customers/:customer_id/balances",
|
url: "/customers/:customer_id/balances",
|
||||||
|
type: "delete",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/customers/customer_entitlements/:customer_entitlement_id",
|
url: "/customers/:customer_id/entitlements/:customer_entitlement_id",
|
||||||
|
type: "delete",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/customers/:customer_id/balances",
|
url: "/customers/:customer_id/balances",
|
||||||
},
|
type: "delete",
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
url: "/customers/:customer_id/coupons/:coupon_id",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/customers/:customer_id/entities",
|
url: "/customers/:customer_id/entities",
|
||||||
|
type: "delete",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/customers/:customer_id/transfer_product",
|
url: "/customers/:customer_id/transfer_product",
|
||||||
|
type: "delete",
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const matchesCusPrefixedUrl = (url: string, method: string) => {
|
||||||
|
return cusPrefixedUrls.find((urlObj) => {
|
||||||
|
// Check if method matches
|
||||||
|
if (urlObj.method !== method) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexPattern = urlObj.url
|
||||||
|
.replace(/:[^/]+/g, "([^/]+)") // Replace :param with capturing group
|
||||||
|
.replace(/\//g, "\\/"); // Escape forward slashes
|
||||||
|
|
||||||
|
const regex = new RegExp(`^${regexPattern}$`);
|
||||||
|
return regex.test(url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const coreUrls = [
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/attach",
|
url: "/attach",
|
||||||
|
type: "delete",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
url: "/cancel",
|
||||||
|
type: "delete",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleRefreshCache = async (req: any, res: any) => {
|
||||||
|
const { logger } = req;
|
||||||
|
const pathMatch = matchesCusPrefixedUrl(
|
||||||
|
req.originalUrl.replace("/v1", ""),
|
||||||
|
req.method
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pathMatch) {
|
||||||
|
const customerId = req.params.customer_id || req.params.customerId;
|
||||||
|
logger.info(
|
||||||
|
`Clearing cache for customer ${customerId}, url: ${req.originalUrl}`
|
||||||
|
);
|
||||||
|
await deleteCusCache({
|
||||||
|
customerId,
|
||||||
|
orgId: req.org.id,
|
||||||
|
env: req.env,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const coreMatch = coreUrls.find(
|
||||||
|
(urlObj) =>
|
||||||
|
urlObj.url === req.originalUrl.replace("/v1", "") &&
|
||||||
|
urlObj.method === req.method
|
||||||
|
);
|
||||||
|
|
||||||
|
if (coreMatch && req.body.customer_id) {
|
||||||
|
logger.info(`Clearing cache for core url ${req.originalUrl}`);
|
||||||
|
await deleteCusCache({
|
||||||
|
customerId: req.body.customer_id,
|
||||||
|
orgId: req.org.id,
|
||||||
|
env: req.env,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const refreshCacheMiddleware = async (req: any, res: any, next: any) => {
|
export const refreshCacheMiddleware = async (req: any, res: any, next: any) => {
|
||||||
res.on("finish", async () => {
|
// Replace res.send...
|
||||||
console.log("URL:", req.originalUrl);
|
const originalSend = res.send;
|
||||||
console.log("METHOD:", req.method);
|
res.send = async (body: any) => {
|
||||||
console.log("--------------------------------");
|
await handleRefreshCache(req, res);
|
||||||
});
|
await originalSend.call(res, body);
|
||||||
|
};
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { initCustomer } from "tests/utils/init.js";
|
|||||||
|
|
||||||
// UNCOMMENT FROM HERE
|
// UNCOMMENT FROM HERE
|
||||||
describe(`${chalk.yellowBright(
|
describe(`${chalk.yellowBright(
|
||||||
"referrals2: Testing referrals (immediate redemption)",
|
"referrals2: Testing referrals (immediate redemption)"
|
||||||
)}`, () => {
|
)}`, () => {
|
||||||
let mainCustomerId = "main-referral-2";
|
let mainCustomerId = "main-referral-2";
|
||||||
let redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"];
|
let redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"];
|
||||||
@@ -53,7 +53,7 @@ describe(`${chalk.yellowBright(
|
|||||||
org: this.org,
|
org: this.org,
|
||||||
env: this.env,
|
env: this.env,
|
||||||
attachPm: true,
|
attachPm: true,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,21 +94,9 @@ describe(`${chalk.yellowBright(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try redeem for redeemer1 again
|
|
||||||
// try {
|
|
||||||
// let redemption1 = await autumn.referrals.redeem({
|
|
||||||
// customerId: redeemers[0],
|
|
||||||
// code: referralCode.code,
|
|
||||||
// });
|
|
||||||
// assert.fail("Should not be able to redeem again");
|
|
||||||
// } catch (error) {
|
|
||||||
// assert.instanceOf(error, AutumnError);
|
|
||||||
// assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Check stripe customer
|
// Check stripe customer
|
||||||
let stripeCus = (await stripeCli.customers.retrieve(
|
let stripeCus = (await stripeCli.customers.retrieve(
|
||||||
mainCustomer.processor?.id,
|
mainCustomer.processor?.id
|
||||||
)) as Stripe.Customer;
|
)) as Stripe.Customer;
|
||||||
|
|
||||||
assert.notEqual(stripeCus.discount, null);
|
assert.notEqual(stripeCus.discount, null);
|
||||||
@@ -123,11 +111,12 @@ describe(`${chalk.yellowBright(
|
|||||||
|
|
||||||
await timeout(3000);
|
await timeout(3000);
|
||||||
|
|
||||||
curTime = addDays(addDays(curTime, 7), 1);
|
curTime = addDays(addDays(curTime, 7), 4);
|
||||||
await advanceTestClock({
|
await advanceTestClock({
|
||||||
testClockId,
|
testClockId,
|
||||||
advanceTo: curTime.getTime(),
|
advanceTo: curTime.getTime(),
|
||||||
stripeCli,
|
stripeCli,
|
||||||
|
waitForSeconds: 30,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1. Get invoice
|
// 1. Get invoice
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
|||||||
|
|
||||||
// UNCOMMENT FROM HERE
|
// UNCOMMENT FROM HERE
|
||||||
describe(`${chalk.yellowBright(
|
describe(`${chalk.yellowBright(
|
||||||
"referrals4: Testing free product referrals with trial",
|
"referrals4: Testing free product referrals with trial"
|
||||||
)}`, () => {
|
)}`, () => {
|
||||||
let mainCustomerId = "main-referral-4";
|
let mainCustomerId = "main-referral-4";
|
||||||
// let redeemers = ["referral4-r1", "referral4-r2"];
|
// let redeemers = ["referral4-r1", "referral4-r2"];
|
||||||
@@ -95,12 +95,13 @@ describe(`${chalk.yellowBright(
|
|||||||
it("should be triggered after trial ends", async function () {
|
it("should be triggered after trial ends", async function () {
|
||||||
let advanceTo = addHours(
|
let advanceTo = addHours(
|
||||||
addDays(new Date(), 7),
|
addDays(new Date(), 7),
|
||||||
hoursToFinalizeInvoice,
|
hoursToFinalizeInvoice
|
||||||
).getTime();
|
).getTime();
|
||||||
await advanceTestClock({
|
await advanceTestClock({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
testClockId,
|
testClockId,
|
||||||
advanceTo,
|
advanceTo,
|
||||||
|
waitForSeconds: 30,
|
||||||
});
|
});
|
||||||
|
|
||||||
let redemption = await autumn.redemptions.get(redemptions[0].id);
|
let redemption = await autumn.redemptions.get(redemptions[0].id);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { timeout } from "@/utils/genUtils.js";
|
|||||||
|
|
||||||
const testCase = "basic5";
|
const testCase = "basic5";
|
||||||
describe(`${chalk.yellowBright(
|
describe(`${chalk.yellowBright(
|
||||||
"basic5: Testing cancel through Stripe at period end and now",
|
"basic5: Testing cancel through Stripe at period end and now"
|
||||||
)}`, () => {
|
)}`, () => {
|
||||||
const customerId = testCase;
|
const customerId = testCase;
|
||||||
let stripeCli: Stripe;
|
let stripeCli: Stripe;
|
||||||
@@ -45,7 +45,7 @@ describe(`${chalk.yellowBright(
|
|||||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||||
|
|
||||||
const proProduct = cusRes.products.find(
|
const proProduct = cusRes.products.find(
|
||||||
(p: any) => p.id === products.pro.id,
|
(p: any) => p.id === products.pro.id
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const subId of proProduct.subscription_ids) {
|
for (const subId of proProduct.subscription_ids) {
|
||||||
@@ -64,13 +64,13 @@ describe(`${chalk.yellowBright(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const proProduct = cusRes.products.find(
|
const proProduct = cusRes.products.find(
|
||||||
(p: any) => p.id === products.pro.id,
|
(p: any) => p.id === products.pro.id
|
||||||
);
|
);
|
||||||
expect(proProduct.canceled_at).to.not.equal(null);
|
expect(proProduct.canceled_at).to.not.equal(null);
|
||||||
expect(proProduct.status).to.equal(CusProductStatus.Active);
|
expect(proProduct.status).to.equal(CusProductStatus.Active);
|
||||||
|
|
||||||
const freeProduct = cusRes.products.find(
|
const freeProduct = cusRes.products.find(
|
||||||
(p: any) => p.id === products.free.id,
|
(p: any) => p.id === products.free.id
|
||||||
);
|
);
|
||||||
expect(freeProduct).to.exist;
|
expect(freeProduct).to.exist;
|
||||||
expect(freeProduct.status).to.equal(CusProductStatus.Scheduled);
|
expect(freeProduct.status).to.equal(CusProductStatus.Scheduled);
|
||||||
@@ -79,7 +79,7 @@ describe(`${chalk.yellowBright(
|
|||||||
it("should cancel pro product (now)", async function () {
|
it("should cancel pro product (now)", async function () {
|
||||||
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
const cusRes: any = await AutumnCli.getCustomer(customerId);
|
||||||
const proProduct = cusRes.products.find(
|
const proProduct = cusRes.products.find(
|
||||||
(p: any) => p.id === products.pro.id,
|
(p: any) => p.id === products.pro.id
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const subId of proProduct.subscription_ids) {
|
for (const subId of proProduct.subscription_ids) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { compareMainProduct } from "tests/utils/compare.js";
|
|||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
import { setupBefore } from "tests/before.js";
|
import { setupBefore } from "tests/before.js";
|
||||||
import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
|
|
||||||
|
|
||||||
const testCase = "downgrade6";
|
const testCase = "downgrade6";
|
||||||
describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => {
|
describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => {
|
||||||
@@ -25,6 +24,7 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => {
|
|||||||
org: this.org,
|
org: this.org,
|
||||||
env: this.env,
|
env: this.env,
|
||||||
autumn: this.autumnJs,
|
autumn: this.autumnJs,
|
||||||
|
attachPm: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
customer = customer_;
|
customer = customer_;
|
||||||
@@ -39,12 +39,17 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should expire premium", async function () {
|
it("should expire premium", async function () {
|
||||||
const cusProduct = await getMainCusProduct({
|
// const cusProduct = await getMainCusProduct({
|
||||||
db: this.db,
|
// db: this.db,
|
||||||
internalCustomerId: customer.internal_id,
|
// internalCustomerId: customer.internal_id,
|
||||||
});
|
// });
|
||||||
|
|
||||||
await AutumnCli.expire(cusProduct!.id);
|
// await AutumnCli.expire(cusProduct!.id);
|
||||||
|
await autumn.cancel({
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: products.premium.id,
|
||||||
|
cancel_immediately: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct product and entitlements after expiration", async function () {
|
it("should have correct product and entitlements after expiration", async function () {
|
||||||
@@ -55,60 +60,4 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => {
|
|||||||
cusRes: res,
|
cusRes: res,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// // 2. Get premium
|
|
||||||
// it("POST /attach -- attaching premium, then attach pro", async function () {
|
|
||||||
// this.timeout(30000);
|
|
||||||
// await AutumnCli.attach({
|
|
||||||
// customerId: customerId,
|
|
||||||
// productId: products.premium.id,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// await AutumnCli.attach({
|
|
||||||
// customerId: customerId,
|
|
||||||
// productId: products.pro.id,
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it("Expiring pro product (should re-attach premium)", async function () {
|
|
||||||
// this.timeout(30000);
|
|
||||||
|
|
||||||
// // Expire pro product
|
|
||||||
// const customerProduct = await getCusProduct(
|
|
||||||
// this.sb,
|
|
||||||
// customer.internal_id,
|
|
||||||
// products.pro.id,
|
|
||||||
// );
|
|
||||||
// await AutumnCli.expire(customerProduct.id);
|
|
||||||
// await timeout(5000);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it("GET /customers/:customer_id -- checking product and ents", async function () {
|
|
||||||
// this.timeout(30000);
|
|
||||||
// // Check that free is attached
|
|
||||||
// const res = await AutumnCli.getCustomer(customerId);
|
|
||||||
// compareMainProduct({
|
|
||||||
// sent: products.premium,
|
|
||||||
// cusRes: res,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Get stripe subscription (ensure canceled is null)
|
|
||||||
// const stripeCli = createStripeCli({
|
|
||||||
// org: this.org,
|
|
||||||
// env: this.env,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const premiumCusProduct = await getCusProduct(
|
|
||||||
// this.sb,
|
|
||||||
// customer.internal_id,
|
|
||||||
// products.premium.id,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const stripeSub = await stripeCli.subscriptions.retrieve(
|
|
||||||
// premiumCusProduct.processor.subscription_id,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Check that canceled is null
|
|
||||||
// assert.isNull(stripeSub.canceled_at);
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { setupBefore } from "tests/before.js";
|
|||||||
import { findCusProductById } from "@/internal/customers/cusProducts/cusProductUtils/findCusProduct.js";
|
import { findCusProductById } from "@/internal/customers/cusProducts/cusProductUtils/findCusProduct.js";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js";
|
import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js";
|
||||||
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
|
|
||||||
const testCase = "downgrade7";
|
const testCase = "downgrade7";
|
||||||
describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)}`, () => {
|
describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)}`, () => {
|
||||||
let customerId = testCase;
|
let customerId = testCase;
|
||||||
let testClockId: string;
|
let testClockId: string;
|
||||||
let customer: Customer;
|
let customer: Customer;
|
||||||
|
let autumn: AutumnInt = new AutumnInt();
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
await setupBefore(this);
|
await setupBefore(this);
|
||||||
@@ -46,14 +48,19 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)}
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should expire scheduled product (pro)", async function () {
|
it("should expire scheduled product (pro)", async function () {
|
||||||
const cusProduct = await findCusProductById({
|
// const cusProduct = await findCusProductById({
|
||||||
db: this.db,
|
// db: this.db,
|
||||||
internalCustomerId: customer.internal_id,
|
// internalCustomerId: customer.internal_id,
|
||||||
productId: products.pro.id,
|
// productId: products.pro.id,
|
||||||
});
|
// });
|
||||||
|
|
||||||
expect(cusProduct).to.exist;
|
// expect(cusProduct).to.exist;
|
||||||
await AutumnCli.expire(cusProduct!.id);
|
await autumn.cancel({
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: products.pro.id,
|
||||||
|
cancel_immediately: true,
|
||||||
|
});
|
||||||
|
// await AutumnCli.expire(cusProduct!.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct product and entitlements (premium)", async function () {
|
it("should have correct product and entitlements (premium)", async function () {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
import { AppEnv, Organization, ProductV2 } from "@autumn/shared";
|
import { AppEnv, LimitedItem, Organization, ProductV2 } from "@autumn/shared";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
@@ -21,12 +21,12 @@ import { runMigrationTest } from "./runMigrationTest.js";
|
|||||||
let messagesItem = constructFeatureItem({
|
let messagesItem = constructFeatureItem({
|
||||||
featureId: TestFeature.Messages,
|
featureId: TestFeature.Messages,
|
||||||
includedUsage: 500,
|
includedUsage: 500,
|
||||||
});
|
}) as LimitedItem;
|
||||||
|
|
||||||
let wordsItem = constructFeatureItem({
|
let wordsItem = constructFeatureItem({
|
||||||
featureId: TestFeature.Words,
|
featureId: TestFeature.Words,
|
||||||
includedUsage: 100,
|
includedUsage: 100,
|
||||||
});
|
}) as LimitedItem;
|
||||||
|
|
||||||
export let free = constructProduct({
|
export let free = constructProduct({
|
||||||
items: [messagesItem, wordsItem],
|
items: [messagesItem, wordsItem],
|
||||||
@@ -143,6 +143,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`
|
|||||||
stripeCli,
|
stripeCli,
|
||||||
testClockId,
|
testClockId,
|
||||||
advanceTo: addWeeks(Date.now(), 1).getTime(),
|
advanceTo: addWeeks(Date.now(), 1).getTime(),
|
||||||
|
waitForSeconds: 30,
|
||||||
});
|
});
|
||||||
|
|
||||||
let customer = await autumn.customers.get(customerId);
|
let customer = await autumn.customers.get(customerId);
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const runMigrationTest = async ({
|
|||||||
to_version: toProduct.version,
|
to_version: toProduct.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
await timeout(5000);
|
await timeout(10000);
|
||||||
|
|
||||||
const { subs: subsAfter } = await getSubsFromCusId({
|
const { subs: subsAfter } = await getSubsFromCusId({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
@@ -91,6 +91,7 @@ export const runMigrationTest = async ({
|
|||||||
expectSubsSame({ subsBefore, subsAfter });
|
expectSubsSame({ subsBefore, subsAfter });
|
||||||
|
|
||||||
const cusAfter = await autumn.customers.get(customerId);
|
const cusAfter = await autumn.customers.get(customerId);
|
||||||
|
|
||||||
expectFeaturesCorrect({
|
expectFeaturesCorrect({
|
||||||
customer: cusAfter,
|
customer: cusAfter,
|
||||||
product: toProduct,
|
product: toProduct,
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ describe(
|
|||||||
expect(premiumGroup1!.scheduled_ids!.length).to.equal(1);
|
expect(premiumGroup1!.scheduled_ids!.length).to.equal(1);
|
||||||
expect(starterGroup2!.scheduled_ids!.length).to.equal(1);
|
expect(starterGroup2!.scheduled_ids!.length).to.equal(1);
|
||||||
expect(premiumGroup1!.scheduled_ids![0]).to.equal(
|
expect(premiumGroup1!.scheduled_ids![0]).to.equal(
|
||||||
starterGroup2!.scheduled_ids![0],
|
starterGroup2!.scheduled_ids![0]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Check that there's no starter group 1
|
// 2. Check that there's no starter group 1
|
||||||
@@ -151,5 +151,5 @@ describe(
|
|||||||
expect(sub.cancel_at).to.equal(null);
|
expect(sub.cancel_at).to.equal(null);
|
||||||
expect(sub.status).to.equal("active");
|
expect(sub.status).to.equal("active");
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -174,14 +174,14 @@ describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proratio
|
|||||||
testClockId,
|
testClockId,
|
||||||
advanceTo: addHours(
|
advanceTo: addHours(
|
||||||
addMonths(new Date(), 1),
|
addMonths(new Date(), 1),
|
||||||
hoursToFinalizeInvoice,
|
hoursToFinalizeInvoice
|
||||||
).getTime(),
|
).getTime(),
|
||||||
waitForSeconds: 30,
|
waitForSeconds: 40,
|
||||||
});
|
});
|
||||||
|
|
||||||
const autumnCus = await autumn.customers.get(customerId);
|
const autumnCus = await autumn.customers.get(customerId);
|
||||||
expect(autumnCus.features[TestFeature.Messages].balance).to.equal(
|
expect(autumnCus.features[TestFeature.Messages].balance).to.equal(
|
||||||
newQuantity,
|
newQuantity
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(autumnCus.invoices.length).to.equal(3);
|
expect(autumnCus.invoices.length).to.equal(3);
|
||||||
@@ -190,8 +190,9 @@ describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proratio
|
|||||||
const cusProduct = await getMainCusProduct({
|
const cusProduct = await getMainCusProduct({
|
||||||
db,
|
db,
|
||||||
internalCustomerId: customer.internal_id,
|
internalCustomerId: customer.internal_id,
|
||||||
|
productGroup: testCase,
|
||||||
});
|
});
|
||||||
// console.log(cusProduct);
|
|
||||||
expect(cusProduct?.options[0].quantity).to.equal(newQuantity / 100);
|
expect(cusProduct?.options[0].quantity).to.equal(newQuantity / 100);
|
||||||
expect(cusProduct?.options[0].upcoming_quantity).to.not.exist;
|
expect(cusProduct?.options[0].upcoming_quantity).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create / delete ent
|
|||||||
});
|
});
|
||||||
|
|
||||||
await autumn.entities.create(customerId, entities);
|
await autumn.entities.create(customerId, entities);
|
||||||
|
await timeout(3000);
|
||||||
|
|
||||||
usage += entities.length;
|
usage += entities.length;
|
||||||
|
|
||||||
await expectSubQuantityCorrect({
|
await expectSubQuantityCorrect({
|
||||||
|
|||||||
@@ -36,23 +36,24 @@ export class CusService {
|
|||||||
|
|
||||||
static async updateCusEntitlement(
|
static async updateCusEntitlement(
|
||||||
axios: AxiosInstance,
|
axios: AxiosInstance,
|
||||||
|
customer_id: string,
|
||||||
customer_entitlement_id: string,
|
customer_entitlement_id: string,
|
||||||
data: any,
|
data: any
|
||||||
) {
|
) {
|
||||||
return await axios.post(
|
return await axios.post(
|
||||||
`/v1/customers/customer_entitlements/${customer_entitlement_id}`,
|
`/v1/customers/${customer_id}/entitlements/${customer_entitlement_id}`,
|
||||||
data,
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateCusProductStatus(
|
static async updateCusProductStatus(
|
||||||
axios: AxiosInstance,
|
axios: AxiosInstance,
|
||||||
customer_product_id: string,
|
customer_product_id: string,
|
||||||
data: any,
|
data: any
|
||||||
) {
|
) {
|
||||||
return await axios.post(
|
return await axios.post(
|
||||||
`/v1/customers/customer_products/${customer_product_id}`,
|
`/v1/customers/customer_products/${customer_product_id}`,
|
||||||
data,
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ export class CusService {
|
|||||||
coupon_id: string;
|
coupon_id: string;
|
||||||
}) {
|
}) {
|
||||||
return await axios.post(
|
return await axios.post(
|
||||||
`/v1/customers/${customer_id}/coupons/${coupon_id}`,
|
`/v1/customers/${customer_id}/coupons/${coupon_id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function UpdateCusEntitlement({
|
|||||||
|
|
||||||
const [updateLoading, setUpdateLoading] = useState(false);
|
const [updateLoading, setUpdateLoading] = useState(false);
|
||||||
|
|
||||||
let cusEnt = selectedCusEntitlement;
|
const cusEnt = selectedCusEntitlement;
|
||||||
|
|
||||||
const [updateFields, setUpdateFields] = useState<any>({
|
const [updateFields, setUpdateFields] = useState<any>({
|
||||||
balance:
|
balance:
|
||||||
@@ -78,11 +78,16 @@ function UpdateCusEntitlement({
|
|||||||
|
|
||||||
setUpdateLoading(true);
|
setUpdateLoading(true);
|
||||||
try {
|
try {
|
||||||
await CusService.updateCusEntitlement(axiosInstance, cusEnt.id, {
|
await CusService.updateCusEntitlement(
|
||||||
balance: balanceInt,
|
axiosInstance,
|
||||||
next_reset_at: updateFields.next_reset_at,
|
customer.id || customer.internal_id,
|
||||||
entity_id: entityId,
|
cusEnt.id,
|
||||||
});
|
{
|
||||||
|
balance: balanceInt,
|
||||||
|
next_reset_at: updateFields.next_reset_at,
|
||||||
|
entity_id: entityId,
|
||||||
|
}
|
||||||
|
);
|
||||||
toast.success("Entitlement updated successfully");
|
toast.success("Entitlement updated successfully");
|
||||||
await cusMutate();
|
await cusMutate();
|
||||||
setSelectedCusEntitlement(null);
|
setSelectedCusEntitlement(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user