Files
cfw-autumn/server/tests/balances/track/misc/trackMisc5.test.ts
2025-10-30 07:11:42 -07:00

212 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ApiVersion, ProductItemFeatureType, type Organization } from "@autumn/shared";
import type { AppEnv, Autumn } from "autumn-js";
import { expect } from "chai";
import chalk from "chalk";
import type { Stripe } from "stripe";
import { setupBefore } from "tests/before.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearItem, constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
import { TestFeature } from "tests/setup/v2Features.js";
const testCase = "trackMisc5";
const customerId = `${testCase}_cus1`;
const seatItem = constructFeatureItem({
featureId: TestFeature.Users,
includedUsage: 5,
featureType: ProductItemFeatureType.ContinuousUse,
});
const perSeatMessagesItem = constructArrearItem({
featureId: TestFeature.Messages,
entityFeatureId: TestFeature.Users,
price: 0.01,
includedUsage: 500,
usageLimit: 600,
});
const pro = constructProduct({
id: "pro",
items: [seatItem, perSeatMessagesItem],
type: "pro",
})
describe(`${chalk.yellowBright(`trackMisc/${testCase}: Testing per-entity trackMisc track with concurrent requests`)}`, () => {
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
let stripeCli: Stripe;
let autumnInt: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
let autumnJs: Autumn;
before(async function () {
await setupBefore(this);
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
autumnJs = this.autumnJs;
try {
await (autumnInt as AutumnInt).customers.delete(customerId);
} catch (_) {}
await addPrefixToProducts({
products: [pro],
prefix: testCase,
})
await createProducts({
autumn: autumnInt,
products: [pro],
customerId,
db,
orgId: org.id,
env,
})
});
it("should create a customer and issue balances", async () => {
const { customer } = await initCustomerV2({
autumn: autumnInt,
customerId,
org,
env,
db,
attachPm: "success",
})
expect(customer).to.exist;
expect(customer.id).to.equal(customerId);
expect(customer.name).to.equal(customerId);
expect(customer.email).to.equal(`${customerId}@example.com`);
await autumnJs.attach({
customer_id: customerId,
product_id: pro.id,
})
});
it("should create 5 seats each with 500 messages", async () => {
const customer = await autumnInt.customers.get(customerId);
const seatBalance = customer.features[TestFeature.Users].balance;
expect(seatBalance).to.equal(5, `Seat balance should be 5, got ${seatBalance}`);
// Create 5 entities (seats)
const entities = [
{ id: "seat1", name: "Seat 1" },
{ id: "seat2", name: "Seat 2" },
{ id: "seat3", name: "Seat 3" },
{ id: "seat4", name: "Seat 4" },
{ id: "seat5", name: "Seat 5" },
];
for (const entity of entities) {
await autumnInt.entities.create(customerId, {
id: entity.id,
name: entity.name,
feature_id: TestFeature.Users,
});
}
// Verify each seat has 500 messages
const updatedEntity = await autumnInt.entities.get(customerId, entities[0].id);
console.log(JSON.stringify(updatedEntity, null, 4));
expect(updatedEntity.features[TestFeature.Messages].balance).to.equal(500, JSON.stringify(updatedEntity, null, 4));
});
it("should enforce usage_limit of 600 per seat with concurrent 200-unit requests", async () => {
const entityId = "seat1";
// Verify seat1 has 500 included messages with 600 usage_limit
const entityRes = await autumnInt.entities.get(customerId, entityId);
expect(entityRes.features[TestFeature.Messages].balance).to.equal(500);
// expect(entityRes.features[TestFeature.Messages].usage_limit).to.equal(600);
console.log("🚀 Starting 5 concurrent track calls (200 units each) for seat1...");
console.log(` Initial state: balance=${entityRes.features[TestFeature.Messages].balance}, usage_limit=${entityRes.features[TestFeature.Messages].usage_limit}`);
// Try 5 concurrent 200-unit sends to seat1
// With usage_limit of 600, only 3 should succeed (3×200=600 <= 600)
const promises = [
autumnInt.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
entity_id: entityId,
value: 200,
}),
autumnInt.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
entity_id: entityId,
value: 200,
}),
autumnInt.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
entity_id: entityId,
value: 200,
}),
autumnInt.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
entity_id: entityId,
value: 200,
}),
autumnInt.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
entity_id: entityId,
value: 200,
}),
];
let results = await Promise.allSettled(promises);
console.log("📊 Results breakdown:");
results.forEach((result, index) => {
if (result.status === "rejected") {
console.log(` [${index}] ❌ REJECTED:`, result.reason?.message || result.reason);
console.log(` Error code:`, result.reason?.code);
} else {
console.log(` [${index}] ✅ FULFILLED (HTTP 200):`, JSON.stringify(result.value));
}
});
const successCount = results.filter(r => r.status === "fulfilled").length;
const rejectedCount = results.filter(r => r.status === "rejected").length;
console.log(`\n📈 Summary: ${successCount} HTTP 200 responses, ${rejectedCount} HTTP errors`);
expect(successCount).to.equal(3, `Expected exactly 3 HTTP 200 responses, got ${successCount}`);
expect(rejectedCount).to.equal(2, `Expected exactly 2 HTTP errors (usage_limit exceeded), got ${rejectedCount}`);
// Get final state
const finalEntityRes = await autumnInt.entities.get(customerId, entityId);
console.log(`\n📦 Final state for ${entityId}:`);
console.log(`- Balance: ${finalEntityRes.features[TestFeature.Messages].balance} (expected: -100)`);
console.log(`- Usage: ${finalEntityRes.features[TestFeature.Messages].usage} (expected: 600)`);
console.log(`- Usage limit: ${finalEntityRes.features[TestFeature.Messages].usage_limit} (expected: 600)`);
expect(finalEntityRes.features[TestFeature.Messages].balance).to.equal(
-100,
`Balance should be -100 (500 included - 600 used), got ${finalEntityRes.features[TestFeature.Messages].balance}`,
);
expect(finalEntityRes.features[TestFeature.Messages].usage).to.equal(
600,
`Usage should be 600, got ${finalEntityRes.features[TestFeature.Messages].usage}`,
);
// Verify other seats remain untouched at 500
for (const seatId of ["seat2", "seat3", "seat4", "seat5"]) {
const otherSeatRes = await autumnInt.entities.get(customerId, seatId);
expect(otherSeatRes.features[TestFeature.Messages].balance).to.equal(
500,
`${seatId} should still have 500 messages, got ${otherSeatRes.features[TestFeature.Messages].balance}`,
);
}
});
});