114 lines
6.0 KiB
JavaScript
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.');
|