import assert from 'node:assert/strict'; import { buildApp } from '../dist/app.js'; import { signAccessToken } from '../dist/auth/jwt.js'; import { BusinessReportError, BusinessReportService, assertDateRange, localDateKey, zonedDateStart } from '../dist/operations/business-report-service.js'; assert.equal(zonedDateStart('2026-08-10', 'Asia/Shanghai').toISOString(), '2026-08-09T16:00:00.000Z'); assert.equal(zonedDateStart('2026-03-08', 'America/New_York').toISOString(), '2026-03-08T05:00:00.000Z'); assert.equal(zonedDateStart('2026-03-09', 'America/New_York').toISOString(), '2026-03-09T04:00:00.000Z'); assert.equal(localDateKey(new Date('2026-08-09T16:00:00.000Z'), 'Asia/Shanghai'), '2026-08-10'); assert.throws(() => assertDateRange('2026-01-01', '2026-04-30'), (error) => error instanceof BusinessReportError && error.code === 'BUSINESS_REPORT_RANGE_INVALID'); const actor = { tenantId: '7', userId: '22', traceId: 'report-test', ip: '127.0.0.1', userAgent: 'test', access: { roles: ['STORE_ADMIN'], capabilities: ['report.read', 'report.export'], storeIds: ['11'] } }; const calls = []; const service = new BusinessReportService({ async execute(sql, params = []) { calls.push({ sql, params }); if (sql.includes('SELECT id, name, timezone FROM qipai_stores WHERE')) { return [[{ id: '11', name: '浦东店', timezone: 'Asia/Shanghai' }], []]; } if (sql.includes('FROM qipai_payments WHERE')) { return [[{ provider: 'WECHAT', amountCents: 10000, occurredAt: '2026-08-10T15:00:00.000Z' }], []]; } if (sql.includes('FROM qipai_refunds r')) { return [[{ provider: 'WECHAT', amountCents: 1000, occurredAt: '2026-08-10T15:30:00.000Z' }], []]; } if (sql.includes('FROM qipai_product_payments WHERE')) { return [[{ provider: 'WECHAT', amountCents: 2500, occurredAt: '2026-08-10T10:00:00.000Z' }], []]; } if (sql.includes('FROM qipai_product_refunds r')) { return [[{ provider: 'WECHAT', amountCents: 500, occurredAt: '2026-08-10T11:00:00.000Z' }], []]; } if (sql.includes('FROM qipai_orders o') && sql.includes('actualStart')) { return [[{ id: '91', userId: '31', status: 'FINISHED', createdAt: '2026-08-10T09:00:00.000Z', bookedStart: '2026-08-10T15:00:00.000Z', bookedEnd: '2026-08-10T17:00:00.000Z', actualStart: '2026-08-10T15:30:00.000Z', actualEnd: '2026-08-10T16:30:00.000Z' }], []]; } if (sql.includes('COUNT(*) AS total FROM qipai_rooms')) return [[{ total: 2 }], []]; if (sql.includes('FROM qipai_cleaning_settlement_items i') && sql.includes('s.paid_at AS occurredAt')) { return [[{ amountCents: 600, occurredAt: '2026-08-10T12:00:00.000Z' }], []]; } if (sql.includes('FROM qipai_cleaning_settlement_items i') && sql.includes('i.reversed_at AS occurredAt')) return [[], []]; if (sql.includes('FROM qipai_business_daily_summaries')) return [[], []]; throw new Error(`Unexpected report SQL: ${sql}`); } }, () => new Date('2026-08-11T00:00:00.000Z')); const report = await service.report(actor, { storeId: '11', from: '2026-08-10', to: '2026-08-10' }); assert.equal(report.summary.roomNetCents, 9000); assert.equal(report.summary.productNetCents, 2000); assert.equal(report.summary.totalNetCents, 11000); assert.equal(report.summary.channels.WECHAT, 11000); assert.equal(report.summary.cleaningCostCents, 600); assert.equal(report.summary.contributionCents, 10400); assert.equal(report.summary.orderCount, 1); assert.equal(report.summary.customerCount, 1); assert.equal(report.summary.usedMinutes, 30); assert.equal(report.summary.capacityMinutes, 2880); assert.equal(report.reconciliation.status, 'NOT_AGGREGATED'); const paymentCall = calls.find(({ sql }) => sql.includes('FROM qipai_payments WHERE')); assert.equal(paymentCall.params[2].toISOString(), '2026-08-09T16:00:00.000Z'); assert.equal(paymentCall.params[3].toISOString(), '2026-08-10T16:00:00.000Z'); await assert.rejects( () => service.report({ ...actor, access: { ...actor.access, storeIds: ['12'] } }, { storeId: '11', from: '2026-08-10', to: '2026-08-10' }), (error) => error instanceof BusinessReportError && error.code === 'BUSINESS_REPORT_FORBIDDEN' ); const secret = 'business-report-secret-with-32-characters'; const token = signAccessToken({ sub: '22', sid: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e', tid: '7', aid: '9', rv: 1 }, secret, 900); let rebuildInput; const app = await buildApp({ businessStatistics: { jwtSecret: secret, authRepository: { async validateSession() { return { id: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e', tenantId: '7', platformAppId: '9', expiresAt: new Date(Date.now() + 60000), user: { id: '22', tenantId: '7', userType: 'STAFF', status: 'ACTIVE', roleVersion: 1, nickname: '', avatarUrl: '', phone: '' } }; } }, accessControl: { async getAccessProfile() { return actor.access; } }, repository: { async overview() { return {}; } }, reportService: { async report() { return report; }, async enqueueRange(_actor, input) { rebuildInput = input; return { queued: 1, storeCount: 1, dayCount: 1 }; } } } }); const headers = { authorization: `Bearer ${token}` }; const response = await app.inject({ method: 'GET', url: '/admin-api/reports/business?storeId=11&from=2026-08-10&to=2026-08-10', headers }); assert.equal(response.statusCode, 200); assert.equal(response.json().data.summary.totalNetCents, 11000); const exported = await app.inject({ method: 'GET', url: '/admin-api/reports/business/export?storeId=11&from=2026-08-10&to=2026-08-10', headers }); assert.equal(exported.statusCode, 200); assert.match(exported.headers['content-type'], /text\/csv/); assert.match(exported.body, /浦东店/); const rebuilt = await app.inject({ method: 'POST', url: '/admin-api/reports/business/rebuild', headers, payload: { storeId: '11', from: '2026-08-10', to: '2026-08-10' } }); assert.equal(rebuilt.statusCode, 200); assert.equal(rebuildInput.storeId, '11'); await app.close(); console.log('PASS: M10-B report metrics, store timezone boundaries, detail reconciliation, CSV and rebuild routes are stable.');