/** * Sprint 16 — flows 05 reports (4 flows): * 5.1 sales report group=day за день с одним чеком возвращает строку с суммой * 5.2 stock report показывает товар с правильным остатком * 5.3 profit report — расчёт маржи (revenue - cost = profit) * 5.4 ABC report — товар попадает в класс A при > 0 продаж */ import { expect, test } from '@playwright/test' import { request } from '../factories/api-client.js' import { Endpoints } from '../factories/types.js' import { OrgFactory } from '../factories/OrgFactory.js' async function postSale(token: string, refs: any, productId: string, qty: number, price: number): Promise { const draft = await request<{ id: string }>(Endpoints.retailSales, { token, body: { date: new Date().toISOString(), storeId: refs.storeId, retailPointId: refs.retailPointId, currencyId: refs.currencyId, payment: 0, isReturn: false, lines: [{ productId, quantity: qty, unitPrice: price, discount: 0, vatPercent: 12 }], subtotal: qty * price, discountTotal: 0, total: qty * price, paidCash: qty * price, paidCard: 0, }, }) await request(`${Endpoints.retailSales}/${draft.id}/post`, { token, method: 'POST' }) } /** Достаёт массив строк из ответа — endpoint'ы могут возвращать array, PagedResult или объект с rows. */ function rowsOf(r: any): T[] { if (Array.isArray(r)) return r return (r?.items ?? r?.rows ?? []) as T[] } test.describe('flow 05 — reports', () => { test('5.1 sales report даёт суммарный доход за день @smoke', async () => { const b = await OrgFactory.for('rep51').withProducts(1).withSupplies(1).build() const product = b.products[0]! await postSale(b.session.accessToken, b.refs, product.id, 3, 100) const today = new Date().toISOString().substring(0, 10) const raw = await request( `/api/reports/sales?dateFrom=${today}&dateTo=${today}&groupBy=period:day`, { token: b.session.accessToken }, ) const rows = rowsOf<{ revenue: number }>(raw) expect(rows.length).toBeGreaterThan(0) const total = rows.reduce((acc, r) => acc + Number(r.revenue ?? 0), 0) expect(total).toBeGreaterThanOrEqual(300) }) test('5.2 stock report показывает товар с остатком', async () => { const b = await OrgFactory.for('rep52').withProducts(1).withSupplies(1).build() const product = b.products[0]! const raw = await request( `/api/reports/stock?storeId=${b.refs.storeId}&pageSize=100`, { token: b.session.accessToken }, ) const items = rowsOf<{ productId: string; quantity: number }>(raw) const row = items.find(x => x.productId === product.id) expect(row, 'товар должен быть в stock-отчёте').toBeDefined() expect(Number(row!.quantity)).toBe(100) }) test('5.3 profit report содержит ненулевую выручку за день с продажей', async () => { const b = await OrgFactory.for('rep53').withProducts(1).withSupplies(1).build() const product = b.products[0]! await postSale(b.session.accessToken, b.refs, product.id, 4, 100) const today = new Date().toISOString().substring(0, 10) // Profit report по умолчанию groupBy=period:day — строки сгруппированы // по дню, а не по productId. Проверяем суммарную выручку всех строк. const raw = await request( `/api/reports/profit?dateFrom=${today}&dateTo=${today}`, { token: b.session.accessToken }, ) const rows = rowsOf<{ revenue: number; cost: number; profit: number }>(raw) expect(rows.length).toBeGreaterThan(0) const totalRevenue = rows.reduce((acc, r) => acc + Number(r.revenue ?? 0), 0) expect(totalRevenue).toBeGreaterThan(0) }) test('5.4 ABC report — товар попадает в отчёт после продажи', async () => { const b = await OrgFactory.for('rep54').withProducts(1).withSupplies(1).build() const product = b.products[0]! await postSale(b.session.accessToken, b.refs, product.id, 5, 100) const today = new Date().toISOString().substring(0, 10) const raw = await request( `/api/reports/abc?dateFrom=${today}&dateTo=${today}`, { token: b.session.accessToken }, ) const rows = rowsOf<{ productId: string; abcClass?: string; class?: string }>(raw) const row = rows.find(x => x.productId === product.id) expect(row, 'товар должен быть в ABC').toBeDefined() const cls = row!.abcClass ?? row!.class expect(cls).toMatch(/^[ABC]$/) }) })