feat(F-146): completed feature

This commit is contained in:
chattie
2026-08-22 12:52:13 +02:00
parent 62a3681657
commit 91044da178
17 changed files with 1182 additions and 37 deletions

View File

@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import type pg from 'pg';
import Fastify, { type FastifyInstance } from 'fastify';
import { AppError, errorEnvelope } from '../../../shared/errors.js';
import { registerReportingRoutes } from './reporting.routes.js';
@@ -30,8 +31,12 @@ async function buildApp(authenticatedUser: unknown) {
.code(500)
.send(errorEnvelope(500, 'INTERNAL_ERROR', 'Internal Server Error', 'test-request-id'));
});
const mockPool = {
query: vi.fn().mockResolvedValue({ rows: [] }),
} as unknown as pg.Pool;
const deps = {
authenticate: vi.fn().mockResolvedValue(authenticatedUser),
pool: mockPool,
} as unknown as ReportingRoutesDeps;
await registerReportingRoutes(app, deps);
await app.ready();
@@ -137,3 +142,99 @@ describe('GET /reporting/filters/validate (REPORTING_SALES)', () => {
expect(res.statusCode).toBe(403);
});
});
// ── F-146: Summary and Sales endpoint tests ──────────────────────────────
describe('GET /reporting/summary (REPORTING_SALES)', () => {
const BASE = '/reporting/summary?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z';
it('returns summary DTO with all required fields (AC1)', async () => {
const { app } = await buildApp(ADMIN);
const res = await app.inject({ method: 'GET', url: BASE });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body ?? '') as Record<string, unknown>;
expect(body).toHaveProperty('range');
expect(body).toHaveProperty('filters');
expect(body).toHaveProperty('comparison');
expect(body).toHaveProperty('dataAvailability');
expect(body).toHaveProperty('totals');
expect(body).toHaveProperty('updatedAt');
expect(body).toHaveProperty('cache');
});
it('returns correct dataAvailability flags (AC5)', async () => {
const { app } = await buildApp(ADMIN);
const res = await app.inject({ method: 'GET', url: BASE });
const body = JSON.parse(res.body ?? '') as {
dataAvailability: Record<string, string>;
};
expect(body.dataAvailability.grossSales).toBe('available');
expect(body.dataAvailability.netSales).toBe('unavailable');
expect(body.dataAvailability.margin).toBe('unavailable');
expect(body.dataAvailability.paymentMethod).toBe('unavailable');
expect(body.dataAvailability.shipping).toBe('available');
expect(body.dataAvailability.discounts).toBe('available');
});
it('returns 400 on inverted dates (AC7)', async () => {
const { app } = await buildApp(ADMIN);
const res = await app.inject({
method: 'GET',
url: '/reporting/summary?from=2026-08-31T00:00:00Z&to=2026-08-01T00:00:00Z',
});
expect(res.statusCode).toBe(400);
});
it('customer is forbidden (RBAC, AC8)', async () => {
const { app } = await buildApp(CUSTOMER);
const res = await app.inject({ method: 'GET', url: BASE });
expect(res.statusCode).toBe(403);
});
});
describe('GET /reporting/sales (REPORTING_SALES)', () => {
const BASE = '/reporting/sales?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z';
it('returns sales DTO with items and pagination (AC2)', async () => {
const { app } = await buildApp(ADMIN);
const res = await app.inject({ method: 'GET', url: `${BASE}&groupBy=day` });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body ?? '') as Record<string, unknown>;
expect(body).toHaveProperty('range');
expect(body).toHaveProperty('filters');
expect(body).toHaveProperty('items');
expect(body).toHaveProperty('totals');
expect(body).toHaveProperty('pagination');
expect(body).toHaveProperty('updatedAt');
expect(Array.isArray((body as Record<string, unknown>).items)).toBe(true);
});
it('pagination fields present (AC4)', async () => {
const { app } = await buildApp(ADMIN);
const res = await app.inject({ method: 'GET', url: `${BASE}&groupBy=month&page=2&pageSize=20` });
const body = JSON.parse(res.body ?? '') as {
pagination: { page: number; pageSize: number; totalRows: number };
};
expect(body.pagination.page).toBe(2);
expect(body.pagination.pageSize).toBe(20);
});
it('returns 400 on invalid channel (AC7)', async () => {
const { app } = await buildApp(ADMIN);
const res = await app.inject({
method: 'GET',
url: '/reporting/sales?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z&channel=invalid',
});
expect(res.statusCode).toBe(400);
});
it('customer is forbidden (RBAC, AC8)', async () => {
const { app } = await buildApp(CUSTOMER);
const res = await app.inject({ method: 'GET', url: BASE });
expect(res.statusCode).toBe(403);
});
});