212 lines
7.2 KiB
TypeScript
212 lines
7.2 KiB
TypeScript
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}`,
|
||
);
|
||
}
|
||
});
|
||
});
|