fix: usage_limits field returned in get customer and check

This commit is contained in:
John Yeo
2025-07-08 18:52:27 +01:00
parent 43a385383f
commit 980f2d958a
14 changed files with 295 additions and 77 deletions

View File

@@ -41,11 +41,25 @@ export const sendTextEmail = async ({
if (error) {
logger.error(`Error sending email`, {
error,
data: {
from,
fromEmail,
to,
subject,
body,
},
});
}
} catch (error) {
logger.error(`Error sending email`, {
error,
data: {
from,
fromEmail,
to,
subject,
body,
},
});
throw error;
}

View File

@@ -5,21 +5,16 @@ import {
type Feature,
FeatureType,
type FullCustomerEntitlement,
type Organization,
} from "@autumn/shared";
import {
cusEntsContainFeature,
getFeatureBalance,
getUnlimitedAndUsageAllowed,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { getFeatureBalance } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import { handleEventSent } from "../events/eventRouter.js";
import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { SuccessCode } from "@autumn/shared";
import { handleProductCheck } from "./handlers/handleProductCheck.js";
import { getBooleanEntitledResult } from "./checkUtils.js";
import { getCheckPreview } from "./getCheckPreview.js";
@@ -183,6 +178,7 @@ checkRouter.post("", async (req: any, res: any) => {
org,
cusProducts,
requiredBalance,
apiVersion,
});
const { allowed, balance } = v2Response;

View File

@@ -1,14 +1,10 @@
import {
getFeatureBalance,
getUnlimitedAndUsageAllowed,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { cusEntMatchesFeature } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import { getCusBalances } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.js";
import { balancesToFeatureResponse } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.js";
import {
CheckResponseSchema,
Feature,
FeatureType,
FullCusEntWithFullCusProduct,
FullCusProduct,
FullCustomer,
@@ -16,7 +12,6 @@ import {
SuccessCode,
} from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
import { freeTrialsAreSame } from "@/internal/products/free-trials/freeTrialUtils.js";
export const getFeatureToUse = ({
creditSystems,
@@ -59,6 +54,7 @@ export const getV2CheckResponse = async ({
org,
cusProducts,
requiredBalance,
apiVersion,
}: {
fullCus: FullCustomer;
cusEnts: FullCusEntWithFullCusProduct[];
@@ -67,6 +63,7 @@ export const getV2CheckResponse = async ({
org: Organization;
cusProducts: FullCusProduct[];
requiredBalance?: number;
apiVersion: number;
}) => {
// 1. Get the feature to use
const featureToUse = getFeatureToUse({
@@ -92,6 +89,7 @@ export const getV2CheckResponse = async ({
cusPrices,
org,
entity: fullCus.entity,
apiVersion,
});
let cusFeatures = balancesToFeatureResponse({
@@ -99,37 +97,40 @@ export const getV2CheckResponse = async ({
balances,
});
// cusFeature.balance += overage_limit
const cusFeature = cusFeatures[featureToUse.id] || {};
let allowed = false;
let totalUsageLimit = 0;
console.log(featureCusEnts.length);
for (const ent of featureCusEnts) {
totalUsageLimit += ent.entitlement.usage_limit || 0;
}
console.log("Feature Balance", cusFeature.balance);
console.log("Total Usage Limit", totalUsageLimit);
console.log("Required Balance", requiredBalance);
console.log("Current Usage", cusFeature.usage);
let totalPaidUsageAllowance = featureCusEnts.reduce((acc, ce) => {
let ent = ce.entitlement;
if (notNullish(ent.usage_limit)) {
return acc + ent.usage_limit! - (ent.allowance || 0);
}
return acc;
}, 0);
if (
(cusFeature && unlimited) ||
usageAllowed ||
cusFeature.balance >= (requiredBalance || 1)
// cusFeature.balance >= (requiredBalance || 1)
cusFeature.balance + totalPaidUsageAllowance >= (requiredBalance || 1)
) {
allowed = true;
}
console.log("Included Usage", cusFeature.included_usage);
// let totalUsageLimit = 0;
if (totalUsageLimit > 0) {
if ((cusFeature.balance - (requiredBalance || 1)) < -(totalUsageLimit - (cusFeature.included_usage || 0))) {
allowed = false;
}
}
// for (const ent of featureCusEnts) {
// totalUsageLimit += ent.entitlement.usage_limit || 0;
// }
// if (totalUsageLimit > 0) {
// if (
// cusFeature.balance - (requiredBalance || 1) <
// -(totalUsageLimit - (cusFeature.included_usage || 0))
// ) {
// allowed = false;
// }
// }
return CheckResponseSchema.parse({
customer_id: fullCus.id,

View File

@@ -351,7 +351,10 @@ export const getUnlimitedAndUsageAllowed = ({
);
const usageAllowed = cusEnts.some(
(ent) => ent.internal_feature_id === internalFeatureId && ent.usage_allowed,
(ent) =>
ent.internal_feature_id === internalFeatureId &&
ent.usage_allowed &&
nullish(ent.entitlement.usage_limit),
);
return { unlimited, usageAllowed };

View File

@@ -12,6 +12,7 @@ import {
getCusFeatureType,
isCreditSystem,
} from "@/internal/features/featureUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const sumValues = (
entList: CusEntResponse[],
@@ -46,6 +47,7 @@ export const featuresToObject = ({
entList: CusEntResponse[];
}) => {
let featureObject: Record<string, CusEntResponseV2> = {};
for (let entRes of entList) {
let feature = features.find((f) => f.id == entRes.feature_id)!;
let featureType = getCusFeatureType({ feature });
@@ -57,7 +59,13 @@ export const featuresToObject = ({
if (featureObject[featureId]) {
continue;
}
let includedUsage = sumValues(relatedEnts, "included_usage");
let usageLimit: number | undefined = sumValues(relatedEnts, "usage_limit");
if (notNullish(usageLimit) && usageLimit === includedUsage) {
usageLimit = undefined;
}
featureObject[featureId] = {
id: featureId,
name: feature.name,
@@ -66,7 +74,7 @@ export const featuresToObject = ({
balance: unlimited ? null : sumValues(relatedEnts, "balance"),
usage: sumValues(relatedEnts, "usage"),
included_usage: sumValues(relatedEnts, "included_usage"),
usage_limit: sumValues(relatedEnts, "usage_limit"),
usage_limit: usageLimit,
next_reset_at: getEarliestNextResetAt(relatedEnts),
interval: relatedEnts.length == 1 ? relatedEnts[0].interval : "multiple",

View File

@@ -63,6 +63,7 @@ export const getV1EntitlementsRes = ({
res.next_reset_at =
isBoolean || unlimited ? undefined : cusEnt.next_reset_at;
res.allowance = isBoolean || unlimited ? undefined : 0;
res.usage_limit = isBoolean || unlimited ? undefined : 0;
}
return res;
@@ -74,6 +75,7 @@ export const getCusBalances = async ({
cusPrices,
org,
entity,
apiVersion,
}: {
cusEntsWithCusProduct: (FullCustomerEntitlement & {
customer_product: FullCusProduct;
@@ -81,6 +83,7 @@ export const getCusBalances = async ({
cusPrices: FullCustomerPrice[];
org: Organization;
entity?: Entity;
apiVersion: number;
}) => {
const data: Record<string, any> = {};
const features = cusEntsWithCusProduct.map(
@@ -116,7 +119,7 @@ export const getCusBalances = async ({
});
// 1. Initialize balance object
if (!data[key] && org.api_version == APIVersion.v1) {
if (!data[key] && apiVersion == APIVersion.v1) {
data[key] = getV1EntitlementsRes({
org,
cusEnt,
@@ -146,13 +149,13 @@ export const getCusBalances = async ({
used: isBoolean ? undefined : unlimited ? null : 0,
unused: 0,
overage_allowed: usageAllowed,
usage_limit: ent.usage_limit ?? null,
};
if (org.config.api_version >= BREAK_API_VERSION) {
data[key].next_reset_at =
isBoolean || unlimited ? undefined : cusEnt.next_reset_at;
data[key].allowance = isBoolean || unlimited ? undefined : 0;
data[key].usage_limit = isBoolean || unlimited ? undefined : 0;
}
}
}
@@ -179,7 +182,6 @@ export const getCusBalances = async ({
data[key].total += total;
data[key].unused += unused || 0;
// data[key].overage_limit = ent.usage_limit;
if (org.config.api_version >= BREAK_API_VERSION) {
if (
@@ -197,6 +199,13 @@ export const getCusBalances = async ({
});
data[key].allowance += (resetBalance || 0) * count;
let usageLimit = ent.usage_limit;
if (notNullish(usageLimit)) {
data[key].usage_limit += usageLimit;
} else {
data[key].usage_limit += ent.allowance || 0;
}
}
}

View File

@@ -15,9 +15,7 @@ import {
Organization,
FullCustomer,
CusExpand,
RewardType,
RewardResponse,
CouponDurationType,
EntityResponseSchema,
} from "@autumn/shared";
import { getCusInvoices } from "./cusUtils.js";
@@ -77,6 +75,7 @@ export const getCustomerDetails = async ({
cusEntsWithCusProduct: cusEnts,
cusPrices: cusProductsToCusPrices({ cusProducts, inStatuses }),
org,
apiVersion,
});
let subIds = cusProducts.flatMap(

View File

@@ -27,7 +27,8 @@ export const sendMigrationEmail = safeResend({
console.log("Sending migration email");
await sendTextEmail({
from: "John",
from: `Autumn`,
fromEmail: "hey",
to: "johnyeocx@gmail.com",
subject: `Migration Job Finished -- ${migrationJob.id}`,
body: `

View File

@@ -134,7 +134,7 @@ export const createEntityForCusProduct = async ({
) {
throw new RecaseError({
message: `Cannot create ${inputEntities.length} entities for feature ${feature.name} as it would exceed the usage limit.`,
code: ErrCode.InvalidInputs,
code: ErrCode.FeatureLimitReached,
});
}

View File

@@ -49,7 +49,6 @@ export const auth = betterAuth({
"http://localhost:3000",
"https://app.useautumn.com",
"https://*.useautumn.com",
process.env.CLIENT_URL!,
],
emailAndPassword: {
enabled: true,

View File

@@ -2,7 +2,7 @@ import chalk from "chalk";
import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { APIVersion, AppEnv, Organization } from "@autumn/shared";
import { APIVersion, AppEnv, ErrCode, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
@@ -11,19 +11,22 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { expect } from "chai";
const userItem = constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 50,
includedUsage: 0,
usageLimit: 2,
});
export let pro = constructProduct({
items: [
constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 50,
usageLimit: 2,
}),
],
items: [userItem],
type: "pro",
});
const testCase = "entity1";
const testCase = "usageLimit1";
describe(`${chalk.yellowBright(`${testCase}: Testing entities`)}`, () => {
let customerId = testCase;
@@ -34,29 +37,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing entities`)}`, () => {
let curUnix = new Date().getTime();
const entities = [
{
id: "1",
name: "Entity 1",
featureId: TestFeature.Users,
},
{
id: "2",
name: "Entity 1",
featureId: TestFeature.Users,
},
{
id: "3",
name: "Entity 1",
featureId: TestFeature.Users,
},
{
id: "4",
name: "Entity 1",
featureId: TestFeature.Users,
},
];
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
@@ -92,6 +72,29 @@ describe(`${chalk.yellowBright(`${testCase}: Testing entities`)}`, () => {
testClockId = testClockId1!;
});
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
{
id: "3",
name: "Entity 3",
feature_id: TestFeature.Users,
},
{
id: "4",
name: "Entity 4",
feature_id: TestFeature.Users,
},
];
it("should attach pro product", async function () {
await runAttachTest({
autumn,
@@ -103,4 +106,44 @@ describe(`${chalk.yellowBright(`${testCase}: Testing entities`)}`, () => {
env,
});
});
it("should create more entities than the limit and hit error", async function () {
await expectAutumnError({
errCode: ErrCode.FeatureLimitReached,
func: async () => {
await autumn.entities.create(customerId, entities);
},
});
});
it("should create entities one by one, then hit usage limit", async function () {
await autumn.entities.create(customerId, entities[0]);
await autumn.entities.create(customerId, entities[1]);
await expectAutumnError({
errCode: ErrCode.FeatureLimitReached,
func: async () => {
await autumn.entities.create(customerId, entities[2]);
},
});
});
it("should have correct check and get customer value", async function () {
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Users,
});
const customer = await autumn.customers.get(customerId);
console.log(check);
console.log(customer);
expect(check.balance).to.equal(-2);
// @ts-ignore
expect(check.usage_limit).to.equal(userItem.usage_limit);
// @ts-ignore
expect(customer.features[TestFeature.Users].usage_limit).to.equal(
userItem.usage_limit,
);
});
});

View File

@@ -0,0 +1,145 @@
import chalk from "chalk";
import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { APIVersion, AppEnv, ErrCode, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { expect } from "chai";
const userItem = constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 50,
includedUsage: 0,
usageLimit: 2,
});
export let pro = constructProduct({
items: [userItem],
type: "pro",
});
const testCase = "entity1";
describe(`${chalk.yellowBright(`${testCase}: Testing entities`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro],
customerId,
db,
orgId: org.id,
env,
});
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = testClockId1!;
});
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
{
id: "3",
name: "Entity 3",
feature_id: TestFeature.Users,
},
{
id: "4",
name: "Entity 4",
feature_id: TestFeature.Users,
},
];
it("should attach pro product", async function () {
await runAttachTest({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
it("should create more entities than the limit and hit error", async function () {
await expectAutumnError({
errCode: ErrCode.FeatureLimitReached,
func: async () => {
await autumn.entities.create(customerId, entities);
},
});
});
it("should create entities one by one, then hit usage limit", async function () {
await autumn.entities.create(customerId, entities[0]);
await autumn.entities.create(customerId, entities[1]);
await expectAutumnError({
errCode: ErrCode.FeatureLimitReached,
func: async () => {
await autumn.entities.create(customerId, entities[2]);
},
});
});
it("should have correct check and get customer value", async function () {
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Users,
});
expect(check.balance).to.equal(-2);
// @ts-ignore
expect(check.usage_limit).to.equal(userItem.usage_limit);
const customer = await autumn.customers.get(customerId);
// @ts-ignore
expect(customer.features[TestFeature.Users].usage_limit).to.equal(
userItem.usage_limit,
);
});
});

View File

@@ -17,7 +17,6 @@ export const ErrCode = {
// General
InvalidInputs: "invalid_inputs",
LimitsReached: "limits_reached",
InvalidRequest: "invalid_request",
InvalidExpand: "invalid_expand",
@@ -30,6 +29,7 @@ export const ErrCode = {
InvalidFeature: "invalid_feature",
DuplicateFeatureId: "duplicate_feature_id",
InvalidEventName: "invalid_event_name",
FeatureLimitReached: "feature_limit_reached",
// Internal
InternalError: "internal_error",

View File

@@ -119,7 +119,7 @@ export const SignIn = () => {
Continue with Google
</Button>
<Button
{/* <Button
variant="auth"
onClick={handleGoogleSignIn}
isLoading={googleLoading}
@@ -129,7 +129,7 @@ export const SignIn = () => {
className={height}
>
Continue with Stripe
</Button>
</Button> */}
{/* Divider */}
<div className="relative">