wip
This commit is contained in:
14
package.json
14
package.json
@@ -16,16 +16,16 @@
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun scripts/start-dev.js",
|
||||
"d": "ENV_FILE=.env infisical run --env=dev -- bun scripts/start-dev.js",
|
||||
"p": "ENV_FILE=.env.prod infisical run --env=prod -- bun scripts/start-dev.js",
|
||||
"dev": "bun scripts/dev.ts",
|
||||
"d": "ENV_FILE=.env infisical run --env=dev -- bun scripts/dev.ts",
|
||||
"p": "ENV_FILE=.env.prod infisical run --env=prod -- bun scripts/dev.ts",
|
||||
|
||||
"setup": "node scripts/setup.js",
|
||||
"setup:test": "bun scripts/setup-test.ts",
|
||||
"setup": "node scripts/setup/setup.js",
|
||||
"setup:test": "bun scripts/setup/setup-test.ts",
|
||||
"tests": "bun scripts/test.ts",
|
||||
|
||||
"setupci": "node scripts/setupci.js",
|
||||
"replicate": "bun scripts/replicate.ts",
|
||||
"setupci": "node scripts/setup/setupci.js",
|
||||
"replicate": "bun scripts/db/replicate.ts",
|
||||
"db:push": " bun -F @autumn/shared db:push",
|
||||
"db:generate": "bun -F @autumn/shared db:generate",
|
||||
"db:migrate": " bun -F @autumn/shared db:migrate"
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { getProcessOnPort, killProcess } from "./detect-ports.js";
|
||||
|
||||
const PORTS_TO_CHECK = [3000, 3001, 8080, 8081, 8082, 8083];
|
||||
|
||||
async function cleanupPorts() {
|
||||
console.log("🧹 Cleaning up dev server ports...\n");
|
||||
|
||||
let killedCount = 0;
|
||||
|
||||
for (const port of PORTS_TO_CHECK) {
|
||||
const processInfo = await getProcessOnPort(port);
|
||||
|
||||
if (processInfo) {
|
||||
const { pid, processName } = processInfo;
|
||||
|
||||
// Check if it's a dev server process
|
||||
const isDevProcess =
|
||||
processName.includes("bun") ||
|
||||
processName.includes("node") ||
|
||||
processName.includes("vite");
|
||||
|
||||
if (isDevProcess) {
|
||||
console.log(`⚠️ Port ${port}: ${processName} (PID: ${pid})`);
|
||||
const killed = await killProcess(pid);
|
||||
|
||||
if (killed) {
|
||||
console.log(` ✅ Killed process ${pid}\n`);
|
||||
killedCount++;
|
||||
} else {
|
||||
console.log(` ❌ Failed to kill process ${pid}\n`);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`ℹ️ Port ${port}: ${processName} (PID: ${pid}) - skipping (not a dev server)\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (killedCount === 0) {
|
||||
console.log("✨ No dev server processes found on common ports");
|
||||
} else {
|
||||
console.log(`✅ Cleaned up ${killedCount} process(es)`);
|
||||
}
|
||||
}
|
||||
|
||||
cleanupPorts().catch((error) => {
|
||||
console.error("Error cleaning up ports:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,248 +0,0 @@
|
||||
import { exec } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import net from "node:net";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const DEFAULT_VITE_PORT = 3000;
|
||||
const DEFAULT_SERVER_PORT = 8080;
|
||||
|
||||
/**
|
||||
* Get the process ID using a specific port
|
||||
*/
|
||||
async function getProcessOnPort(port) {
|
||||
try {
|
||||
const { stdout } = await execAsync(`lsof -ti:${port}`);
|
||||
const pid = stdout.trim();
|
||||
if (pid) {
|
||||
// Get process details
|
||||
const { stdout: psOut } = await execAsync(`ps -p ${pid} -o comm=`);
|
||||
const processName = psOut.trim();
|
||||
return { pid: parseInt(pid), processName };
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a process by PID
|
||||
*/
|
||||
async function killProcess(pid) {
|
||||
try {
|
||||
await execAsync(`kill -9 ${pid}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is available, and optionally kill old dev server processes
|
||||
*/
|
||||
async function isPortAvailable(port, killIfDevServer = true) {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
|
||||
server.once("error", async (err) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
// Port is in use - check what's using it
|
||||
if (killIfDevServer) {
|
||||
const processInfo = await getProcessOnPort(port);
|
||||
if (processInfo) {
|
||||
const { pid, processName } = processInfo;
|
||||
// Check if it's likely an old dev server process (bun, node, vite)
|
||||
const isDevProcess =
|
||||
processName.includes("bun") ||
|
||||
processName.includes("node") ||
|
||||
processName.includes("vite");
|
||||
|
||||
if (isDevProcess) {
|
||||
console.log(
|
||||
`⚠️ Port ${port} is in use by ${processName} (PID: ${pid})`,
|
||||
);
|
||||
console.log(` Attempting to kill old dev server process...`);
|
||||
const killed = await killProcess(pid);
|
||||
if (killed) {
|
||||
console.log(` ✅ Killed process ${pid}`);
|
||||
// Wait a bit for the port to be released
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
resolve(true);
|
||||
return;
|
||||
} else {
|
||||
console.log(` ❌ Failed to kill process ${pid}`);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`⚠️ Port ${port} is in use by ${processName} (PID: ${pid})`,
|
||||
);
|
||||
console.log(` Skipping - not a dev server process`);
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
server.once("listening", () => {
|
||||
server.close();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
server.listen(port);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next available port starting from the given port
|
||||
*/
|
||||
async function findAvailablePort(startPort, maxAttempts = 10) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const port = startPort + i;
|
||||
if (await isPortAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find available port starting from ${startPort}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or create .env file with the detected ports
|
||||
*/
|
||||
function updateEnvFile(filePath, updates) {
|
||||
let content = "";
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
content = fs.readFileSync(filePath, "utf-8");
|
||||
}
|
||||
|
||||
// Parse existing env file
|
||||
const lines = content.split("\n");
|
||||
const envMap = new Map();
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith("#")) {
|
||||
const [key, ...valueParts] = trimmed.split("=");
|
||||
if (key) {
|
||||
envMap.set(key.trim(), valueParts.join("="));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update with new values
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
envMap.set(key, value);
|
||||
}
|
||||
|
||||
// Rebuild content preserving comments and empty lines
|
||||
const newLines = [];
|
||||
const processedKeys = new Set();
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
newLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [key] = trimmed.split("=");
|
||||
if (key && envMap.has(key.trim())) {
|
||||
processedKeys.add(key.trim());
|
||||
newLines.push(`${key.trim()}=${envMap.get(key.trim())}`);
|
||||
} else {
|
||||
newLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new keys that weren't in the original file
|
||||
for (const [key, value] of envMap.entries()) {
|
||||
if (!processedKeys.has(key)) {
|
||||
newLines.push(`${key}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, newLines.join("\n"));
|
||||
}
|
||||
|
||||
async function detectAndSetPorts() {
|
||||
console.log(
|
||||
`🔍 Checking default ports (Frontend: ${DEFAULT_VITE_PORT}, Backend: ${DEFAULT_SERVER_PORT})...`,
|
||||
);
|
||||
|
||||
// First try default ports and kill old dev servers if needed
|
||||
const viteAvailable = await isPortAvailable(DEFAULT_VITE_PORT, true);
|
||||
const serverAvailable = await isPortAvailable(DEFAULT_SERVER_PORT, true);
|
||||
|
||||
// If still not available after cleanup, find alternative ports
|
||||
const vitePort = viteAvailable
|
||||
? DEFAULT_VITE_PORT
|
||||
: await findAvailablePort(DEFAULT_VITE_PORT + 1);
|
||||
const serverPort = serverAvailable
|
||||
? DEFAULT_SERVER_PORT
|
||||
: await findAvailablePort(DEFAULT_SERVER_PORT + 1);
|
||||
|
||||
console.log(`\n✅ Using ports:`);
|
||||
console.log(
|
||||
` Frontend: ${vitePort}${vitePort !== DEFAULT_VITE_PORT ? " (alternative)" : ""}`,
|
||||
);
|
||||
console.log(
|
||||
` Backend: ${serverPort}${serverPort !== DEFAULT_SERVER_PORT ? " (alternative)" : ""}`,
|
||||
);
|
||||
|
||||
// Get root directory (parent of scripts folder)
|
||||
const rootDir = path.dirname(new URL(import.meta.url).pathname);
|
||||
const projectRoot = path.join(rootDir, "..");
|
||||
|
||||
// Update vite .env
|
||||
const viteEnvPath = path.join(projectRoot, "vite", ".env");
|
||||
updateEnvFile(viteEnvPath, {
|
||||
VITE_FRONTEND_URL: `http://localhost:${vitePort}`,
|
||||
VITE_BACKEND_URL: `http://localhost:${serverPort}`,
|
||||
});
|
||||
|
||||
// Update server .env
|
||||
const serverEnvPath = path.join(projectRoot, "server", ".env");
|
||||
updateEnvFile(serverEnvPath, {
|
||||
BETTER_AUTH_URL: `http://localhost:${serverPort}`,
|
||||
CLIENT_URL: `http://localhost:${vitePort}`,
|
||||
});
|
||||
|
||||
// Set environment variables for current process
|
||||
process.env.VITE_PORT = vitePort.toString();
|
||||
process.env.SERVER_PORT = serverPort.toString();
|
||||
process.env.VITE_FRONTEND_URL = `http://localhost:${vitePort}`;
|
||||
process.env.VITE_BACKEND_URL = `http://localhost:${serverPort}`;
|
||||
process.env.BETTER_AUTH_URL = `http://localhost:${serverPort}`;
|
||||
process.env.CLIENT_URL = `http://localhost:${vitePort}`;
|
||||
|
||||
console.log(`✅ Environment variables updated\n`);
|
||||
|
||||
return { vitePort, serverPort };
|
||||
}
|
||||
|
||||
// Only run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
detectAndSetPorts()
|
||||
.then(({ vitePort, serverPort }) => {
|
||||
console.log(`\n🚀 Ready to start development servers`);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error detecting ports:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
detectAndSetPorts,
|
||||
findAvailablePort,
|
||||
isPortAvailable,
|
||||
getProcessOnPort,
|
||||
killProcess,
|
||||
};
|
||||
107
scripts/dev.ts
Normal file
107
scripts/dev.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { existsSync, readFileSync, rmSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const VITE_PORT = 3000;
|
||||
const SERVER_PORT = 8080;
|
||||
|
||||
/**
|
||||
* Read environment variable from .env file
|
||||
*/
|
||||
function getEnvVariable(filePath: string, key: string): string | null {
|
||||
if (!existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith("#")) {
|
||||
const [envKey, ...valueParts] = trimmed.split("=");
|
||||
if (envKey && envKey.trim() === key) {
|
||||
return valueParts.join("=");
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function startDev() {
|
||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(rootDir, "..");
|
||||
|
||||
try {
|
||||
// Check if using remote backend (api.useautumn.com)
|
||||
const viteEnvPath = join(projectRoot, "vite", ".env");
|
||||
const backendUrl =
|
||||
process.env.VITE_BACKEND_URL ||
|
||||
getEnvVariable(viteEnvPath, "VITE_BACKEND_URL");
|
||||
const isUsingRemoteBackend = backendUrl?.includes("api.useautumn.com");
|
||||
|
||||
if (isUsingRemoteBackend) {
|
||||
console.log("\n🌐 Using remote backend (api.useautumn.com)");
|
||||
console.log("⏭️ Skipping port cleanup...\n");
|
||||
} else {
|
||||
// Port cleanup disabled (detection is unreliable)
|
||||
console.log("⏭️ Skipping port cleanup...\n");
|
||||
}
|
||||
|
||||
// Clear Vite cache to prevent dep optimization issues
|
||||
const viteCachePath = join(projectRoot, "vite", "node_modules", ".vite");
|
||||
if (existsSync(viteCachePath)) {
|
||||
console.log("🧹 Clearing Vite cache...\n");
|
||||
rmSync(viteCachePath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log("🚀 Starting development servers in watch mode...\n");
|
||||
|
||||
// Start server, workers, and vite using Bun.spawn
|
||||
// Use sh -c to run the shell command with cd
|
||||
const concurrentlyProc = Bun.spawn(
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
`bunx concurrently -n server,workers,vite -c green,yellow,blue "cd server && SERVER_PORT=${SERVER_PORT} bun dev" "cd server && bun workers:dev" "cd vite && VITE_PORT=${VITE_PORT} bun dev"`,
|
||||
],
|
||||
{
|
||||
cwd: projectRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_PORT: VITE_PORT.toString(),
|
||||
SERVER_PORT: SERVER_PORT.toString(),
|
||||
},
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
onExit(proc, exitCode, signalCode, error) {
|
||||
if (error) {
|
||||
console.error("Failed to start development servers:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
if (exitCode !== 0 && exitCode !== null) {
|
||||
console.error(`Development servers exited with code ${exitCode}`);
|
||||
}
|
||||
process.exit(exitCode ?? 0);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Handle termination signals
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\n\n🛑 Shutting down development servers...");
|
||||
concurrentlyProc.kill("SIGINT");
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("\n\n🛑 Shutting down development servers...");
|
||||
concurrentlyProc.kill("SIGTERM");
|
||||
});
|
||||
|
||||
// Wait for the process to exit
|
||||
await concurrentlyProc.exited;
|
||||
} catch (error) {
|
||||
console.error("Error starting development servers:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startDev();
|
||||
@@ -4,9 +4,9 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"setup": "tsx setup.js",
|
||||
"setup-test": "tsx setup-test.ts",
|
||||
"replicate": "bun run replicate.ts"
|
||||
"setup": "tsx setup/setup.js",
|
||||
"setup-test": "tsx setup/setup-test.ts",
|
||||
"replicate": "bun run db/replicate.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@autumn/shared": "workspace:*",
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
import chalk from "chalk";
|
||||
import inquirer from "inquirer";
|
||||
import { createTestOrg, TEST_ORG_CONFIG } from "./setupTestUtils/createTestOrg.js";
|
||||
import {
|
||||
createTestOrg,
|
||||
TEST_ORG_CONFIG,
|
||||
} from "../setupTestUtils/createTestOrg.js";
|
||||
import {
|
||||
updateMultipleEnvVars,
|
||||
updateSingleEnvVar,
|
||||
} from "../setupTestUtils/incrementalEnvUpdate.js";
|
||||
import {
|
||||
setupStripeTestKey,
|
||||
setupTunnelUrl,
|
||||
setupUpstash,
|
||||
} from "./setupTestUtils/setupPrompts.js";
|
||||
import { updateEnvFile } from "./setupTestUtils/updateEnvFile.js";
|
||||
import {
|
||||
updateSingleEnvVar,
|
||||
updateMultipleEnvVars,
|
||||
} from "./setupTestUtils/incrementalEnvUpdate.js";
|
||||
} from "../setupTestUtils/setupPrompts.js";
|
||||
import { updateEnvFile } from "../setupTestUtils/updateEnvFile.js";
|
||||
|
||||
async function showPreparationChecklist() {
|
||||
console.log(
|
||||
chalk.magentaBright("\n================ Autumn Test Setup ================\n"),
|
||||
chalk.magentaBright(
|
||||
"\n================ Autumn Test Setup ================\n",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.cyan("This script will set up a test organization for development.\n"),
|
||||
chalk.cyan(
|
||||
"This script will set up a test organization for development.\n",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.yellowBright("Before you begin, please have the following ready:\n"),
|
||||
);
|
||||
console.log(chalk.yellowBright("Before you begin, please have the following ready:\n"));
|
||||
|
||||
console.log(chalk.whiteBright("1. Stripe Test API Key (sk_test_...)"));
|
||||
console.log(
|
||||
chalk.gray(" → Used to link Stripe to your test account for payment processing\n"),
|
||||
chalk.gray(
|
||||
" → Used to link Stripe to your test account for payment processing\n",
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.whiteBright("2. Upstash Redis REST URL and Token"));
|
||||
console.log(
|
||||
chalk.gray(" → Used for caching customer objects and testing race conditions\n"),
|
||||
chalk.gray(
|
||||
" → Used for caching customer objects and testing race conditions\n",
|
||||
),
|
||||
);
|
||||
|
||||
console.log(chalk.whiteBright("3. Tunnel URL (e.g., ngrok URL)"));
|
||||
@@ -50,7 +63,11 @@ async function showPreparationChecklist() {
|
||||
]);
|
||||
|
||||
if (!ready) {
|
||||
console.log(chalk.yellow("\nSetup cancelled. Run the script again when you're ready!\n"));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"\nSetup cancelled. Run the script again when you're ready!\n",
|
||||
),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +78,7 @@ async function main() {
|
||||
|
||||
try {
|
||||
// Import db from server
|
||||
const { db } = await import("../server/src/db/initDrizzle.js");
|
||||
const { db } = await import("@server/db/initDrizzle.js");
|
||||
|
||||
// Step 1: Create test organization in database and get API key
|
||||
const autumnSecretKey = await createTestOrg({ db });
|
||||
@@ -12,7 +12,7 @@ const __dirname = dirname(__filename);
|
||||
* Find server/.env file robustly - works whether running from root or scripts dir
|
||||
*/
|
||||
function findEnvPath(): string {
|
||||
// Try from script directory (scripts/setup-test.ts -> ../server/.env)
|
||||
// Try from setupTestUtils directory (scripts/setupTestUtils/updateEnvFile.ts -> ../../server/.env)
|
||||
const fromScriptDir = resolve(__dirname, "../../server/.env");
|
||||
if (existsSync(fromScriptDir)) {
|
||||
return fromScriptDir;
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
import { exec, spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const VITE_PORT = 3000;
|
||||
const SERVER_PORT = 8080;
|
||||
|
||||
/**
|
||||
* Read environment variable from .env file
|
||||
*/
|
||||
function getEnvVariable(filePath, key) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith("#")) {
|
||||
const [envKey, ...valueParts] = trimmed.split("=");
|
||||
if (envKey && envKey.trim() === key) {
|
||||
return valueParts.join("=");
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the process info using a specific port
|
||||
*/
|
||||
async function getProcessOnPort(port) {
|
||||
try {
|
||||
const { stdout } = await execAsync(`lsof -ti:${port}`);
|
||||
const pid = stdout.trim();
|
||||
if (pid) {
|
||||
const { stdout: psOut } = await execAsync(`ps -p ${pid} -o comm=`);
|
||||
const processName = psOut.trim();
|
||||
return { pid: parseInt(pid), processName };
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a process by PID
|
||||
*/
|
||||
async function killProcess(pid) {
|
||||
try {
|
||||
await execAsync(`kill -9 ${pid}`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for confirmation
|
||||
*/
|
||||
function promptUser(question) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and kill processes on ports 3000 and 8080
|
||||
*/
|
||||
async function handlePorts() {
|
||||
const viteProcess = await getProcessOnPort(VITE_PORT);
|
||||
const serverProcess = await getProcessOnPort(SERVER_PORT);
|
||||
|
||||
const processesToKill = [];
|
||||
if (viteProcess) processesToKill.push({ port: VITE_PORT, ...viteProcess });
|
||||
if (serverProcess)
|
||||
processesToKill.push({ port: SERVER_PORT, ...serverProcess });
|
||||
|
||||
if (processesToKill.length === 0) {
|
||||
console.log("✅ Ports 3000 and 8080 are available\n");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n⚠️ Found processes on required ports:");
|
||||
for (const proc of processesToKill) {
|
||||
console.log(` Port ${proc.port}: ${proc.processName} (PID: ${proc.pid})`);
|
||||
}
|
||||
|
||||
const shouldKill = await promptUser(
|
||||
"\nKill these processes and continue? (y/n): ",
|
||||
);
|
||||
|
||||
if (!shouldKill) {
|
||||
console.log("❌ Aborted by user");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log("\n🔨 Killing processes...");
|
||||
for (const proc of processesToKill) {
|
||||
const killed = await killProcess(proc.pid);
|
||||
if (killed) {
|
||||
console.log(` ✅ Killed process ${proc.pid} on port ${proc.port}`);
|
||||
} else {
|
||||
console.log(
|
||||
` ❌ Failed to kill process ${proc.pid} on port ${proc.port}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for ports to be released
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
console.log("");
|
||||
}
|
||||
|
||||
async function startDev() {
|
||||
try {
|
||||
// Check if using remote backend (api.useautumn.com)
|
||||
const rootDir = path.dirname(new URL(import.meta.url).pathname);
|
||||
const projectRoot = path.join(rootDir, "..");
|
||||
const viteEnvPath = path.join(projectRoot, "vite", ".env");
|
||||
const backendUrl =
|
||||
process.env.VITE_BACKEND_URL ||
|
||||
getEnvVariable(viteEnvPath, "VITE_BACKEND_URL");
|
||||
const isUsingRemoteBackend = backendUrl?.includes("api.useautumn.com");
|
||||
|
||||
if (isUsingRemoteBackend) {
|
||||
console.log("\n🌐 Using remote backend (api.useautumn.com)");
|
||||
console.log("⏭️ Skipping port cleanup...\n");
|
||||
} else {
|
||||
// Port cleanup disabled (detection is unreliable)
|
||||
console.log("⏭️ Skipping port cleanup...\n");
|
||||
// await handlePorts();
|
||||
}
|
||||
|
||||
// Clear Vite cache to prevent dep optimization issues
|
||||
const viteCachePath = path.join(
|
||||
projectRoot,
|
||||
"vite",
|
||||
"node_modules",
|
||||
".vite",
|
||||
);
|
||||
if (fs.existsSync(viteCachePath)) {
|
||||
console.log("🧹 Clearing Vite cache...\n");
|
||||
fs.rmSync(viteCachePath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log("🚀 Starting development servers in watch mode...\n");
|
||||
|
||||
// Start server, workers, and vite (they'll use the shared package source files directly)
|
||||
const concurrentlyCmd = spawn(
|
||||
"bunx",
|
||||
[
|
||||
"concurrently",
|
||||
"-n",
|
||||
"server,workers,vite",
|
||||
"-c",
|
||||
"green,yellow,blue",
|
||||
`"cd server && SERVER_PORT=${SERVER_PORT} bun dev"`,
|
||||
`"cd server && bun workers:dev"`,
|
||||
`"cd vite && VITE_PORT=${VITE_PORT} bun dev"`,
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_PORT: VITE_PORT.toString(),
|
||||
SERVER_PORT: SERVER_PORT.toString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
concurrentlyCmd.on("error", (error) => {
|
||||
console.error("Failed to start development servers:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
concurrentlyCmd.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
console.error(`Development servers exited with code ${code}`);
|
||||
}
|
||||
process.exit(code);
|
||||
});
|
||||
|
||||
// Handle termination signals
|
||||
process.on("SIGINT", () => {
|
||||
console.log("\n\n🛑 Shutting down development servers...");
|
||||
concurrentlyCmd.kill("SIGINT");
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("\n\n🛑 Shutting down development servers...");
|
||||
concurrentlyCmd.kill("SIGTERM");
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error starting development servers:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startDev();
|
||||
@@ -15,16 +15,16 @@ fi
|
||||
# Run tests using TypeScript runner with compact mode
|
||||
# Adjust --max to control concurrency (default: 6)
|
||||
BUN_PARALLEL_COMPACT \
|
||||
'server/tests/balances/track/basic' \
|
||||
# 'server/tests/balances/track/concurrency' \
|
||||
'server/tests/balances/track/concurrency' \
|
||||
# 'server/tests/balances/track/allocated' \
|
||||
# 'server/tests/balances/check/basic' \
|
||||
# 'server/tests/balances/check/credit-systems' \
|
||||
# 'server/tests/balances/check/misc' \
|
||||
# 'server/tests/balances/track/basic' \
|
||||
# 'server/tests/balances/track/credit-systems' \
|
||||
# 'server/tests/balances/track/entity-balances' \
|
||||
# 'server/tests/balances/track/entity-products' \
|
||||
# 'server/tests/balances/track/legacy' \
|
||||
# 'server/tests/balances/check/basic' \
|
||||
# 'server/tests/balances/check/credit-systems' \
|
||||
# 'server/tests/balances/check/misc' \
|
||||
|
||||
# BUN_PARALLEL_COMPACT \
|
||||
# 'server/tests/attach/basic' \
|
||||
|
||||
16
server/nodemon.json
Normal file
16
server/nodemon.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"watch": ["../shared", "src"],
|
||||
"ext": "js,ts",
|
||||
"ignore": [
|
||||
"../shared/dist/**/*.d.ts",
|
||||
"../shared/node_modules",
|
||||
"../shared/scripts",
|
||||
"scripts",
|
||||
"tests"
|
||||
],
|
||||
"exec": "bun src/index.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"d": "ENV_FILE=.env infisical run --env=dev -- bun dev",
|
||||
"p": "ENV_FILE=.env.prod infisical run --env=prod -- bun dev",
|
||||
|
||||
"dev": "cross-env NODE_ENV=development bunx nodemon -w ../shared/dist -w src --ext js,ts --ignore '../shared/dist/**/*.d.ts' --ignore scripts --ignore tests --exec bun src/index.ts",
|
||||
"workers:dev": "cross-env NODE_ENV=development bunx nodemon --signal SIGTERM --delay 500ms -w ../shared/dist -w src/workers.ts -w src/queue -w src/internal --ext js,ts --ignore '../shared/dist/**/*.d.ts' --ignore scripts --ignore tests --exec bun src/workers.ts",
|
||||
"dev": "cross-env NODE_ENV=development bunx nodemon",
|
||||
"workers:dev": "cross-env NODE_ENV=development bunx nodemon --exec bun src/workers.ts --signal SIGTERM --delay 500ms",
|
||||
"workers": "bun src/workers.ts",
|
||||
"cron": "bun src/cron.ts",
|
||||
"check": "bun src/check.ts",
|
||||
|
||||
@@ -359,24 +359,30 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
|
||||
for index, breakdown in ipairs(cusFeature.breakdown) do
|
||||
if remaining <= 0 then break end
|
||||
|
||||
-- Check if this breakdown allows overage
|
||||
local breakdownAllowOverage = breakdown.overage_allowed or allowOverage
|
||||
-- Check if this breakdown explicitly allows overage
|
||||
-- Only deduct from breakdowns that have overage_allowed=true
|
||||
-- Don't fall back to top-level allowOverage - each breakdown controls its own overage
|
||||
local breakdownAllowOverage = breakdown.overage_allowed == true
|
||||
|
||||
if breakdownAllowOverage then
|
||||
local breakdownPurchasedBalance = breakdown.purchased_balance or 0
|
||||
local breakdownMaxPurchase = breakdown.max_purchase or (cusFeature.max_purchase or 0)
|
||||
-- Calculate availableCapacity: nil if breakdown.max_purchase is nil/null (unlimited), otherwise max_purchase - purchased_balance
|
||||
local availableCapacity
|
||||
if breakdown.max_purchase == nil or breakdown.max_purchase == cjson.null then
|
||||
-- No max_purchase limit - unlimited capacity
|
||||
availableCapacity = nil
|
||||
else
|
||||
-- Use breakdown max_purchase limit
|
||||
local breakdownMaxPurchase = toNum(breakdown.max_purchase)
|
||||
availableCapacity = breakdownMaxPurchase - breakdownPurchasedBalance
|
||||
end
|
||||
|
||||
-- Calculate how much we can increment purchased_balance (up to max_purchase)
|
||||
local availableCapacity = breakdownMaxPurchase - breakdownPurchasedBalance
|
||||
|
||||
if availableCapacity > 0 then
|
||||
local toIncrement = math.min(remaining, availableCapacity)
|
||||
if availableCapacity == nil or availableCapacity > 0 then
|
||||
local toIncrement = availableCapacity == nil and remaining or math.min(remaining, availableCapacity)
|
||||
|
||||
-- Collect Redis deltas
|
||||
table.insert(deltas, {key = breakdown._key, field = "purchased_balance", delta = toIncrement})
|
||||
table.insert(deltas, {key = breakdown._key, field = "current_balance", delta = toIncrement})
|
||||
table.insert(deltas, {key = cusFeature._key, field = "purchased_balance", delta = toIncrement})
|
||||
table.insert(deltas, {key = cusFeature._key, field = "current_balance", delta = toIncrement})
|
||||
|
||||
-- Either increment usage or decrement granted_balance based on flag
|
||||
if adjustGrantedBalance then
|
||||
@@ -394,12 +400,6 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
|
||||
field = "purchased_balance",
|
||||
delta = toIncrement
|
||||
})
|
||||
table.insert(stateChanges, {
|
||||
type = "breakdown",
|
||||
index = index,
|
||||
field = "current_balance",
|
||||
delta = toIncrement
|
||||
})
|
||||
if adjustGrantedBalance then
|
||||
table.insert(stateChanges, {
|
||||
type = "breakdown",
|
||||
@@ -430,11 +430,6 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
|
||||
field = "purchased_balance",
|
||||
delta = toIncrement
|
||||
})
|
||||
table.insert(stateChanges, {
|
||||
type = "cusFeature",
|
||||
field = "current_balance",
|
||||
delta = toIncrement
|
||||
})
|
||||
|
||||
remaining = remaining - toIncrement
|
||||
end
|
||||
@@ -443,17 +438,22 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
|
||||
else
|
||||
-- No breakdowns: deduct from top-level overage
|
||||
local topLevelPurchasedBalance = cusFeature.purchased_balance or 0
|
||||
local topLevelMaxPurchase = cusFeature.max_purchase or 0
|
||||
-- Calculate availableCapacity: nil if max_purchase is nil/null (unlimited), otherwise max_purchase - purchased_balance
|
||||
local availableCapacity
|
||||
if cusFeature.max_purchase == nil or cusFeature.max_purchase == cjson.null then
|
||||
-- No max_purchase limit - unlimited capacity
|
||||
availableCapacity = nil
|
||||
else
|
||||
-- Use max_purchase limit
|
||||
local topLevelMaxPurchase = toNum(cusFeature.max_purchase)
|
||||
availableCapacity = topLevelMaxPurchase - topLevelPurchasedBalance
|
||||
end
|
||||
|
||||
-- Calculate how much we can increment purchased_balance (up to max_purchase)
|
||||
local availableCapacity = topLevelMaxPurchase - topLevelPurchasedBalance
|
||||
|
||||
if availableCapacity > 0 then
|
||||
local toIncrement = math.min(remaining, availableCapacity)
|
||||
if availableCapacity == nil or availableCapacity > 0 then
|
||||
local toIncrement = availableCapacity == nil and remaining or math.min(remaining, availableCapacity)
|
||||
|
||||
-- Collect Redis deltas
|
||||
table.insert(deltas, {key = cusFeature._key, field = "purchased_balance", delta = toIncrement})
|
||||
table.insert(deltas, {key = cusFeature._key, field = "current_balance", delta = toIncrement})
|
||||
|
||||
-- Either increment usage or decrement granted_balance based on flag
|
||||
if adjustGrantedBalance then
|
||||
@@ -468,11 +468,6 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
|
||||
field = "purchased_balance",
|
||||
delta = toIncrement
|
||||
})
|
||||
table.insert(stateChanges, {
|
||||
type = "cusFeature",
|
||||
field = "current_balance",
|
||||
delta = toIncrement
|
||||
})
|
||||
if adjustGrantedBalance then
|
||||
table.insert(stateChanges, {
|
||||
type = "cusFeature",
|
||||
|
||||
11
server/src/external/autumn/autumnCli.ts
vendored
11
server/src/external/autumn/autumnCli.ts
vendored
@@ -7,6 +7,7 @@ import {
|
||||
type ApiEntity,
|
||||
type AttachBody,
|
||||
type BalancesUpdateParams,
|
||||
type CheckQuery,
|
||||
type CreateCustomerParams,
|
||||
type CreateEntityParams,
|
||||
type CreateRewardProgram,
|
||||
@@ -557,8 +558,14 @@ export class AutumnInt {
|
||||
return data;
|
||||
};
|
||||
|
||||
check = async (params: CheckParams): Promise<CheckResult> => {
|
||||
const data = await this.post(`/check`, params);
|
||||
check = async <T = CheckResult>(
|
||||
params: CheckParams & CheckQuery,
|
||||
): Promise<T> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.skip_cache) {
|
||||
queryParams.append("skip_cache", "true");
|
||||
}
|
||||
const data = await this.post(`/check?${queryParams.toString()}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
||||
|
||||
// Query params
|
||||
expand: [],
|
||||
skipCache: false,
|
||||
});
|
||||
|
||||
// childLogger.info(`${method} ${path}`);
|
||||
|
||||
@@ -47,7 +47,16 @@ const ROUTE_SPECIFIC_RULES: Array<{
|
||||
name: string;
|
||||
match: (err: Error, c: Context<HonoEnv>) => boolean;
|
||||
statusCode: ContentfulStatusCode;
|
||||
}> = [];
|
||||
}> = [
|
||||
{
|
||||
name: "Stripe webhook secret not set in development",
|
||||
match: (err: Error) =>
|
||||
err.message.includes(
|
||||
"STRIPE_WEBHOOK_SECRET env variable is not set (live)",
|
||||
) && process.env.NODE_ENV === "development",
|
||||
statusCode: 500,
|
||||
},
|
||||
];
|
||||
|
||||
/** Stripe-specific error handling rules */
|
||||
const STRIPE_RULES = [
|
||||
@@ -224,13 +233,16 @@ export const handleErrorSkip = (err: Error, c: Context<HonoEnv>) => {
|
||||
// 3. Check advanced route-specific rules
|
||||
for (const rule of ROUTE_SPECIFIC_RULES) {
|
||||
if (rule.match(err, c)) {
|
||||
const recaseErr = err as RecaseError;
|
||||
logger.warn(`${recaseErr.message}, org: ${ctx.org?.slug || "unknown"}`);
|
||||
const errorCode =
|
||||
err instanceof RecaseError || err instanceof SharedRecaseError
|
||||
? err.code
|
||||
: ErrCode.InternalError;
|
||||
logger.warn(`${rule.name}, org: ${ctx.org?.slug || "unknown"}`);
|
||||
return createErrorResponse({
|
||||
c,
|
||||
ctx,
|
||||
message: recaseErr.message,
|
||||
code: recaseErr.code,
|
||||
message: err.message,
|
||||
code: errorCode,
|
||||
statusCode: rule.statusCode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,17 +4,21 @@ import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
/**
|
||||
* Extracts expand from validated query and sets it in context.
|
||||
* Must run AFTER versionedValidator so expand has been transformed.
|
||||
* Uses c.req.query("expand") to access the parsed expand field.
|
||||
* Uses c.req.valid("query") to access the transformed/validated expand field.
|
||||
*/
|
||||
export const expandMiddleware = (): MiddlewareHandler<HonoEnv> => {
|
||||
return async (c, next) => {
|
||||
// Query is parsed by queryMiddleware and validated by versionedValidator/validator
|
||||
// queryStringArray normalizes expand to an array during validation, but we access
|
||||
// the parsed query which may still be a string for single values
|
||||
const expandValue = c.req.query("expand");
|
||||
// queryMiddleware converts "true"/"false" strings to boolean values
|
||||
// Handle both boolean (from queryMiddleware) and string (fallback) cases
|
||||
const skipCacheQuery = c.req.query("skip_cache");
|
||||
// Get validated query (which includes transformed values from version changes)
|
||||
// Fallback to raw query if validation hasn't happened yet
|
||||
const validatedQuery = (c.req as any).valid?.("query") as
|
||||
| { expand?: string | string[]; skip_cache?: boolean }
|
||||
| undefined;
|
||||
const rawQuery = c.req.query();
|
||||
|
||||
// Prefer validated query (transformed), fallback to raw query
|
||||
const expandValue = validatedQuery?.expand ?? rawQuery?.expand;
|
||||
const skipCacheQuery = validatedQuery?.skip_cache ?? rawQuery?.skip_cache;
|
||||
|
||||
const skipCacheValue =
|
||||
(typeof skipCacheQuery === "boolean" && skipCacheQuery === true) ||
|
||||
(typeof skipCacheQuery === "string" && skipCacheQuery === "true");
|
||||
|
||||
@@ -72,7 +72,7 @@ export const refreshCacheMiddleware = async (
|
||||
}
|
||||
|
||||
const ctx = c.get("ctx");
|
||||
const { logger, db, org, env } = ctx;
|
||||
const { logger, org, env } = ctx;
|
||||
|
||||
const pathname = new URL(c.req.url).pathname.replace("/v1", "");
|
||||
const method = c.req.method;
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type { Feature, FullCusEntWithFullCusProduct } from "@autumn/shared";
|
||||
import {
|
||||
type ApiBalance,
|
||||
type ApiCustomer,
|
||||
type ApiEntity,
|
||||
cusEntToPrepaidQuantity,
|
||||
cusProductsToCusEnts,
|
||||
filterEntityLevelCusProducts,
|
||||
filterOutEntitiesFromCusProducts,
|
||||
getRelevantFeatures,
|
||||
orgToInStatuses,
|
||||
sumValues,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
@@ -24,12 +30,35 @@ export interface SyncItem {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const apiToBackendBalance = ({ apiBalance }: { apiBalance?: ApiBalance }) => {
|
||||
if (!apiBalance) {
|
||||
return undefined;
|
||||
}
|
||||
const apiToBackendBalance = ({
|
||||
cusEnts,
|
||||
features,
|
||||
apiBalance,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
features: Feature[];
|
||||
apiBalance?: ApiBalance;
|
||||
}) => {
|
||||
const feature = features.find((f) => f.id === apiBalance?.feature_id);
|
||||
if (!apiBalance || !feature) return 0;
|
||||
|
||||
const totalPrepaidQuantity = sumValues(
|
||||
cusEnts.map((cusEnt) => cusEntToPrepaidQuantity({ cusEnt })),
|
||||
);
|
||||
|
||||
const backendBalance = new Decimal(totalPrepaidQuantity)
|
||||
.add(apiBalance.current_balance)
|
||||
.sub(apiBalance.purchased_balance)
|
||||
.toNumber();
|
||||
|
||||
// console.log("Converting api balance to backend balance");
|
||||
// console.log(`Current balance: ${apiBalance.current_balance}`);
|
||||
// console.log(`Purchased balance: ${apiBalance.purchased_balance}`);
|
||||
// console.log(`Total prepaid quantity: ${totalPrepaidQuantity}`);
|
||||
// console.log(`Backend balance: ${backendBalance}`);
|
||||
|
||||
// 1. Current balance = granted balance + purchased balance - usage
|
||||
return apiBalance.current_balance - apiBalance.purchased_balance;
|
||||
return backendBalance;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -49,6 +78,7 @@ export const syncItem = async ({
|
||||
// Get cached customer/entity from Redis WITHOUT merging
|
||||
// For sync, we need the raw balance for that specific scope (not merged)
|
||||
let redisEntity: ApiCustomer | ApiEntity;
|
||||
|
||||
if (entityId) {
|
||||
const { apiEntity } = await getCachedApiEntity({
|
||||
ctx,
|
||||
@@ -103,7 +133,19 @@ export const syncItem = async ({
|
||||
const redisBalance = redisEntity.balances?.[relevantFeature.id];
|
||||
if (!redisBalance) continue;
|
||||
|
||||
const backendBalance = apiToBackendBalance({ apiBalance: redisBalance });
|
||||
const cusEnts = cusProductsToCusEnts({
|
||||
cusProducts: fullCus.customer_products,
|
||||
featureIds: relevantFeatures.map((f) => f.id),
|
||||
reverseOrder: org.config?.reverse_deduction_order,
|
||||
entity: fullCus.entity,
|
||||
inStatuses: orgToInStatuses({ org }),
|
||||
});
|
||||
|
||||
const backendBalance = apiToBackendBalance({
|
||||
apiBalance: redisBalance,
|
||||
cusEnts,
|
||||
features: relevantFeatures,
|
||||
});
|
||||
|
||||
featureDeductions.push({
|
||||
feature: relevantFeature,
|
||||
|
||||
@@ -32,16 +32,6 @@ export const cusEntMatchesEntity = ({
|
||||
return cusProductMatch && entityFeatureIdMatch;
|
||||
};
|
||||
|
||||
export const cusEntMatchesFeature = ({
|
||||
cusEnt,
|
||||
feature,
|
||||
}: {
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
feature: Feature;
|
||||
}) => {
|
||||
return cusEnt.entitlement.feature.internal_id === feature.internal_id;
|
||||
};
|
||||
|
||||
export const findMainCusEntForFeature = ({
|
||||
cusEnts,
|
||||
feature,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
type ApiBalanceReset,
|
||||
type ApiBalanceRollover,
|
||||
type ApiFeature,
|
||||
cusEntToKey,
|
||||
entIntvToResetIntv,
|
||||
type Feature,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
@@ -36,15 +35,16 @@ export const cusEntsToReset = ({
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
feature: Feature;
|
||||
}): ApiBalanceReset | undefined => {
|
||||
// 1. If feature is allocated, undefined
|
||||
if (isContUseFeature({ feature })) return undefined;
|
||||
}): ApiBalanceReset | null => {
|
||||
// 1. If feature is allocated, null
|
||||
if (isContUseFeature({ feature })) return null;
|
||||
|
||||
const cusEntKeys = cusEnts.map((cusEnt) => cusEntToKey({ cusEnt }));
|
||||
const uniqueCusEntKeys = [...new Set(cusEntKeys)];
|
||||
// Check if there are multiple intervals
|
||||
const uniqueIntervals = [
|
||||
...new Set(cusEnts.map((cusEnt) => cusEnt.entitlement.interval)),
|
||||
];
|
||||
|
||||
// 2. If > 1 cus ent key, return multiple
|
||||
if (uniqueCusEntKeys.length > 1) {
|
||||
if (uniqueIntervals.length > 1) {
|
||||
return { interval: "multiple", interval_count: undefined, resets_at: null };
|
||||
}
|
||||
|
||||
@@ -108,10 +108,10 @@ export const getBooleanApiBalance = ({
|
||||
current_balance: 0,
|
||||
usage: 0,
|
||||
|
||||
max_purchase: 0,
|
||||
overage_allowed: false,
|
||||
max_purchase: null,
|
||||
reset: null,
|
||||
|
||||
reset: undefined,
|
||||
breakdown: undefined,
|
||||
rollovers: undefined,
|
||||
} satisfies ApiBalance;
|
||||
@@ -137,10 +137,10 @@ export const getUnlimitedApiBalance = ({
|
||||
current_balance: 0,
|
||||
usage: 0,
|
||||
|
||||
max_purchase: 0,
|
||||
reset: null,
|
||||
max_purchase: null,
|
||||
overage_allowed: false,
|
||||
|
||||
reset: undefined,
|
||||
breakdown: undefined,
|
||||
rollovers: undefined,
|
||||
};
|
||||
@@ -163,10 +163,10 @@ export const getNoCusEntsApiBalance = ({
|
||||
current_balance: 0,
|
||||
usage: 0,
|
||||
|
||||
max_purchase: 0,
|
||||
reset: null,
|
||||
max_purchase: null,
|
||||
overage_allowed: false,
|
||||
|
||||
reset: undefined,
|
||||
breakdown: undefined,
|
||||
rollovers: undefined,
|
||||
};
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
ApiBalanceBreakdownSchema,
|
||||
ApiBalanceSchema,
|
||||
CusExpand,
|
||||
cusEntMatchesFeature,
|
||||
cusEntsToMaxPurchase,
|
||||
cusEntToBalance,
|
||||
cusEntToCusPrice,
|
||||
cusEntToGrantedBalance,
|
||||
cusEntToKey,
|
||||
cusEntToMaxPurchase,
|
||||
cusEntToPurchasedBalance,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
@@ -26,7 +27,6 @@ import { Decimal } from "decimal.js";
|
||||
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import type { CusFeatureLegacyData } from "../../../../../../../shared/api/customers/cusFeatures/cusFeatureLegacyData.js";
|
||||
import { cusEntMatchesFeature } from "../../../cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
|
||||
import {
|
||||
cusEntsToReset,
|
||||
cusEntsToRollovers,
|
||||
@@ -85,14 +85,14 @@ const cusEntsToBreakdown = ({
|
||||
return breakdown;
|
||||
};
|
||||
|
||||
const cusEntsToPrepaidQuantity = ({
|
||||
export const cusEntsToPrepaidQuantity = ({
|
||||
cusEnts,
|
||||
feature,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
feature: Feature;
|
||||
}) => {
|
||||
const prepaidQuantity = new Decimal(0);
|
||||
let prepaidQuantity = new Decimal(0);
|
||||
|
||||
for (const cusEnt of cusEnts) {
|
||||
// 1. if cus ent doesn't match feature, skip
|
||||
@@ -100,18 +100,21 @@ const cusEntsToPrepaidQuantity = ({
|
||||
|
||||
// 2. If cus ent is not prepaid, skip
|
||||
const cusPrice = cusEntToCusPrice({ cusEnt });
|
||||
|
||||
if (!cusPrice || !isPrepaidPrice({ price: cusPrice.price })) continue;
|
||||
|
||||
// 3. Get quantity
|
||||
const options = cusEnt.customer_product.options.find(
|
||||
(option) => option.internal_feature_id === feature.internal_id,
|
||||
);
|
||||
|
||||
if (!options) continue;
|
||||
|
||||
const quantityWithUnits = new Decimal(options.quantity)
|
||||
.mul(cusPrice.price.config.billing_units ?? 1)
|
||||
.toNumber();
|
||||
prepaidQuantity.add(quantityWithUnits);
|
||||
|
||||
prepaidQuantity = prepaidQuantity.add(quantityWithUnits);
|
||||
}
|
||||
|
||||
return prepaidQuantity.toNumber();
|
||||
@@ -180,9 +183,7 @@ export const getApiBalance = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const totalMaxPurchase = sumValues(
|
||||
cusEnts.map((cusEnt) => cusEntToMaxPurchase({ cusEnt })),
|
||||
);
|
||||
const totalMaxPurchase = cusEntsToMaxPurchase({ cusEnts, entityId });
|
||||
|
||||
// 1. Granted balance
|
||||
const totalGrantedBalanceWithRollovers = sumValues(
|
||||
@@ -260,19 +261,19 @@ export const getApiBalance = ({
|
||||
usage: totalUsage,
|
||||
|
||||
// Max purchase...
|
||||
max_purchase: totalMaxPurchase ?? 0,
|
||||
overage_allowed: usageAllowed ?? false,
|
||||
|
||||
max_purchase: totalMaxPurchase,
|
||||
reset: reset,
|
||||
breakdown: cusEntsToBreakdown({ ctx, fullCus, cusEnts }),
|
||||
rollovers,
|
||||
});
|
||||
} satisfies ApiBalance);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Return in latest format - version transformation happens at Customer level
|
||||
const totalPrepaidQuantity = cusEntsToPrepaidQuantity({ cusEnts, feature });
|
||||
console.log("Total prepaid quantity:", totalPrepaidQuantity);
|
||||
|
||||
return {
|
||||
data: apiBalance,
|
||||
legacyData: {
|
||||
|
||||
@@ -17,14 +17,12 @@ export const getApiCustomer = async ({
|
||||
withAutumnId = false,
|
||||
customerId,
|
||||
fullCus,
|
||||
skipCache = false,
|
||||
baseData,
|
||||
}: {
|
||||
ctx: RequestContext;
|
||||
withAutumnId?: boolean;
|
||||
customerId?: string;
|
||||
fullCus?: FullCustomer;
|
||||
skipCache?: boolean;
|
||||
baseData?: { apiCustomer: ApiCustomer; legacyData: CustomerLegacyData };
|
||||
}) => {
|
||||
let baseCustomer: ApiCustomer;
|
||||
@@ -34,7 +32,6 @@ export const getApiCustomer = async ({
|
||||
const { apiCustomer, legacyData } = await getCachedApiCustomer({
|
||||
ctx,
|
||||
customerId: customerId || "",
|
||||
skipCache,
|
||||
});
|
||||
baseCustomer = apiCustomer;
|
||||
cusLegacyData = legacyData;
|
||||
|
||||
@@ -18,7 +18,7 @@ export const handleGetCustomerV2 = createRoute({
|
||||
handler: async (c) => {
|
||||
const ctx = c.get("ctx");
|
||||
const customerId = c.req.param("customer_id");
|
||||
const { env, db, logger, org, expand } = ctx;
|
||||
const { expand } = ctx;
|
||||
const { skip_cache = false, with_autumn_id } = c.req.valid("query");
|
||||
|
||||
// SIDE EFFECT
|
||||
@@ -34,8 +34,6 @@ export const handleGetCustomerV2 = createRoute({
|
||||
const customer = await getApiCustomer({
|
||||
ctx,
|
||||
customerId,
|
||||
|
||||
skipCache: skip_cache,
|
||||
withAutumnId: with_autumn_id,
|
||||
});
|
||||
|
||||
|
||||
@@ -129,15 +129,15 @@ export const normalizeCachedData = <T extends ApiCustomer | ApiEntity>(
|
||||
for (const featureId in data.balances) {
|
||||
const feature = data.balances[featureId];
|
||||
|
||||
if (!feature.reset) {
|
||||
feature.reset = undefined;
|
||||
}
|
||||
// if (!feature.reset) {
|
||||
// feature.reset = null;
|
||||
// }
|
||||
|
||||
if (feature.breakdown) {
|
||||
for (const breakdown of feature.breakdown) {
|
||||
if (!breakdown.reset) {
|
||||
breakdown.reset = undefined;
|
||||
}
|
||||
// if (!breakdown.reset) {
|
||||
// breakdown.reset = null;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
type CheckResponseV2,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
type CheckResponseV2,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
@@ -75,7 +75,7 @@ describe(`${chalk.yellowBright("check2: test /check on boolean feature")}`, () =
|
||||
purchased_balance: 0,
|
||||
current_balance: 0,
|
||||
usage: 0,
|
||||
max_purchase: 0,
|
||||
max_purchase: null,
|
||||
overage_allowed: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
ResetInterval,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
@@ -72,7 +72,7 @@ describe(`${chalk.yellowBright("check3: test /check on metered feature")}`, () =
|
||||
purchased_balance: 0,
|
||||
current_balance: 1000,
|
||||
usage: 0,
|
||||
max_purchase: 0,
|
||||
max_purchase: null,
|
||||
overage_allowed: false,
|
||||
reset: {
|
||||
interval: ResetInterval.Month,
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
ResetInterval,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
@@ -73,7 +73,7 @@ describe(`${chalk.yellowBright("check5: test /check on usage-based feature")}`,
|
||||
purchased_balance: 0,
|
||||
current_balance: messagesFeature.included_usage,
|
||||
usage: 0,
|
||||
max_purchase: 0,
|
||||
max_purchase: null,
|
||||
overage_allowed: true,
|
||||
reset: {
|
||||
interval: ResetInterval.Month,
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
ResetInterval,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
constructArrearItem,
|
||||
@@ -77,7 +77,8 @@ describe(`${chalk.yellowBright("check6: test /check on feature with multiple bal
|
||||
purchased_balance: 0,
|
||||
current_balance: 1000,
|
||||
usage: 0,
|
||||
max_purchase: 0,
|
||||
max_purchase: null,
|
||||
overage_allowed: false,
|
||||
reset: {
|
||||
interval: ResetInterval.OneOff,
|
||||
resets_at: null,
|
||||
@@ -89,7 +90,7 @@ describe(`${chalk.yellowBright("check6: test /check on feature with multiple bal
|
||||
purchased_balance: 0,
|
||||
current_balance: 100,
|
||||
usage: 0,
|
||||
max_purchase: 0,
|
||||
max_purchase: null,
|
||||
reset: {
|
||||
interval: ResetInterval.Month,
|
||||
},
|
||||
@@ -115,7 +116,7 @@ describe(`${chalk.yellowBright("check6: test /check on feature with multiple bal
|
||||
current_balance:
|
||||
monthlyMessages.included_usage + lifetimeMessages.included_usage,
|
||||
usage: 0,
|
||||
max_purchase: 0,
|
||||
max_purchase: null,
|
||||
overage_allowed: true,
|
||||
reset: {
|
||||
interval: "multiple",
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
type LimitedItem,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
featureToCreditSystem,
|
||||
@@ -88,7 +88,7 @@ describe(`${chalk.yellowBright("credit-systems1: test /check on action that uses
|
||||
purchased_balance: 0,
|
||||
current_balance: creditsFeature.included_usage,
|
||||
usage: 0,
|
||||
max_purchase: 0,
|
||||
max_purchase: null,
|
||||
overage_allowed: false,
|
||||
reset: {
|
||||
interval: "month",
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
type LimitedItem,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { LegacyVersion, type LimitedItem } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
@@ -35,7 +35,6 @@ describe(`${chalk.yellowBright("check-misc1: Checking credit systems")}`, () =>
|
||||
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
|
||||
|
||||
beforeAll(async () => {
|
||||
const { customer: customer_, testClockId: testClockId_ } =
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, CusExpand } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
ApiVersion,
|
||||
type CheckResponseV0,
|
||||
type CheckResponseV1,
|
||||
type CheckResponseV2,
|
||||
type LimitedItem,
|
||||
@@ -32,7 +31,6 @@ const testCase = "check-prepaid1";
|
||||
|
||||
describe(`${chalk.yellowBright("check-prepaid1: test /check when prepaid feature attached")}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumnV0: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
|
||||
|
||||
@@ -81,14 +79,16 @@ describe(`${chalk.yellowBright("check-prepaid1: test /check when prepaid feature
|
||||
current_balance:
|
||||
prepaidQuantity + prepaidMessagesFeature.included_usage,
|
||||
usage: 0,
|
||||
max_purchase: 0,
|
||||
|
||||
max_purchase: null,
|
||||
overage_allowed: false,
|
||||
reset: {
|
||||
interval: "month",
|
||||
// resets_at: 1765462114000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.balance?.reset?.resets_at).toBeDefined();
|
||||
});
|
||||
|
||||
test("should have allowed true if value is less than current balance", async () => {
|
||||
@@ -119,26 +119,19 @@ describe(`${chalk.yellowBright("check-prepaid1: test /check when prepaid feature
|
||||
feature_id: TestFeature.Messages,
|
||||
})) as unknown as CheckResponseV1;
|
||||
|
||||
console.log("Res:", res);
|
||||
|
||||
// expect(res).toStrictEqual({
|
||||
// allowed: false,
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Messages,
|
||||
// required_balance: 1,
|
||||
// code: SuccessCode.FeatureFound,
|
||||
// });
|
||||
});
|
||||
return;
|
||||
|
||||
test("should have correct v0 response", async () => {
|
||||
const res = (await autumnV0.check({
|
||||
expect(res).toMatchObject({
|
||||
allowed: true,
|
||||
code: "feature_found",
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
})) as unknown as CheckResponseV0;
|
||||
|
||||
expect(res.allowed).toBe(false);
|
||||
expect(res.balances).toBeDefined();
|
||||
expect(res.balances).toHaveLength(0);
|
||||
required_balance: 1,
|
||||
interval: "month",
|
||||
interval_count: 1,
|
||||
unlimited: false,
|
||||
balance: prepaidQuantity + prepaidMessagesFeature.included_usage,
|
||||
usage: 0,
|
||||
included_usage: prepaidQuantity + prepaidMessagesFeature.included_usage,
|
||||
overage_allowed: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
288
server/tests/balances/check/prepaid/check-prepaid2.test.ts
Normal file
288
server/tests/balances/check/prepaid/check-prepaid2.test.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
ApiVersion,
|
||||
type CheckResponseV1,
|
||||
type CheckResponseV2,
|
||||
type LimitedItem,
|
||||
} from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
constructArrearItem,
|
||||
constructPrepaidItem,
|
||||
} from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { timeout } from "../../../utils/genUtils";
|
||||
|
||||
const prepaidItem = constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
billingUnits: 100,
|
||||
price: 8.5,
|
||||
}) as LimitedItem;
|
||||
|
||||
const usageItem = constructArrearItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 200,
|
||||
price: 0.5,
|
||||
billingUnits: 1,
|
||||
usageLimit: 500,
|
||||
}) as LimitedItem;
|
||||
|
||||
const prod = constructProduct({
|
||||
type: "free",
|
||||
isDefault: false,
|
||||
items: [prepaidItem, usageItem],
|
||||
});
|
||||
|
||||
const testCase = "check-prepaid2";
|
||||
|
||||
describe(`${chalk.yellowBright("check-prepaid2: test /check on prepaid + pay per use feature")}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
|
||||
|
||||
const prepaidQuantity = 500;
|
||||
const grantedBalance = prepaidItem.included_usage + usageItem.included_usage;
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
withTestClock: false,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [prod],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: prod.id,
|
||||
options: [
|
||||
{
|
||||
feature_id: TestFeature.Messages,
|
||||
quantity: prepaidQuantity,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct v2 response for empty usage", async () => {
|
||||
const res = (await autumnV2.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
})) as unknown as CheckResponseV2;
|
||||
|
||||
expect(res).toMatchObject({
|
||||
allowed: true,
|
||||
customer_id: customerId,
|
||||
required_balance: 1,
|
||||
balance: {
|
||||
feature_id: TestFeature.Messages,
|
||||
unlimited: false,
|
||||
granted_balance: grantedBalance,
|
||||
purchased_balance: prepaidQuantity,
|
||||
current_balance: prepaidQuantity + grantedBalance,
|
||||
usage: 0,
|
||||
max_purchase: null,
|
||||
overage_allowed: true,
|
||||
reset: {
|
||||
interval: prepaidItem.interval,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.balance?.reset?.resets_at).toBeDefined();
|
||||
|
||||
const expectedPrepaidBreakdown = {
|
||||
granted_balance: prepaidItem.included_usage,
|
||||
purchased_balance: prepaidQuantity,
|
||||
current_balance: prepaidQuantity + prepaidItem.included_usage,
|
||||
usage: 0,
|
||||
max_purchase: null,
|
||||
overage_allowed: false,
|
||||
reset: {
|
||||
interval: "month",
|
||||
},
|
||||
};
|
||||
|
||||
const expectedUsageBreakdown = {
|
||||
granted_balance: usageItem.included_usage,
|
||||
purchased_balance: 0,
|
||||
current_balance: usageItem.included_usage,
|
||||
usage: 0,
|
||||
max_purchase: 300,
|
||||
overage_allowed: true,
|
||||
reset: {
|
||||
interval: "month",
|
||||
},
|
||||
};
|
||||
|
||||
expect(res.balance?.breakdown).toHaveLength(2);
|
||||
expect(res.balance?.breakdown?.[0]).toMatchObject(expectedPrepaidBreakdown);
|
||||
expect(res.balance?.breakdown?.[1]).toMatchObject(expectedUsageBreakdown);
|
||||
});
|
||||
|
||||
test("should have correct v1 response for empty usage", async () => {
|
||||
const res = (await autumnV1.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
})) as unknown as CheckResponseV1;
|
||||
|
||||
expect(res).toMatchObject({
|
||||
allowed: true,
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
required_balance: 1,
|
||||
interval: prepaidItem.interval,
|
||||
unlimited: false,
|
||||
included_usage: prepaidQuantity + grantedBalance,
|
||||
balance: prepaidQuantity + grantedBalance,
|
||||
usage: 0,
|
||||
});
|
||||
});
|
||||
|
||||
let curUsage = 0;
|
||||
test("should track 500 and verify check response uses prepaid balance first", async () => {
|
||||
await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 500,
|
||||
});
|
||||
|
||||
const res = (await autumnV2.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
})) as unknown as CheckResponseV2;
|
||||
|
||||
const expectedCurrentBalance = prepaidQuantity + grantedBalance - 500;
|
||||
curUsage = 500;
|
||||
|
||||
const balance = res.balance;
|
||||
|
||||
expect(balance?.granted_balance).toBe(grantedBalance);
|
||||
expect(balance?.current_balance).toBe(expectedCurrentBalance);
|
||||
expect(balance?.usage).toBe(curUsage);
|
||||
expect(balance?.purchased_balance).toBe(500);
|
||||
|
||||
const prepaidBreakdown = res.balance?.breakdown?.[0];
|
||||
expect(prepaidBreakdown).toMatchObject({
|
||||
granted_balance: prepaidItem.included_usage,
|
||||
purchased_balance: prepaidQuantity,
|
||||
current_balance: prepaidQuantity + prepaidItem.included_usage - curUsage,
|
||||
usage: curUsage,
|
||||
});
|
||||
});
|
||||
|
||||
// Balances at this point:
|
||||
test("should track another 500 -- 100 from prepaid, 200 from usage-based granted, 200 paid", async () => {
|
||||
await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 500,
|
||||
});
|
||||
|
||||
const res = (await autumnV2.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
})) as unknown as CheckResponseV2;
|
||||
|
||||
curUsage = curUsage + 500;
|
||||
|
||||
const balance = res.balance;
|
||||
|
||||
expect(balance).toMatchObject({
|
||||
granted_balance: grantedBalance,
|
||||
current_balance: 0,
|
||||
usage: curUsage,
|
||||
purchased_balance: prepaidQuantity + 200,
|
||||
});
|
||||
|
||||
const prepaidBreakdown = res.balance?.breakdown?.[0];
|
||||
expect(prepaidBreakdown).toMatchObject({
|
||||
granted_balance: prepaidItem.included_usage,
|
||||
purchased_balance: prepaidQuantity,
|
||||
current_balance: 0,
|
||||
usage: 600,
|
||||
});
|
||||
|
||||
const usageBreakdown = res.balance?.breakdown?.[1];
|
||||
expect(usageBreakdown).toMatchObject({
|
||||
granted_balance: usageItem.included_usage,
|
||||
purchased_balance: 200,
|
||||
current_balance: 0,
|
||||
usage: 400,
|
||||
});
|
||||
});
|
||||
|
||||
test("should track another 200 and only 100 used due to usage limit", async () => {
|
||||
await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 200,
|
||||
});
|
||||
|
||||
const res = (await autumnV2.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
})) as unknown as CheckResponseV2;
|
||||
|
||||
curUsage = curUsage + 100;
|
||||
|
||||
const balance = res.balance;
|
||||
expect(balance).toMatchObject({
|
||||
usage: curUsage,
|
||||
purchased_balance: prepaidQuantity + 300,
|
||||
});
|
||||
|
||||
const usageBreakdown = res.balance?.breakdown?.[1];
|
||||
expect(usageBreakdown).toMatchObject({
|
||||
granted_balance: usageItem.included_usage,
|
||||
purchased_balance: 300,
|
||||
current_balance: 0,
|
||||
usage: 500,
|
||||
max_purchase: 300,
|
||||
});
|
||||
});
|
||||
|
||||
test("should check that non-cached customer returns correct response", async () => {
|
||||
await timeout(2000);
|
||||
const res = (await autumnV2.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
skip_cache: true,
|
||||
})) as unknown as CheckResponseV2;
|
||||
|
||||
expect(res.balance).toMatchObject({
|
||||
granted_balance: grantedBalance,
|
||||
current_balance: 0,
|
||||
usage: curUsage,
|
||||
purchased_balance: prepaidQuantity + 300,
|
||||
});
|
||||
|
||||
const prepaidBreakdown = res.balance?.breakdown?.[0];
|
||||
expect(prepaidBreakdown).toMatchObject({
|
||||
granted_balance: prepaidItem.included_usage,
|
||||
purchased_balance: prepaidQuantity,
|
||||
current_balance: 0,
|
||||
usage: 600,
|
||||
});
|
||||
|
||||
const usageBreakdown = res.balance?.breakdown?.[1];
|
||||
expect(usageBreakdown).toMatchObject({
|
||||
granted_balance: usageItem.included_usage,
|
||||
purchased_balance: 300,
|
||||
current_balance: 0,
|
||||
usage: 500,
|
||||
max_purchase: 300,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, type TrackResponseV2 } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, type LimitedItem } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
ApiVersion,
|
||||
type LimitedItem,
|
||||
type TrackResponseV2,
|
||||
} from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
constructArrearItem,
|
||||
@@ -40,6 +44,7 @@ const monthlyProduct = constructProduct({
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing deduction order with monthly pay-per-use and lifetime one-off`)}`, () => {
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initProductsV0({
|
||||
@@ -93,23 +98,46 @@ describe(`${chalk.yellowBright(`${testCase}: Testing deduction order with monthl
|
||||
const currentUsage = 40;
|
||||
|
||||
test("should deduct from monthly first", async () => {
|
||||
await autumnV1.track({
|
||||
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: currentUsage,
|
||||
overage_behavior: "reject",
|
||||
});
|
||||
|
||||
// 1. Verify track response
|
||||
const totalBalance =
|
||||
monthlyMsges.included_usage + lifetimeMsges.included_usage;
|
||||
expect(trackRes.balance).toMatchObject({
|
||||
granted_balance: totalBalance,
|
||||
purchased_balance: 0,
|
||||
current_balance: totalBalance - currentUsage,
|
||||
usage: currentUsage,
|
||||
});
|
||||
const trackMonthlyBreakdown = trackRes.balance?.breakdown?.[0];
|
||||
expect(trackMonthlyBreakdown).toMatchObject({
|
||||
granted_balance: monthlyMsges.included_usage,
|
||||
purchased_balance: 0,
|
||||
current_balance: monthlyMsges.included_usage - currentUsage,
|
||||
usage: currentUsage,
|
||||
});
|
||||
const trackLifetimeBreakdown = trackRes.balance?.breakdown?.[1];
|
||||
expect(trackLifetimeBreakdown).toMatchObject({
|
||||
granted_balance: lifetimeMsges.included_usage,
|
||||
purchased_balance: 0,
|
||||
current_balance: lifetimeMsges.included_usage,
|
||||
usage: 0,
|
||||
});
|
||||
|
||||
// Check top-level balance and usage
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
const msgesFeature = customer.features[TestFeature.Messages];
|
||||
|
||||
// Check top-level balance and usage
|
||||
expect(msgesFeature.balance).toBe(
|
||||
monthlyMsges.included_usage + lifetimeMsges.included_usage - currentUsage,
|
||||
);
|
||||
expect(msgesFeature.usage).toBe(currentUsage);
|
||||
|
||||
// Check breakdown balances and usage
|
||||
const monthlyBreakdown = msgesFeature.breakdown?.find(
|
||||
(b: any) => b.interval === "month",
|
||||
);
|
||||
@@ -132,30 +160,43 @@ describe(`${chalk.yellowBright(`${testCase}: Testing deduction order with monthl
|
||||
|
||||
const usage2 = 50; // 10 from monthly, 40 from lifetime
|
||||
test("should deduct from monthly and lifetime in correct order", async () => {
|
||||
await autumnV1.track({
|
||||
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: usage2,
|
||||
overage_behavior: "reject",
|
||||
});
|
||||
|
||||
// 1. Verify track response
|
||||
const trackMonthlyBreakdown = trackRes.balance?.breakdown?.[0];
|
||||
expect(trackMonthlyBreakdown).toMatchObject({
|
||||
granted_balance: monthlyMsges.included_usage,
|
||||
purchased_balance: 0,
|
||||
current_balance: 0,
|
||||
usage: monthlyMsges.included_usage,
|
||||
});
|
||||
const trackLifetimeBreakdown = trackRes.balance?.breakdown?.[1];
|
||||
expect(trackLifetimeBreakdown).toMatchObject({
|
||||
granted_balance: lifetimeMsges.included_usage,
|
||||
purchased_balance: 0,
|
||||
current_balance: 10,
|
||||
usage: 40,
|
||||
});
|
||||
|
||||
// 2. Verify customer balances
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
const msgesFeature = customer.features[TestFeature.Messages];
|
||||
|
||||
// Check top-level balance and usage
|
||||
expect(msgesFeature.balance).toBe(10);
|
||||
expect(msgesFeature.usage).toBe(currentUsage + usage2);
|
||||
|
||||
// Check breakdown balances and usage
|
||||
const monthlyBreakdown = msgesFeature.breakdown?.find(
|
||||
(b: any) => b.interval === "month",
|
||||
);
|
||||
expect(monthlyBreakdown?.balance).toBe(0);
|
||||
expect(monthlyBreakdown?.usage).toBe(monthlyMsges.included_usage);
|
||||
|
||||
const lifetimeBreakdown = msgesFeature.breakdown?.find(
|
||||
(b: any) => b.interval === "lifetime",
|
||||
);
|
||||
|
||||
expect(monthlyBreakdown?.balance).toBe(0);
|
||||
expect(monthlyBreakdown?.usage).toBe(monthlyMsges.included_usage);
|
||||
expect(lifetimeBreakdown?.balance).toBe(10);
|
||||
expect(lifetimeBreakdown?.usage).toBe(40);
|
||||
|
||||
@@ -167,21 +208,37 @@ describe(`${chalk.yellowBright(`${testCase}: Testing deduction order with monthl
|
||||
|
||||
const usage3 = 50; // 10 from lifetime, 40 from monthly overage
|
||||
test("should deduct from lifetime and monthly in correct order", async () => {
|
||||
await autumnV1.track({
|
||||
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: usage3,
|
||||
overage_behavior: "reject",
|
||||
});
|
||||
|
||||
// 1. Verify track response
|
||||
// 1. Verify track response
|
||||
const trackMonthlyBreakdown = trackRes.balance?.breakdown?.[0];
|
||||
expect(trackMonthlyBreakdown).toMatchObject({
|
||||
granted_balance: monthlyMsges.included_usage,
|
||||
purchased_balance: 40,
|
||||
current_balance: 0,
|
||||
usage: monthlyMsges.included_usage + 40,
|
||||
});
|
||||
const trackLifetimeBreakdown = trackRes.balance?.breakdown?.[1];
|
||||
expect(trackLifetimeBreakdown).toMatchObject({
|
||||
granted_balance: lifetimeMsges.included_usage,
|
||||
purchased_balance: 0,
|
||||
current_balance: 0,
|
||||
usage: 40 + 10,
|
||||
});
|
||||
|
||||
// 2. Verify customer response
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
const msgesFeature = customer.features[TestFeature.Messages];
|
||||
|
||||
// Check top-level balance and usage
|
||||
expect(msgesFeature.balance).toBe(-40);
|
||||
expect(msgesFeature.usage).toBe(currentUsage + usage2 + usage3);
|
||||
|
||||
// Check breakdown balances and usage
|
||||
const monthlyBreakdown = msgesFeature.breakdown?.find(
|
||||
(b: any) => b.interval === "month",
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, ErrCode, type TrackResponseV2 } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
type ApiCustomer,
|
||||
ApiVersion,
|
||||
ErrCode,
|
||||
type TrackResponseV2,
|
||||
} from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
@@ -15,11 +20,11 @@ const testCase = "track-basic8";
|
||||
const customerId = testCase;
|
||||
|
||||
// Prepaid feature: 5 included, no overage allowed
|
||||
const prepaidQuantity = 500;
|
||||
|
||||
const prepaidItem = constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 0,
|
||||
billingUnits: 100,
|
||||
includedUsage: 2,
|
||||
billingUnits: 1,
|
||||
price: 1,
|
||||
});
|
||||
|
||||
@@ -29,6 +34,7 @@ const prepaidProduct = constructProduct({
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
const prepaidQuantity = 3;
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing prepaid tracking`)}`, () => {
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
|
||||
@@ -62,7 +68,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing prepaid tracking`)}`, () =>
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
const balance = customer.features[TestFeature.Messages].balance;
|
||||
|
||||
expect(balance).toBe(prepaidQuantity);
|
||||
expect(balance).toBe(5);
|
||||
});
|
||||
|
||||
test("should reject tracking 7 units when balance is 5 (no overage)", async () => {
|
||||
@@ -72,7 +78,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing prepaid tracking`)}`, () =>
|
||||
await autumnV1.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: prepaidQuantity + 1,
|
||||
value: 7,
|
||||
overage_behavior: "reject",
|
||||
});
|
||||
},
|
||||
@@ -82,7 +88,22 @@ describe(`${chalk.yellowBright(`${testCase}: Testing prepaid tracking`)}`, () =>
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
const balance = customer.features[TestFeature.Messages].balance;
|
||||
|
||||
expect(balance).toBe(prepaidQuantity);
|
||||
expect(balance).toBe(5);
|
||||
});
|
||||
|
||||
test("should track 4 units and have correct balance", async () => {
|
||||
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 4,
|
||||
});
|
||||
|
||||
expect(trackRes.balance).toMatchObject({
|
||||
granted_balance: 2,
|
||||
purchased_balance: 3,
|
||||
current_balance: 1,
|
||||
usage: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test("should reflect unchanged balance in non-cached customer after 2s", async () => {
|
||||
@@ -90,21 +111,16 @@ describe(`${chalk.yellowBright(`${testCase}: Testing prepaid tracking`)}`, () =>
|
||||
await timeout(2000);
|
||||
|
||||
// Fetch customer with skip_cache=true
|
||||
const customer = await autumnV1.customers.get(customerId, {
|
||||
const customer = (await autumnV2.customers.get(customerId, {
|
||||
skip_cache: "true",
|
||||
});
|
||||
const balance = customer.features[TestFeature.Messages].balance;
|
||||
})) as unknown as ApiCustomer;
|
||||
const feature = customer.balances[TestFeature.Messages];
|
||||
|
||||
expect(balance).toBe(prepaidQuantity);
|
||||
});
|
||||
|
||||
test("should track 3 units and have correct balance", async () => {
|
||||
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 3,
|
||||
});
|
||||
|
||||
console.log("Track res:", trackRes);
|
||||
expect(feature).toMatchObject({
|
||||
granted_balance: 2,
|
||||
purchased_balance: 3,
|
||||
current_balance: 1,
|
||||
usage: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { ApiVersion, type TrackResponseV2 } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { trackWasSuccessful } from "../trackTestUtils.js";
|
||||
|
||||
const testCase = "track-basic9";
|
||||
const customerId = testCase;
|
||||
@@ -31,7 +30,7 @@ const payPerUseProduct = constructProduct({
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing pay-per-use (overage allowed) with reject behavior`)}`, () => {
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
@@ -60,14 +59,20 @@ describe(`${chalk.yellowBright(`${testCase}: Testing pay-per-use (overage allowe
|
||||
});
|
||||
|
||||
test("should allow tracking 7 units when balance is 5 (overage allowed)", async () => {
|
||||
const res = await autumnV1.track({
|
||||
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 7,
|
||||
overage_behavior: "reject",
|
||||
});
|
||||
|
||||
expect(trackWasSuccessful({ res })).toBe(true);
|
||||
// Verify track response
|
||||
expect(trackRes.balance).toMatchObject({
|
||||
granted_balance: 5,
|
||||
purchased_balance: 2,
|
||||
current_balance: 0,
|
||||
usage: 7,
|
||||
});
|
||||
|
||||
// Verify balance went negative (overage)
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
@@ -80,14 +85,20 @@ describe(`${chalk.yellowBright(`${testCase}: Testing pay-per-use (overage allowe
|
||||
|
||||
// Track 3 more units, should be allowed
|
||||
test("should allow tracking 3 units when balance is -2 (overage allowed)", async () => {
|
||||
const res = await autumnV1.track({
|
||||
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 3,
|
||||
overage_behavior: "reject",
|
||||
});
|
||||
|
||||
expect(trackWasSuccessful({ res })).toBe(true);
|
||||
// Verify track response
|
||||
expect(trackRes.balance).toMatchObject({
|
||||
granted_balance: 5,
|
||||
purchased_balance: 5,
|
||||
current_balance: 0,
|
||||
usage: 10,
|
||||
});
|
||||
|
||||
// Verify balance went negative (overage)
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { trackWasSuccessful } from "../trackTestUtils.js";
|
||||
|
||||
const testCase = "concurrentTrack1";
|
||||
const customerId = testCase;
|
||||
@@ -85,10 +84,10 @@ describe(`${chalk.yellowBright(`concurrentTrack1: Testing track with concurrent
|
||||
}),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
// With cap behavior, all requests pass (cap at 0 instead of rejecting)
|
||||
const allFulfilled = results.every((r) => trackWasSuccessful({ res: r }));
|
||||
const allFulfilled = results.every((r) => r.status === "fulfilled");
|
||||
expect(allFulfilled).toBe(true);
|
||||
|
||||
// Check final balance
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, ProductItemFeatureType } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
@@ -85,16 +85,6 @@ describe(`${chalk.yellowBright(`concurrentTrack2: Testing concurrent track, allo
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// console.log(results);
|
||||
// return;
|
||||
|
||||
// const successCount = results.filter((r) => r.status === "fulfilled").length;
|
||||
// const rejectedCount = results.filter((r) => r.status === "rejected").length;
|
||||
|
||||
// // Only 1 should succeed, 4 should be rejected due to insufficient balance
|
||||
// expect(successCount).toBe(1);
|
||||
// expect(rejectedCount).toBe(4);
|
||||
|
||||
// Check final balance
|
||||
const customer = await autumnV1.customers.get(customerId);
|
||||
const finalBalance = customer.features[TestFeature.Users].balance;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, ProductItemFeatureType } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
constructArrearItem,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, type LimitedItem } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
|
||||
import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js";
|
||||
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
|
||||
@@ -141,25 +141,25 @@ describe(chalk.yellowBright("Plan V2 - Basic CREATE Tests"), () => {
|
||||
} catch (_error) {}
|
||||
|
||||
const created = (await autumnV2.products.create({
|
||||
id: "metered_monthly",
|
||||
id: productId,
|
||||
name: "Metered Monthly",
|
||||
features: [
|
||||
{
|
||||
feature_id: features.metered1.id,
|
||||
granted: 1000,
|
||||
reset_interval: ResetInterval.Month,
|
||||
granted_balance: 1000,
|
||||
reset: {
|
||||
interval: ResetInterval.Month,
|
||||
},
|
||||
},
|
||||
],
|
||||
} as CreatePlanParams)) as ApiPlan;
|
||||
|
||||
// V2 response validation
|
||||
expect(created.features).to.have.lengthOf(1);
|
||||
expect(created.features[0].granted).to.equal(1000);
|
||||
expect(created.features[0].granted_balance).to.equal(1000);
|
||||
|
||||
// V1.2 validation (items format)
|
||||
const v1_2 = (await autumnV1_2.products.get(
|
||||
"metered_monthly",
|
||||
)) as ApiProduct;
|
||||
const v1_2 = (await autumnV1_2.products.get(productId)) as ApiProduct;
|
||||
expect(v1_2.items[0].included_usage).to.equal(1000);
|
||||
expect(v1_2.items[0].interval).to.equal("month");
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ import { CheckResponseV1Schema } from "../prevVersions/CheckResponseV1.js";
|
||||
export const V1_2_CheckChange = defineVersionChange({
|
||||
name: "V1_2 Check Change",
|
||||
newVersion: ApiVersion.V2_0,
|
||||
oldVersion: ApiVersion.V1_2,
|
||||
oldVersion: ApiVersion.V1_Beta,
|
||||
description: ["Check response transformed to V2.0 format"],
|
||||
affectedResources: [AffectedResource.Check],
|
||||
newSchema: CheckResponseV2Schema,
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
defineVersionChange,
|
||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||
import type { z } from "zod/v4";
|
||||
import { CusExpand } from "../../../../models/cusModels/cusExpand.js";
|
||||
import { CheckQuerySchema } from "../checkParams.js";
|
||||
import { CheckExpand } from "../enums/CheckExpand.js";
|
||||
|
||||
/**
|
||||
* V1_2_CheckQueryChange: Transforms check query TO latest format
|
||||
@@ -49,7 +49,11 @@ export const V1_2_CheckQueryChange = defineVersionChange({
|
||||
const existingExpand = input.expand || [];
|
||||
|
||||
// Add `balance.feature` to expand array
|
||||
const newExpand = [...existingExpand, CheckExpand.BalanceFeature];
|
||||
// Type assertion needed because schema expects CusExpand.BalanceFeature[] specifically
|
||||
const newExpand: CusExpand.BalanceFeature[] = [
|
||||
...(existingExpand as CusExpand.BalanceFeature[]),
|
||||
CusExpand.BalanceFeature,
|
||||
];
|
||||
|
||||
return {
|
||||
...input,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { z } from "zod/v4";
|
||||
import { CusExpand } from "../../../models/cusModels/cusExpand.js";
|
||||
import { CustomerDataSchema } from "../../common/customerData.js";
|
||||
import { EntityDataSchema } from "../../common/entityData.js";
|
||||
import { queryStringArray } from "../../common/queryHelpers.js";
|
||||
import { CheckExpand } from "./enums/CheckExpand.js";
|
||||
|
||||
export const CheckQuerySchema = z.object({
|
||||
skip_cache: z.boolean().optional(),
|
||||
expand: queryStringArray(z.enum(CheckExpand)).optional(),
|
||||
expand: queryStringArray(z.enum([CusExpand.BalanceFeature])).optional(),
|
||||
});
|
||||
|
||||
// Check Feature Schemas
|
||||
|
||||
@@ -63,6 +63,7 @@ export const V1_2_CustomerChange = defineVersionChange({
|
||||
for (const [featureId, feature] of Object.entries(input.balances)) {
|
||||
v3_features[featureId] = transformBalanceToCusFeatureV3({
|
||||
input: feature,
|
||||
legacyData: legacyData?.cusFeatureLegacyData[featureId],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,9 @@ export const ApiBalanceBreakdownSchema = z.object({
|
||||
current_balance: z.number(),
|
||||
usage: z.number(),
|
||||
|
||||
max_purchase: z.number().optional(),
|
||||
overage_allowed: z.boolean().optional(),
|
||||
|
||||
reset: ApiBalanceResetSchema.optional(),
|
||||
overage_allowed: z.boolean(),
|
||||
max_purchase: z.number().nullable(),
|
||||
reset: ApiBalanceResetSchema.nullable(),
|
||||
});
|
||||
|
||||
export const ApiBalanceSchema = z.object({
|
||||
@@ -35,10 +34,10 @@ export const ApiBalanceSchema = z.object({
|
||||
current_balance: z.number(),
|
||||
usage: z.number(),
|
||||
|
||||
max_purchase: z.number(),
|
||||
overage_allowed: z.boolean(),
|
||||
max_purchase: z.number().nullable(),
|
||||
reset: ApiBalanceResetSchema.nullable(),
|
||||
|
||||
reset: ApiBalanceResetSchema.optional(),
|
||||
breakdown: z.array(ApiBalanceBreakdownSchema).nullish(),
|
||||
rollovers: z.array(ApiBalanceRolloverSchema).nullish(),
|
||||
});
|
||||
|
||||
@@ -124,7 +124,6 @@ const toV3BalanceParams = ({
|
||||
}
|
||||
|
||||
// 5. Usage limit
|
||||
|
||||
const usageLimit = input.max_purchase
|
||||
? new Decimal(input.max_purchase).add(includedUsage).toNumber()
|
||||
: undefined;
|
||||
@@ -150,6 +149,7 @@ export function transformBalanceToCusFeatureV3({
|
||||
unlimited: isUnlimited,
|
||||
});
|
||||
|
||||
console.log("Legacy data:", legacyData);
|
||||
const { includedUsage, balance, usage, overageAllowed, usageLimit } =
|
||||
toV3BalanceParams({
|
||||
input,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Decimal } from "decimal.js";
|
||||
import type { FullCusEntWithFullCusProduct } from "../../../models/cusProductModels/cusEntModels/cusEntWithProduct.js";
|
||||
import { cusEntToCusPrice } from "../../productUtils/convertUtils.js";
|
||||
import { isPrepaidPrice } from "../../productUtils/priceUtils.js";
|
||||
|
||||
export const cusEntToPrepaidQuantity = ({
|
||||
cusEnt,
|
||||
}: {
|
||||
cusEnt: FullCusEntWithFullCusProduct;
|
||||
}) => {
|
||||
// 2. If cus ent is not prepaid, skip
|
||||
const cusPrice = cusEntToCusPrice({ cusEnt });
|
||||
|
||||
if (!cusPrice || !isPrepaidPrice({ price: cusPrice.price })) return 0;
|
||||
|
||||
// 3. Get quantity
|
||||
const options = cusEnt.customer_product.options.find(
|
||||
(option) =>
|
||||
option.internal_feature_id === cusEnt.entitlement.internal_feature_id,
|
||||
);
|
||||
|
||||
if (!options) return 0;
|
||||
|
||||
const quantityWithUnits = new Decimal(options.quantity)
|
||||
.mul(cusPrice.price.config.billing_units ?? 1)
|
||||
.toNumber();
|
||||
|
||||
return quantityWithUnits;
|
||||
};
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type FullCustomerEntitlement,
|
||||
getCusEntBalance,
|
||||
getStartingBalance,
|
||||
notNullish,
|
||||
} from "../../index.js";
|
||||
import { cusEntToCusPrice } from "../productUtils/convertUtils.js";
|
||||
import { getRolloverFields } from "./getRolloverFields.js";
|
||||
@@ -13,10 +12,16 @@ import { getRolloverFields } from "./getRolloverFields.js";
|
||||
export const cusEntToKey = ({
|
||||
cusEnt,
|
||||
}: {
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
cusEnt: FullCusEntWithFullCusProduct;
|
||||
}) => {
|
||||
const ent = cusEnt.entitlement;
|
||||
return `${ent.interval || "null"}-${ent.interval_count || 1}-${ent.feature.id}`;
|
||||
// Interval
|
||||
const interval = `${cusEnt.entitlement.interval_count ?? 1}:${cusEnt.entitlement.interval}`;
|
||||
|
||||
const planId = `${cusEnt.customer_product.product_id}`;
|
||||
|
||||
const usageModel = `${cusEnt.usage_allowed}`;
|
||||
|
||||
return `${interval}:${planId}:${usageModel}`;
|
||||
};
|
||||
|
||||
export const cusEntToBalance = ({
|
||||
@@ -94,29 +99,6 @@ export const cusEntToIncludedUsage = ({
|
||||
// }
|
||||
};
|
||||
|
||||
export const cusEntToMaxPurchase = ({
|
||||
cusEnt,
|
||||
entityId,
|
||||
}: {
|
||||
cusEnt: FullCusEntWithFullCusProduct;
|
||||
entityId?: string;
|
||||
}) => {
|
||||
const startingBalance = cusEntToIncludedUsage({
|
||||
cusEnt,
|
||||
entityId,
|
||||
});
|
||||
|
||||
const usageLimit = cusEnt.entitlement.usage_limit;
|
||||
// if (cusEnt.entitlement.usage_limit) return cusEnt.entitlement.usage_limit;
|
||||
// return startingBalance;
|
||||
|
||||
if (notNullish(usageLimit) && notNullish(startingBalance)) {
|
||||
return new Decimal(usageLimit).sub(startingBalance).toNumber();
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
// NEW CUS ENT UTILS
|
||||
export const cusEntToGrantedBalance = ({
|
||||
cusEnt,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Decimal } from "decimal.js";
|
||||
import {
|
||||
cusEntToIncludedUsage,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
isPrepaidCusEnt,
|
||||
notNullish,
|
||||
nullish,
|
||||
} from "../../../index.js";
|
||||
|
||||
export const cusEntsToMaxPurchase = ({
|
||||
cusEnts,
|
||||
entityId,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
entityId?: string;
|
||||
}): number | null => {
|
||||
// 1. If there's usage-based cus ent, return undefined
|
||||
if (
|
||||
cusEnts.some(
|
||||
(cusEnt) =>
|
||||
cusEnt.usage_allowed && nullish(cusEnt.entitlement.usage_limit),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasPrepaidNoLimit = cusEnts.some(
|
||||
(ce) =>
|
||||
isPrepaidCusEnt({ cusEnt: ce }) && nullish(ce.entitlement.usage_limit),
|
||||
);
|
||||
const hasPrepaid = cusEnts.some((ce) => isPrepaidCusEnt({ cusEnt: ce }));
|
||||
const hasUsageBased = cusEnts.some((ce) => ce.usage_allowed);
|
||||
|
||||
// 2. If there's usage-based cus ent, and prepaid no limit
|
||||
if (hasUsageBased && hasPrepaidNoLimit) return null;
|
||||
|
||||
// 3. If there's prepaid
|
||||
if (hasPrepaidNoLimit) return null;
|
||||
|
||||
// 3. If there's no prepaid and no usage-based, return undefined (free feature)
|
||||
if (!hasPrepaid && !hasUsageBased) return null;
|
||||
|
||||
let maxPurchase = new Decimal(0);
|
||||
for (const cusEnt of cusEnts) {
|
||||
const startingBalance = cusEntToIncludedUsage({
|
||||
cusEnt,
|
||||
entityId,
|
||||
});
|
||||
const usageLimit = cusEnt.entitlement.usage_limit;
|
||||
|
||||
if (notNullish(usageLimit) && notNullish(startingBalance)) {
|
||||
maxPurchase = maxPurchase.add(
|
||||
new Decimal(usageLimit).sub(startingBalance),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return maxPurchase.toNumber();
|
||||
};
|
||||
@@ -2,10 +2,10 @@ import type {
|
||||
EntityBalance,
|
||||
FullCustomerEntitlement,
|
||||
} from "@models/cusProductModels/cusEntModels/cusEntModels.js";
|
||||
import type { Entity } from "../../models/cusModels/entityModels/entityModels.js";
|
||||
import type { FullCustomer } from "../../models/cusModels/fullCusModel.js";
|
||||
import type { FullCusEntWithFullCusProduct } from "../../models/cusProductModels/cusEntModels/cusEntWithProduct.js";
|
||||
import { notNullish } from "../utils.js";
|
||||
import { cusEntToCusPrice } from "../productUtils/convertUtils.js";
|
||||
import { isPrepaidPrice } from "../productUtils/priceUtils.js";
|
||||
|
||||
export const formatCusEnt = ({
|
||||
cusEnt,
|
||||
@@ -57,31 +57,52 @@ export const updateCusEntInFullCus = ({
|
||||
}
|
||||
}
|
||||
};
|
||||
export const cusEntMatchesEntity = ({
|
||||
|
||||
// export const cusEntMatchesEntity = ({
|
||||
// cusEnt,
|
||||
// entity,
|
||||
// }: {
|
||||
// cusEnt: FullCusEntWithFullCusProduct;
|
||||
// entity?: Entity;
|
||||
// }) => {
|
||||
// if (!entity) return true;
|
||||
|
||||
// let cusProductMatch = true;
|
||||
|
||||
// if (notNullish(cusEnt.customer_product?.internal_entity_id)) {
|
||||
// cusProductMatch =
|
||||
// cusEnt.customer_product.internal_entity_id === entity.internal_id;
|
||||
// }
|
||||
|
||||
// let entityFeatureIdMatch = true;
|
||||
// // let feature = features?.find(
|
||||
// // (f) => f.id == cusEnt.entitlement.entity_feature_id,
|
||||
// // );
|
||||
|
||||
// if (notNullish(cusEnt.entitlement.entity_feature_id)) {
|
||||
// entityFeatureIdMatch =
|
||||
// cusEnt.entitlement.entity_feature_id === entity.feature_id;
|
||||
// }
|
||||
|
||||
// return cusProductMatch && entityFeatureIdMatch;
|
||||
// };
|
||||
|
||||
export const isPrepaidCusEnt = ({
|
||||
cusEnt,
|
||||
entity,
|
||||
}: {
|
||||
cusEnt: FullCusEntWithFullCusProduct;
|
||||
entity?: Entity;
|
||||
}) => {
|
||||
if (!entity) return true;
|
||||
// 2. If cus ent is not prepaid, skip
|
||||
const cusPrice = cusEntToCusPrice({ cusEnt });
|
||||
if (!cusPrice || !isPrepaidPrice({ price: cusPrice.price })) return false;
|
||||
|
||||
let cusProductMatch = true;
|
||||
// 3. Get quantity
|
||||
const options = cusEnt.customer_product.options.find(
|
||||
(option) =>
|
||||
option.internal_feature_id === cusEnt.entitlement.internal_feature_id,
|
||||
);
|
||||
|
||||
if (notNullish(cusEnt.customer_product?.internal_entity_id)) {
|
||||
cusProductMatch =
|
||||
cusEnt.customer_product.internal_entity_id === entity.internal_id;
|
||||
}
|
||||
if (!options) return false;
|
||||
|
||||
let entityFeatureIdMatch = true;
|
||||
// let feature = features?.find(
|
||||
// (f) => f.id == cusEnt.entitlement.entity_feature_id,
|
||||
// );
|
||||
|
||||
if (notNullish(cusEnt.entitlement.entity_feature_id)) {
|
||||
entityFeatureIdMatch =
|
||||
cusEnt.entitlement.entity_feature_id === entity.feature_id;
|
||||
}
|
||||
|
||||
return cusProductMatch && entityFeatureIdMatch;
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Entity } from "../../models/cusModels/entityModels/entityModels.js";
|
||||
import type { FullCustomerEntitlement } from "../../models/cusProductModels/cusEntModels/cusEntModels.js";
|
||||
import type { FullCusEntWithFullCusProduct } from "../../models/cusProductModels/cusEntModels/cusEntWithProduct.js";
|
||||
import type { Feature } from "../../models/featureModels/featureModels.js";
|
||||
import { notNullish, nullish } from "../utils.js";
|
||||
@@ -62,3 +63,13 @@ export const filterEntityProductCusEnts = ({
|
||||
notNullish(ce.customer_product?.internal_entity_id),
|
||||
);
|
||||
};
|
||||
|
||||
export const cusEntMatchesFeature = ({
|
||||
cusEnt,
|
||||
feature,
|
||||
}: {
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
feature: Feature;
|
||||
}) => {
|
||||
return cusEnt.entitlement.feature.internal_id === feature.internal_id;
|
||||
};
|
||||
|
||||
@@ -119,6 +119,15 @@ export const sortCusEntsForDeduction = ({
|
||||
}
|
||||
}
|
||||
|
||||
// If one has a usage_allowed, it should go last
|
||||
if (a.usage_allowed && !b.usage_allowed) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!a.usage_allowed && b.usage_allowed) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 0a. If both are entity products (attached to entities), sort by entity_id for consistent ordering
|
||||
const aIsProductEntity = !!a.customer_product?.internal_entity_id;
|
||||
const bIsProductEntity = !!b.customer_product?.internal_entity_id;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Cus ent utils
|
||||
|
||||
export * from "./cusEntUtils/balanceUtils/cusEntToPrepaidQuantity.js";
|
||||
export * from "./cusEntUtils/balanceUtils/cusEntToPurchasedBalance.js";
|
||||
export * from "./cusEntUtils/balanceUtils.js";
|
||||
export * from "./cusEntUtils/convertCusEntUtils/cusEntsToMaxPurchase.js";
|
||||
export * from "./cusEntUtils/convertCusEntUtils.js";
|
||||
export * from "./cusEntUtils/cusEntUtils.js";
|
||||
|
||||
export * from "./cusEntUtils/filterCusEntUtils.js";
|
||||
// Cus ent utils
|
||||
export * from "./cusEntUtils/getRolloverFields.js";
|
||||
export * from "./cusEntUtils/getStartingBalance.js";
|
||||
|
||||
@@ -49,7 +49,6 @@ export function SelectFeatureSheet({
|
||||
const timer = setTimeout(() => setSelectOpen(true), 250);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleFeatureSelect = (featureId: string) => {
|
||||
|
||||
@@ -31,8 +31,6 @@ export const PlanFeatureList = ({
|
||||
|
||||
const filteredItems = productV2ToFeatureItems({ items: product.items });
|
||||
|
||||
console.log("Filtered items:", filteredItems);
|
||||
|
||||
// Group items by entity_feature_id
|
||||
const groupedItems = filteredItems.reduce(
|
||||
(acc, item) => {
|
||||
|
||||
@@ -39,6 +39,8 @@ export default defineConfig({
|
||||
"better-auth/react",
|
||||
"@better-auth/stripe",
|
||||
"zod/v4",
|
||||
"drizzle-orm/pg-core",
|
||||
"drizzle-orm",
|
||||
],
|
||||
},
|
||||
// Clear cache on config change
|
||||
|
||||
Reference in New Issue
Block a user