From 4b79bbcbc9b936102a583a8e13bcaaf198f612c8 Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Mon, 6 Jul 2026 12:37:11 +0200 Subject: [PATCH] test(server): add node --test harness with failing free-tier billing tests Co-Authored-By: Claude Fable 5 --- server/lib/billing.js | 6 +++ server/package.json | 22 +++++------ server/test/billing.test.js | 74 +++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 server/test/billing.test.js diff --git a/server/lib/billing.js b/server/lib/billing.js index 67d6882..8d13518 100644 --- a/server/lib/billing.js +++ b/server/lib/billing.js @@ -828,4 +828,10 @@ module.exports = { syncRevenueCatCustomerInfo, syncRevenueCatWebhookEvent, storeEndpointResponse, + // exported for tests + buildDefaultAccount, + alignAccountToCurrentCycle, + getAvailableCredits, + consumeCredits, + buildBillingSummary, }; diff --git a/server/package.json b/server/package.json index 4b44d01..7fcb60e 100644 --- a/server/package.json +++ b/server/package.json @@ -8,18 +8,18 @@ "rebuild:batches": "node scripts/rebuild-from-batches.js", "diagnostics": "node scripts/plant-diagnostics.js", "images:download": "node scripts/download-plant-images.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test test/" }, "keywords": [], "author": "", "license": "ISC", - "dependencies": { - "cors": "^2.8.6", - "dotenv": "^17.3.1", - "express": "^5.2.1", - "minio": "^8.0.5", - "pg": "^8.16.3", - "sharp": "^0.34.5", - "stripe": "^20.3.1" - } -} + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "minio": "^8.0.5", + "pg": "^8.16.3", + "sharp": "^0.34.5", + "stripe": "^20.3.1" + } +} diff --git a/server/test/billing.test.js b/server/test/billing.test.js new file mode 100644 index 0000000..7af7a42 --- /dev/null +++ b/server/test/billing.test.js @@ -0,0 +1,74 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { + buildDefaultAccount, + alignAccountToCurrentCycle, + getAvailableCredits, + consumeCredits, + getMonthlyAllowanceForPlan, +} = require('../lib/billing'); + +const NOW = new Date('2026-07-06T12:00:00Z'); + +const freeAccount = (overrides = {}) => ({ + ...buildDefaultAccount('user-1', NOW), + ...overrides, +}); + +test('free plan gets 3 monthly credits', () => { + assert.equal(getMonthlyAllowanceForPlan('free'), 3); + assert.equal(buildDefaultAccount('u', NOW).monthlyAllowance, 3); +}); + +test('free account has available credits', () => { + const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 1 }); + assert.equal(getAvailableCredits(account), 2); +}); + +test('free account topup balance counts as available', () => { + const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 3, topupBalance: 10 }); + assert.equal(getAvailableCredits(account), 10); +}); + +test('consumeCredits charges a free account from the monthly allowance', () => { + const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 0 }); + const charged = consumeCredits(account, 1); + assert.equal(charged, 1); + assert.equal(account.usedThisCycle, 1); +}); + +test('consumeCredits throws 402 for an exhausted free account', () => { + const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 3, topupBalance: 0 }); + assert.throws(() => consumeCredits(account, 1), (error) => { + assert.equal(error.code, 'INSUFFICIENT_CREDITS'); + assert.equal(error.status, 402); + assert.deepEqual(error.metadata, { required: 1, available: 0 }); + return true; + }); +}); + +test('legacy free account with allowance 0 is migrated to 3', () => { + const account = freeAccount({ monthlyAllowance: 0 }); + const aligned = alignAccountToCurrentCycle(account, NOW); + assert.equal(aligned.monthlyAllowance, 3); +}); + +test('pro and trial allowances are unchanged', () => { + assert.equal(getMonthlyAllowanceForPlan('pro'), 100); + const trial = freeAccount({ plan: 'pro', monthlyAllowance: 30, usedThisCycle: 5 }); + const aligned = alignAccountToCurrentCycle(trial, NOW); + assert.equal(aligned.monthlyAllowance, 30); // trial allowance stays allowed + assert.equal(getAvailableCredits(aligned), 25); +}); + +test('monthly cycle rollover resets free usage', () => { + const account = freeAccount({ + monthlyAllowance: 3, + usedThisCycle: 3, + cycleEndsAt: '2026-07-01T00:00:00.000Z', + }); + const aligned = alignAccountToCurrentCycle(account, NOW); + assert.equal(aligned.usedThisCycle, 0); + assert.equal(aligned.monthlyAllowance, 3); + assert.equal(getAvailableCredits(aligned), 3); +});