fix: edge case for update custom ent, free cont use -> paid cont use, allowance increase
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"setup:test": "bun scripts/setup-test.ts",
|
||||
"tests": "bun scripts/test.ts",
|
||||
"setupci": "node scripts/setupci.js",
|
||||
"replicate": "bun scripts/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",
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"setup": "tsx setup.js",
|
||||
"setup-test": "tsx setup-test.ts"
|
||||
"setup-test": "tsx setup-test.ts",
|
||||
"replicate": "bun run replicate.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@autumn/shared": "workspace:*",
|
||||
|
||||
284
scripts/replicate.ts
Normal file
284
scripts/replicate.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { exec, spawn } from "node:child_process";
|
||||
import readline from "node:readline";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Execute command and show live output
|
||||
*/
|
||||
function execWithOutput(command: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, {
|
||||
shell: true,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for confirmation
|
||||
*/
|
||||
function promptUser(question: string): Promise<boolean> {
|
||||
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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate PostgreSQL URL format
|
||||
*/
|
||||
function validatePostgresUrl(url: string): boolean {
|
||||
return url.startsWith("postgresql://") || url.startsWith("postgres://");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract database name from PostgreSQL URL
|
||||
*/
|
||||
function extractDbName(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.pathname.slice(1) || "database";
|
||||
} catch {
|
||||
return "database";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up PostgreSQL URL for pg_dump/psql
|
||||
* - Removes query parameters after /postgres
|
||||
* - Changes port 6432 to 5432
|
||||
*/
|
||||
function cleanUrl(url: string): string {
|
||||
// Remove everything after /postgres (including query params)
|
||||
let cleaned = url.replace(/\/postgres\?.*$/, "/postgres");
|
||||
|
||||
// Replace port 6432 with 5432
|
||||
cleaned = cleaned.replace(/:6432\//, ":5432/");
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer table row count
|
||||
*/
|
||||
async function getCustomerCount(
|
||||
url: string,
|
||||
): Promise<{ count: number | null; error?: string }> {
|
||||
try {
|
||||
const cleanedUrl = cleanUrl(url);
|
||||
const { stdout } = await execAsync(
|
||||
`psql "${cleanedUrl}" -t -c "SELECT COUNT(*) FROM customers;"`,
|
||||
);
|
||||
const count = parseInt(stdout.trim());
|
||||
if (!Number.isNaN(count)) {
|
||||
return { count };
|
||||
}
|
||||
return { count: null, error: "Could not parse customer count" };
|
||||
} catch (error) {
|
||||
// Table might not exist or connection failed
|
||||
return {
|
||||
count: null,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replicate database from source to destination
|
||||
*/
|
||||
async function replicateDatabase({
|
||||
fromUrl,
|
||||
toUrl,
|
||||
}: {
|
||||
fromUrl: string;
|
||||
toUrl: string;
|
||||
}) {
|
||||
console.log("\n🔄 PostgreSQL Database Replication\n");
|
||||
|
||||
// Validate URLs
|
||||
if (!validatePostgresUrl(fromUrl)) {
|
||||
console.error("❌ Invalid source database URL");
|
||||
console.error(
|
||||
" Expected format: postgresql://user:pass@host:port/dbname",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!validatePostgresUrl(toUrl)) {
|
||||
console.error("❌ Invalid destination database URL");
|
||||
console.error(
|
||||
" Expected format: postgresql://user:pass@host:port/dbname",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fromDbName = extractDbName(fromUrl);
|
||||
const toDbName = extractDbName(toUrl);
|
||||
|
||||
console.log(`📤 Source: ${fromDbName}`);
|
||||
console.log(`📥 Destination: ${toDbName}`);
|
||||
|
||||
// Check destination database for production data
|
||||
console.log("\n🔍 Checking destination database...");
|
||||
const customerResult = await getCustomerCount(toUrl);
|
||||
|
||||
if (customerResult.count === null) {
|
||||
// Table doesn't exist or can't query - likely a new/empty database
|
||||
console.log("✅ Destination appears to be empty (no customers table)");
|
||||
} else {
|
||||
console.log(`📊 Destination has ${customerResult.count} customers`);
|
||||
|
||||
// Protection: Don't allow overwriting databases with > 1000 customers
|
||||
if (customerResult.count > 1000) {
|
||||
console.error(
|
||||
"\n❌ PROTECTION: Destination database has too many customers!",
|
||||
);
|
||||
console.error(
|
||||
` Customer count (${customerResult.count}) exceeds 1000 limit.`,
|
||||
);
|
||||
console.error(
|
||||
" This safety check prevents accidental overwrites of production databases.\n",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for confirmation
|
||||
const confirmed = await promptUser(
|
||||
"\n⚠️ This will OVERWRITE the destination database. Continue? (y/n): ",
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
console.log("\n❌ Replication cancelled\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const tempFile = `/tmp/db_dump_${Date.now()}.sql`;
|
||||
|
||||
// Clean URLs
|
||||
const cleanedFromUrl = cleanUrl(fromUrl);
|
||||
const cleanedToUrl = cleanUrl(toUrl);
|
||||
|
||||
console.log(`\n📤 Using source URL: ${cleanedFromUrl}`);
|
||||
console.log(`📥 Using destination URL: ${cleanedToUrl}\n`);
|
||||
|
||||
try {
|
||||
// Step 1: Dump the source database
|
||||
console.log("📦 Dumping source database...");
|
||||
await execWithOutput(
|
||||
`pg_dump "${cleanedFromUrl}" --no-owner --no-privileges -f "${tempFile}" 2>&1`,
|
||||
);
|
||||
console.log("✅ Source database dumped successfully");
|
||||
|
||||
// Step 2: Restore to destination database
|
||||
console.log("\n📥 Restoring to destination database...\n");
|
||||
await execWithOutput(
|
||||
`psql "${cleanedToUrl}" -f "${tempFile}" --set ON_ERROR_STOP=off 2>&1 | grep -v "invalid command"`,
|
||||
);
|
||||
console.log("\n✅ Database restored successfully");
|
||||
|
||||
// Step 3: Clean up temp file
|
||||
console.log("\n🧹 Cleaning up temporary files...");
|
||||
await execAsync(`rm "${tempFile}"`);
|
||||
console.log("✅ Cleanup complete");
|
||||
|
||||
console.log("\n✨ Database replication completed successfully!\n");
|
||||
} catch (error) {
|
||||
console.error("\n❌ Replication failed:");
|
||||
if (error instanceof Error) {
|
||||
console.error(` ${error.message}\n`);
|
||||
}
|
||||
|
||||
// Try to clean up temp file
|
||||
try {
|
||||
await execAsync(`rm "${tempFile}"`);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments and reconstruct URLs
|
||||
* Handles cases where URLs aren't quoted and get split by shell
|
||||
*/
|
||||
function parseUrls(): { fromUrl: string; toUrl: string } | null {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Join all arguments and try to extract two PostgreSQL URLs
|
||||
const fullString = args.join(" ");
|
||||
|
||||
// Match two postgresql:// URLs (greedy match for first, non-greedy for separation)
|
||||
const urlPattern =
|
||||
/(postgres(?:ql)?:\/\/[^\s]+?)[\s]+(postgres(?:ql)?:\/\/[^\s]+)/;
|
||||
const match = fullString.match(urlPattern);
|
||||
|
||||
if (match?.[1] && match[2]) {
|
||||
return {
|
||||
fromUrl: match[1],
|
||||
toUrl: match[2],
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: if we have exactly 2 args that look like URLs
|
||||
if (args.length === 2) {
|
||||
const [fromUrl, toUrl] = args;
|
||||
if (
|
||||
validatePostgresUrl(fromUrl || "") &&
|
||||
validatePostgresUrl(toUrl || "")
|
||||
) {
|
||||
return { fromUrl, toUrl };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
const urls = parseUrls();
|
||||
|
||||
if (!urls) {
|
||||
console.log("\n🔄 PostgreSQL Database Replication\n");
|
||||
console.log("Usage:");
|
||||
console.log(' bun replicate "<from-url>" "<to-url>"\n');
|
||||
console.log("Example:");
|
||||
console.log(
|
||||
' bun replicate "postgresql://user:pass@eu-host:5432/db" "postgresql://user:pass@us-host:5432/db"\n',
|
||||
);
|
||||
console.log("⚠️ Important: URLs must be quoted to prevent shell expansion\n");
|
||||
console.log("Tip: Store URLs in .env and use environment variables:\n");
|
||||
console.log(" bun replicate $EU_DATABASE_URL $US_DATABASE_URL\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { fromUrl, toUrl } = urls;
|
||||
|
||||
replicateDatabase({ fromUrl, toUrl }).catch((error) => {
|
||||
console.error("Unexpected error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,7 +1,13 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { exec, spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { detectAndSetPorts } from "./detect-ports.js";
|
||||
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
|
||||
@@ -24,6 +30,101 @@ function getEnvVariable(filePath, key) {
|
||||
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)
|
||||
@@ -31,20 +132,16 @@ async function startDev() {
|
||||
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 && backendUrl.includes("api.useautumn.com");
|
||||
|
||||
let vitePort = 3000;
|
||||
let serverPort = 8080;
|
||||
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 detection...\n");
|
||||
console.log("⏭️ Skipping port cleanup...\n");
|
||||
} else {
|
||||
// Detect and set ports
|
||||
const ports = await detectAndSetPorts();
|
||||
vitePort = ports.vitePort;
|
||||
serverPort = ports.serverPort;
|
||||
// Check and kill processes on ports 3000 and 8080 if needed
|
||||
await handlePorts();
|
||||
}
|
||||
|
||||
// Step 1: Build shared package first (initial build)
|
||||
@@ -78,9 +175,9 @@ async function startDev() {
|
||||
"server,workers,vite,shared",
|
||||
"-c",
|
||||
"green,yellow,blue,cyan",
|
||||
`"cd server && SERVER_PORT=${serverPort} bun dev"`,
|
||||
`"cd server && SERVER_PORT=${SERVER_PORT} bun dev"`,
|
||||
`"cd server && bun workers:dev"`,
|
||||
`"cd vite && VITE_PORT=${vitePort} bun dev"`,
|
||||
`"cd vite && VITE_PORT=${VITE_PORT} bun dev"`,
|
||||
`"cd shared && bun run dev:watch"`,
|
||||
],
|
||||
{
|
||||
@@ -88,8 +185,8 @@ async function startDev() {
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_PORT: vitePort.toString(),
|
||||
SERVER_PORT: serverPort.toString(),
|
||||
VITE_PORT: VITE_PORT.toString(),
|
||||
SERVER_PORT: SERVER_PORT.toString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -15,15 +15,15 @@ fi
|
||||
# Run tests using TypeScript runner with compact mode
|
||||
# Adjust --max to control concurrency (default: 6)
|
||||
BUN_PARALLEL_COMPACT \
|
||||
'server/tests/check/basic' \
|
||||
'server/tests/attach/basic' \
|
||||
'server/tests/attach/upgrade' \
|
||||
'server/tests/attach/downgrade' \
|
||||
'server/tests/attach/free' \
|
||||
'server/tests/attach/addOn' \
|
||||
'server/tests/attach/entities' \
|
||||
'server/tests/attach/checkout' \
|
||||
--max=6 \
|
||||
# 'server/tests/check/basic' \
|
||||
# 'server/tests/attach/basic' \
|
||||
# 'server/tests/attach/upgrade' \
|
||||
# 'server/tests/attach/downgrade' \
|
||||
# 'server/tests/attach/free' \
|
||||
# 'server/tests/attach/addOn' \
|
||||
# 'server/tests/attach/entities' \
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -36,16 +36,16 @@ import { checkCusSubCorrect } from "./utils/checkUtils/checkCustomerCorrect.js";
|
||||
|
||||
const { db } = initDrizzle({ maxConnections: 5 });
|
||||
|
||||
const orgSlugs = process.env.ORG_SLUGS!.split(",");
|
||||
let orgSlugs = process.env.ORG_SLUGS!.split(",");
|
||||
const skipEmails = process.env.SKIP_EMAILS!.split(",");
|
||||
const skipIds = [
|
||||
"cus_2tXCCwC6iyiftgA6ndSo1Ubb2dx",
|
||||
"DxG668K7uDd0Vahk54YWjvCGVgf2",
|
||||
];
|
||||
|
||||
// orgSlugs = ["lumenary"];
|
||||
const customerId = null;
|
||||
// customerId = "EBbxiRv9QJKXFy5WiAKeHi";
|
||||
orgSlugs = ["welcome-back-1747670264"];
|
||||
let customerId = null;
|
||||
customerId = "56";
|
||||
|
||||
const getSingleCustomer = async ({
|
||||
stripeCli,
|
||||
|
||||
@@ -110,14 +110,6 @@ export const updateStripeSub2 = async ({
|
||||
logger,
|
||||
});
|
||||
|
||||
// // // 3. Create prorations for continuous use items
|
||||
// let { replaceables, newItems } = await getContUseInvoiceItems({
|
||||
// attachParams,
|
||||
// cusProduct: curMainProduct!,
|
||||
// sub: curSub,
|
||||
// logger,
|
||||
// });
|
||||
|
||||
const { replaceables } = await createAndFilterContUseItems({
|
||||
attachParams,
|
||||
curMainProduct: curMainProduct!,
|
||||
@@ -151,24 +143,3 @@ export const updateStripeSub2 = async ({
|
||||
replaceables,
|
||||
};
|
||||
};
|
||||
|
||||
// await SubService.addUsageFeatures({
|
||||
// db,
|
||||
// stripeId: curSub.id,
|
||||
// usageFeatures: itemSet.usageFeatures,
|
||||
// orgId: org.id,
|
||||
// env: customer.env,
|
||||
// });
|
||||
|
||||
// if (invoiceOnly && attachParams.finalizeInvoice) {
|
||||
// logger.info(`FINALIZING INVOICE ${latestInvoice?.id}`);
|
||||
// try {
|
||||
// latestInvoice = await stripeCli.invoices.finalizeInvoice(
|
||||
// latestInvoice?.id as string
|
||||
// );
|
||||
// } catch (error) {
|
||||
// logger.error(`Failed to finalize invoice ${latestInvoice?.id}`, {
|
||||
// error,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -137,7 +137,7 @@ export const createAndFilterContUseItems = async ({
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Adding invoice item: ${item.description}, ${item.description}, interval: ${interval}`,
|
||||
`Adding invoice item: ${item.description}, amount: ${item.amount}, interval: ${interval}`,
|
||||
);
|
||||
|
||||
const { start, end } = subToPeriodStartEnd({ sub });
|
||||
|
||||
@@ -70,7 +70,19 @@ export const getContUseNewItems = async ({
|
||||
feature_id: ent.feature_id,
|
||||
} as PreviewLineItem;
|
||||
} else {
|
||||
let overage = new Decimal(usage).sub(ent.allowance!).toNumber();
|
||||
/*
|
||||
For example, free plan comes with 3 users, and usage is 3
|
||||
- Pro plan is 0 free, $10 per user
|
||||
- We need to calculate usage for the pro plan, and then charge for that
|
||||
---
|
||||
- When allowance increases though, how do we handle it? Don't allow negative overage.
|
||||
*/
|
||||
|
||||
// let overage = new Decimal(usage).sub(ent.allowance!).toNumber();
|
||||
let overage = Math.max(
|
||||
new Decimal(usage).sub(ent.allowance!).toNumber(),
|
||||
0,
|
||||
);
|
||||
|
||||
if (
|
||||
intervalsSame &&
|
||||
@@ -153,6 +165,8 @@ export const getContUseInvoiceItems = async ({
|
||||
(item) => item.price_id === prevCusPrice?.price.id,
|
||||
);
|
||||
|
||||
// TO DELETE
|
||||
|
||||
if (!prevCusEnt || !sub || !curItem) {
|
||||
const newItem = await getContUseNewItems({
|
||||
price,
|
||||
|
||||
@@ -84,7 +84,7 @@ export const auth = betterAuth({
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
redirectURI: `${process.env.SERVER_URL}/api/auth/callback/google`,
|
||||
redirectURI: `${process.env.BETTER_AUTH_URL}/api/auth/callback/google`,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
@@ -473,8 +473,6 @@ export const checkCusSubCorrect = async ({
|
||||
internalEntityId: cp.internal_entity_id,
|
||||
});
|
||||
|
||||
console.log("Cur scheduled product:", curScheduledProduct?.id);
|
||||
|
||||
if (curScheduledProduct) {
|
||||
const scheduledProduct = cusProductToProduct({
|
||||
cusProduct: curScheduledProduct,
|
||||
|
||||
138
server/tests/contUse/update/updateContUse6.ts
Normal file
138
server/tests/contUse/update/updateContUse6.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
type AppEnv,
|
||||
LegacyVersion,
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import type Stripe from "stripe";
|
||||
import { addPrefixToProducts, replaceItems } from "tests/attach/utils.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import {
|
||||
constructArrearProratedItem,
|
||||
constructFeatureItem,
|
||||
} from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { timeout } from "../../utils/genUtils.js";
|
||||
|
||||
const freeItem = constructFeatureItem({
|
||||
featureId: TestFeature.Users,
|
||||
includedUsage: 3,
|
||||
});
|
||||
|
||||
const paidUserItem = constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
pricePerUnit: 50,
|
||||
includedUsage: 10,
|
||||
config: {
|
||||
on_increase: OnIncrease.ProrateImmediately,
|
||||
on_decrease: OnDecrease.ProrateImmediately,
|
||||
},
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
items: [freeItem],
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
const testCase = "updateContUse6";
|
||||
|
||||
describe(`${chalk.yellowBright(`contUse/${testCase}: free product, continuous use, then upgrade to pro which has MORE included usage`)}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
const curUnix = Date.now();
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
const { autumnJs } = this;
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
autumn,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
const usage = 3;
|
||||
it("should attach free and track usage", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: usage,
|
||||
});
|
||||
await timeout(2000);
|
||||
});
|
||||
|
||||
const customItems = replaceItems({
|
||||
items: pro.items,
|
||||
featureId: TestFeature.Users,
|
||||
newItem: paidUserItem,
|
||||
});
|
||||
|
||||
it("should replace user item with paid and increased allowance", async () => {
|
||||
const customProduct = {
|
||||
...pro,
|
||||
items: customItems,
|
||||
};
|
||||
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
is_custom: true,
|
||||
items: customItems,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expect(customer.invoices?.[0].total).to.equal(0);
|
||||
});
|
||||
|
||||
// const extraUsage = 2;
|
||||
// const newItem = constructArrearProratedItem({
|
||||
// featureId: TestFeature.Users,
|
||||
// pricePerUnit: 50,
|
||||
// includedUsage: (userItem.included_usage as number) + extraUsage,
|
||||
// config: {
|
||||
// on_increase: OnIncrease.ProrateImmediately,
|
||||
// on_decrease: OnDecrease.ProrateImmediately,
|
||||
// },
|
||||
// });
|
||||
});
|
||||
@@ -148,7 +148,7 @@ export const completeInvoiceCheckout = async ({
|
||||
);
|
||||
if (postalInput) {
|
||||
await postalInput.click();
|
||||
await postalInput.type("SW79SJ");
|
||||
await postalInput.type("123123");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Could not find postal code input:", error);
|
||||
|
||||
@@ -63,6 +63,7 @@ export const SignIn = () => {
|
||||
setGoogleLoading(true);
|
||||
try {
|
||||
const frontendUrl = import.meta.env.VITE_FRONTEND_URL;
|
||||
|
||||
const { error } = await signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: `${frontendUrl}${callbackPath}`,
|
||||
|
||||
Reference in New Issue
Block a user