import assert from 'node:assert/strict'; import test from 'node:test'; import { attachPortalBillingRoutes, } from './portal-billing-routes.mjs'; function createResponse() { return { statusCode: 200, body: undefined, status(code) { this.statusCode = code; return this; }, json(body) { this.body = body; return this; }, }; } function createSetup(overrides = {}) { const routes = []; const calls = []; let userAuth = { async getMe(token) { calls.push(['get-me', token]); return { id: 'user-1', status: 'active', }; }, async listBillingLedger(options) { calls.push(['ledger', options]); return [{ id: 'entry-1' }]; }, async getUserById(userId) { calls.push(['get-user', userId]); return { id: userId, balance_cents: 8800, }; }, async purchaseSpaceQuota(userId, sizeMb) { calls.push([ 'space-purchase', userId, sizeMb, ]); return { ok: true, quota: { quotaBytes: 1024, }, balanceCents: 5000, }; }, }; let rechargeService = { async getBillingConfig(userId) { calls.push(['billing-config', userId]); return { minRechargeCents: 1000, }; }, async createOrder(options) { calls.push(['create-order', options]); return { ok: true, order: { id: 'order-1', status: 'pending', }, }; }, async getOrderForUser(userId, orderId) { calls.push([ 'get-order', userId, orderId, ]); return { id: orderId, status: 'paid', }; }, }; let subscriptionService = { async getActiveSubscription(userId) { calls.push(['subscription', userId]); return { planType: 'pro', }; }, async purchaseSubscription( userId, planType, autoRenew, ) { calls.push([ 'subscribe', userId, planType, autoRenew, ]); return { ok: true, subscription: { planType, autoRenew, }, balanceCents: 6800, }; }, async setAutoRenew(userId, enabled) { calls.push([ 'auto-renew', userId, enabled, ]); return { ok: true, enabled, }; }, }; let mindSpace = { async getQuota(userId) { calls.push(['quota', userId]); return { quotaBytes: 2048, }; }, }; const jsonBody = (_req, _res, next) => next(); const app = { get(path, ...handlers) { routes.push({ method: 'get', path, handlers, }); }, post(path, ...handlers) { routes.push({ method: 'post', path, handlers, }); }, }; const options = { app, jsonBody, userAuthReady: Promise.resolve(), getUserAuth: () => userAuth, getRechargeService: () => rechargeService, getSubscriptionService: () => subscriptionService, getMindSpace: () => mindSpace, userToken: () => 'request-token', planCatalogFallback: { free: { planType: 'free', priceCents: 0, }, pro: { planType: 'pro', priceCents: 5000, }, }, ...overrides, }; attachPortalBillingRoutes(options); return { routes, calls, jsonBody, setUserAuth(value) { userAuth = value; }, setRechargeService(value) { rechargeService = value; }, setSubscriptionService(value) { subscriptionService = value; }, setMindSpace(value) { mindSpace = value; }, route(method, path) { return routes.find( (route) => route.method === method && route.path === path, ); }, }; } async function invoke( setup, method, path, req = {}, ) { const route = setup.route(method, path); assert.ok( route, `${method.toUpperCase()} ${path}`, ); const res = createResponse(); await route.handlers.at(-1)( { body: {}, query: {}, params: {}, ip: '127.0.0.1', ...req, }, res, ); return res; } test('registers the billing route inventory and JSON middleware order', () => { const setup = createSetup(); assert.deepEqual( setup.routes.map(({ method, path }) => [ method, path, ]), [ ['get', '/auth/billing/ledger'], ['get', '/auth/billing/config'], ['get', '/auth/billing/subscription'], ['get', '/auth/billing/plans'], ['post', '/auth/billing/subscribe'], ['post', '/auth/billing/space-purchase'], ['post', '/auth/billing/auto-renew'], ['post', '/auth/billing/recharge-orders'], [ 'get', '/auth/billing/recharge-orders/:orderId', ], ], ); for (const route of setup.routes.filter( ({ method }) => method === 'post', )) { assert.equal(route.handlers[0], setup.jsonBody); } }); test('preserves billing ledger scope and billing configuration', async () => { const setup = createSetup(); const ledger = await invoke( setup, 'get', '/auth/billing/ledger', { query: { limit: '500' }, }, ); assert.deepEqual(ledger.body, { entries: [{ id: 'entry-1' }], }); assert.ok( setup.calls.some( (call) => call[0] === 'ledger' && call[1].limit === 100 && call[1].types.join(',') === 'recharge,adjust,refund', ), ); const config = await invoke( setup, 'get', '/auth/billing/config', ); assert.deepEqual(config.body, { minRechargeCents: 1000, subscription: { planType: 'pro', }, }); }); test('preserves subscription detail and plan catalog projections', async () => { const setup = createSetup(); setup.setSubscriptionService(null); const subscription = await invoke( setup, 'get', '/auth/billing/subscription', ); assert.deepEqual(subscription.body, { subscription: null, plans: { free: { planType: 'free', priceCents: 0, }, pro: { planType: 'pro', priceCents: 5000, }, }, }); const plans = await invoke( setup, 'get', '/auth/billing/plans', ); assert.deepEqual(plans.body, { plans: [ { key: 'pro', planType: 'pro', priceCents: 5000, }, ], subscription: null, balanceCents: 8800, }); const dynamic = createSetup(); dynamic.setSubscriptionService({ _planCatalogService: { async listPlans(options) { dynamic.calls.push([ 'catalog', options, ]); return [ { planType: 'free', priceCents: 0, }, { planType: 'team', priceCents: 12000, }, ]; }, }, async getActiveSubscription() { return null; }, }); const dynamicPlans = await invoke( dynamic, 'get', '/auth/billing/plans', ); assert.deepEqual(dynamicPlans.body.plans, [ { key: 'team', planType: 'team', priceCents: 12000, }, ]); }); test('preserves subscription purchase validation and error mappings', async () => { const missing = createSetup(); const missingRes = await invoke( missing, 'post', '/auth/billing/subscribe', ); assert.equal(missingRes.statusCode, 400); const limited = createSetup(); limited.setSubscriptionService({ async purchaseSubscription() { return { ok: false, code: 'INSUFFICIENT_BALANCE', message: '余额不足', balanceCents: 100, requiredCents: 5000, shortfallCents: 4900, }; }, }); const limitedRes = await invoke( limited, 'post', '/auth/billing/subscribe', { body: { planType: 'pro', autoRenew: true, }, }, ); assert.equal(limitedRes.statusCode, 402); assert.equal(limitedRes.body.shortfallCents, 4900); const downgrade = createSetup(); downgrade.setSubscriptionService({ async purchaseSubscription() { return { ok: false, code: 'DOWNGRADE_NOT_ALLOWED', message: '不可降级', currentPlanType: 'team', }; }, }); const downgradeRes = await invoke( downgrade, 'post', '/auth/billing/subscribe', { body: { planType: 'pro' }, }, ); assert.equal(downgradeRes.statusCode, 409); const success = createSetup(); const successRes = await invoke( success, 'post', '/auth/billing/subscribe', { body: { planType: 'pro', autoRenew: 1, }, }, ); assert.deepEqual(successRes.body, { subscription: { planType: 'pro', autoRenew: true, }, balanceCents: 6800, }); }); test('preserves space purchase success and insufficient-balance details', async () => { const setup = createSetup(); const success = await invoke( setup, 'post', '/auth/billing/space-purchase', { body: { sizeMb: '20' }, }, ); assert.deepEqual(success.body, { quota: { quotaBytes: 2048, }, balanceCents: 5000, purchasedMb: 20, costCents: 4000, }); const limited = createSetup(); limited.setUserAuth({ async getMe() { return { id: 'user-1' }; }, async purchaseSpaceQuota() { return { ok: false, code: 'INSUFFICIENT_BALANCE', message: '余额不足', balanceCents: 20, minRechargeCents: 1000, suggestedTiers: [1000, 5000], }; }, }); const limitedRes = await invoke( limited, 'post', '/auth/billing/space-purchase', { body: { sizeMb: 10 }, }, ); assert.equal(limitedRes.statusCode, 402); assert.equal( limitedRes.body.details.minRechargeCents, 1000, ); }); test('preserves auto-renew validation and update', async () => { const setup = createSetup(); const invalid = await invoke( setup, 'post', '/auth/billing/auto-renew', { body: { enabled: 'true' }, }, ); assert.equal(invalid.statusCode, 400); const updated = await invoke( setup, 'post', '/auth/billing/auto-renew', { body: { enabled: false }, }, ); assert.deepEqual(updated.body, { ok: true, enabled: false, }); }); test('preserves recharge order creation scene mapping and errors', async () => { const setup = createSetup(); const res = await invoke( setup, 'post', '/auth/billing/recharge-orders', { body: { amountCents: '3000', payScene: 'invalid', }, ip: '203.0.113.10', }, ); assert.equal(res.statusCode, 201); assert.ok( setup.calls.some( (call) => call[0] === 'create-order' && call[1].amountCents === 3000 && call[1].payScene === 'native' && call[1].clientIp === '203.0.113.10', ), ); const failing = createSetup(); failing.setRechargeService({ async createOrder() { return { ok: false, message: '金额无效', }; }, }); const failingRes = await invoke( failing, 'post', '/auth/billing/recharge-orders', ); assert.equal(failingRes.statusCode, 400); }); test('preserves recharge order lookup and paid balance projection', async () => { const setup = createSetup(); const res = await invoke( setup, 'get', '/auth/billing/recharge-orders/:orderId', { params: { orderId: 'order-1' }, }, ); assert.deepEqual(res.body, { order: { id: 'order-1', status: 'paid', }, balanceCents: 8800, }); const missing = createSetup(); missing.setRechargeService({ async getOrderForUser() { return null; }, }); const missingRes = await invoke( missing, 'get', '/auth/billing/recharge-orders/:orderId', { params: { orderId: 'missing' }, }, ); assert.equal(missingRes.statusCode, 404); }); test('preserves billing service availability and authentication gates', async () => { const unavailable = createSetup(); unavailable.setUserAuth(null); const unavailableRes = await invoke( unavailable, 'get', '/auth/billing/ledger', ); assert.equal(unavailableRes.statusCode, 503); const unauthorized = createSetup(); unauthorized.setUserAuth({ async getMe() { return null; }, }); const unauthorizedRes = await invoke( unauthorized, 'get', '/auth/billing/plans', ); assert.equal(unauthorizedRes.statusCode, 401); });