Files
qipai/backend/tests/business-report.test.mjs
T

114 lines
6.0 KiB
JavaScript

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.');