From ef2fac6fe3d0d6895c880df1c05039c8ddec630b Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 27 Oct 2025 06:42:50 -0700 Subject: [PATCH] fix: edge case for update custom ent, free cont use -> paid cont use, allowance increase --- package.json | 1 + scripts/package.json | 3 +- scripts/replicate.ts | 284 ++++++++++++++++++ scripts/start-dev.js | 129 +++++++- scripts/testGroups/g1.sh | 14 +- server/src/check.ts | 8 +- .../upgradeFlow/updateStripeSub2.ts | 29 -- .../createContUseInvoiceItems.ts | 2 +- .../getContUseItems/getContUseInvoiceItems.ts | 16 +- server/src/utils/auth.ts | 2 +- .../utils/checkUtils/checkCustomerCorrect.ts | 2 - server/tests/contUse/update/updateContUse6.ts | 138 +++++++++ .../stripeUtils/completeInvoiceCheckout.ts | 2 +- vite/src/views/auth/SignIn.tsx | 1 + 14 files changed, 568 insertions(+), 63 deletions(-) create mode 100644 scripts/replicate.ts create mode 100644 server/tests/contUse/update/updateContUse6.ts diff --git a/package.json b/package.json index 597ec1255..4b9e36efa 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/package.json b/scripts/package.json index cfe6dd785..39230d879 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -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:*", diff --git a/scripts/replicate.ts b/scripts/replicate.ts new file mode 100644 index 000000000..a9c4656d1 --- /dev/null +++ b/scripts/replicate.ts @@ -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 { + 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 { + 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 "" ""\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); +}); diff --git a/scripts/start-dev.js b/scripts/start-dev.js index 4b035bcc2..87624004d 100644 --- a/scripts/start-dev.js +++ b/scripts/start-dev.js @@ -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(), }, }, ); diff --git a/scripts/testGroups/g1.sh b/scripts/testGroups/g1.sh index 02b2ce875..c5fab0a6b 100755 --- a/scripts/testGroups/g1.sh +++ b/scripts/testGroups/g1.sh @@ -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' \ diff --git a/server/src/check.ts b/server/src/check.ts index 27f711a96..c68a5605a 100644 --- a/server/src/check.ts +++ b/server/src/check.ts @@ -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, diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts index d01b8aa63..0ad86bf35 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts @@ -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, -// }); -// } -// } diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts index 219effa70..f9c7c6f41 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts @@ -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 }); diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts index 1a678a4e5..3e396d03e 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts @@ -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, diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index 3e000aa2c..f4dd8ee1f 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -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: [ diff --git a/server/src/utils/checkUtils/checkCustomerCorrect.ts b/server/src/utils/checkUtils/checkCustomerCorrect.ts index 11fa1dab5..7a9e47b28 100644 --- a/server/src/utils/checkUtils/checkCustomerCorrect.ts +++ b/server/src/utils/checkUtils/checkCustomerCorrect.ts @@ -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, diff --git a/server/tests/contUse/update/updateContUse6.ts b/server/tests/contUse/update/updateContUse6.ts new file mode 100644 index 000000000..dfdf32687 --- /dev/null +++ b/server/tests/contUse/update/updateContUse6.ts @@ -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, + // }, + // }); +}); diff --git a/server/tests/utils/stripeUtils/completeInvoiceCheckout.ts b/server/tests/utils/stripeUtils/completeInvoiceCheckout.ts index d8b13e54d..7f100bd9d 100644 --- a/server/tests/utils/stripeUtils/completeInvoiceCheckout.ts +++ b/server/tests/utils/stripeUtils/completeInvoiceCheckout.ts @@ -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); diff --git a/vite/src/views/auth/SignIn.tsx b/vite/src/views/auth/SignIn.tsx index bc9a79dac..8581bc93e 100644 --- a/vite/src/views/auth/SignIn.tsx +++ b/vite/src/views/auth/SignIn.tsx @@ -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}`,