import assert from 'node:assert/strict'; import { buildApp } from '../dist/app.js'; import { signAccessToken } from '../dist/auth/jwt.js'; import { BusinessStatisticsError, BusinessStatisticsRepository } from '../dist/operations/business-statistics-repository.js'; const actor = { tenantId: '7', userId: '22', access: { roles: ['STORE_ADMIN'], capabilities: ['store.operation.read'], storeIds: ['11'] }, traceId: 'business-stats-test', ip: '127.0.0.1', userAgent: 'test' }; const repository = new BusinessStatisticsRepository({ async execute(sql) { if (sql.includes('FROM qipai_orders o') && sql.includes('COUNT(DISTINCT owner.user_id)')) { return [[{ orderTotal: 12, activeOrderTotal: 3, finishedOrderTotal: 7, bookedAmountCents: 36000, payingMemberTotal: 9 }], []]; } if (sql.includes('FROM qipai_orders o') && sql.includes('GROUP BY o.status')) { return [[{ status: 'FINISHED', total: 7, amountCents: 21000 }], []]; } if (sql.includes('FROM qipai_payments p') && sql.includes('GROUP BY p.channel')) { return [[ { channel: 'WECHAT', total: 5, amountCents: 15000 }, { channel: 'GROUP_BUY', total: 2, amountCents: 6000 } ], []]; } if (sql.includes('FROM qipai_payments p') && sql.includes('DATE_FORMAT')) { return [[{ date: '2026-08-10', total: 7, amountCents: 21000 }], []]; } if (sql.includes('FROM qipai_rooms')) { return [[{ roomTotal: 8, availableRoomTotal: 4, attentionRoomTotal: 1 }], []]; } if (sql.includes('FROM qipai_group_redemptions')) { return [[{ voucherTotal: 3, voucherSucceeded: 2, voucherFailed: 1 }], []]; } throw new Error(`Unexpected statistics SQL: ${sql}`); } }); const overview = await repository.overview(actor, { storeId: '11', from: new Date('2026-08-01T00:00:00.000Z'), to: new Date('2026-09-01T00:00:00.000Z') }); assert.equal(overview.summary.orderTotal, 12); assert.equal(overview.summary.collectedAmountCents, 21000); assert.equal(overview.summary.voucherSucceeded, 2); assert.equal(overview.summary.availableRoomTotal, 4); assert.equal(overview.dailyRevenue[0].date, '2026-08-10'); await assert.rejects( () => repository.overview(actor, { storeId: '12', from: new Date('2026-08-01T00:00:00.000Z'), to: new Date('2026-09-01T00:00:00.000Z') }), (error) => error instanceof BusinessStatisticsError && error.code === 'BUSINESS_STATISTICS_FORBIDDEN' ); const secret = 'business-statistics-secret-with-32-characters'; const token = signAccessToken({ sub: '22', sid: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e', tid: '7', aid: '9', rv: 1 }, secret, 900); let routed; 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(routedActor, input) { routed = { actor: routedActor, input }; return overview; } } } }); const response = await app.inject({ method: 'GET', url: '/app-api/management/statistics?storeId=11&from=2026-08-01T00%3A00%3A00.000Z&to=2026-09-01T00%3A00%3A00.000Z', headers: { authorization: `Bearer ${token}` } }); assert.equal(response.statusCode, 200); assert.equal(response.json().data.summary.collectedAmountCents, 21000); assert.equal(routed.actor.userId, '22'); assert.equal(routed.input.storeId, '11'); const adminResponse = await app.inject({ method: 'GET', url: '/admin-api/statistics?storeId=11&from=2026-08-01T00%3A00%3A00.000Z&to=2026-09-01T00%3A00%3A00.000Z', headers: { authorization: `Bearer ${token}` } }); assert.equal(adminResponse.statusCode, 200); assert.equal(adminResponse.json().data.summary.orderTotal, 12); const invalidRange = await app.inject({ method: 'GET', url: '/app-api/management/statistics?storeId=11&from=2026-01-01&to=2026-09-01', headers: { authorization: `Bearer ${token}` } }); assert.equal(invalidRange.statusCode, 400); await app.close(); console.log('PASS: M08-C business statistics enforce store scope and aggregate stable metrics.');