130 lines
4.4 KiB
JavaScript
130 lines
4.4 KiB
JavaScript
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.');
|