krona-staff/tests/e2e/harness/fixtures.ts
grok 3475133b10 test(e2e): синтетические суммы кратны рублю (учёт округления расчёта)
Зачем: на ветке codex/fix-live-database цепочка подтверждение → расчёт →
утверждение впервые дошла до конца, и K3/N2 остался красным не из-за
дефекта, а из-за теста: 8 ч × 123,45 ₽ = 98760 коп., а расчёт округляет
строку до rounding_unit_minor (по умолчанию 100 коп., service.ts:695) и
начисляет 98800. Округление до рубля — настройка продукта, не дефект;
тест не должен от него зависеть.

Что: SYNTHETIC_RATE_MINOR 12345 → 12300 (123,00 ₽/ч), оплата смены
официанта в K9 111111 → 111100. Суммы остаются заведомо синтетическими.
2026-09-26 15:25:47 +07:00

159 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Подготовка данных через публичный API. Везде только синтетические данные.
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { expect } from 'vitest';
import { api, ctx, dbQuery, describeResponse } from './client.js';
import { randomUlid, synthetic } from './context.js';
export interface OrgUnit {
id: string;
code: string;
}
export interface Employee {
id: string;
external_ref: string;
personnel_number: string;
}
/** Создаёт торговые точки через импорт (единственный публичный способ в текущем API). */
export async function createOrgUnits(count: number): Promise<OrgUnit[]> {
const units = Array.from({ length: count }, () => {
const code = `e2e-${randomUlid().slice(-10).toLowerCase()}`;
return { code, name: synthetic('Точка') };
});
const response = await api('POST', '/imports/owner-workbook', {
token: ctx().managerToken,
body: { source_name: synthetic('import-units'), units },
});
expect(response.status, describeResponse(response)).toBeLessThan(300);
const rows = await dbQuery<{ id: string; code: string }>(
'SELECT id, code FROM staff_org_units WHERE code = ANY($1::text[])',
[units.map((unit) => unit.code)],
);
return units.map((unit) => ({
code: unit.code,
id: rows.find((row) => row.code === unit.code)!.id,
}));
}
/**
* Создаёт сотрудников через импорт книги владельца: POST /employees на реальной БД
* сейчас падает (N1), а сценарии, не относящиеся к N1, не должны зависеть от этого дефекта.
*/
export async function importEmployees(count: number): Promise<Employee[]> {
const employees = Array.from({ length: count }, () => {
const suffix = randomUlid().slice(-8);
return {
external_ref: `e2e:${suffix}`,
personnel_number: `E2E-${suffix}`,
display_name: synthetic('Сотрудник'),
};
});
const response = await api('POST', '/imports/owner-workbook', {
token: ctx().managerToken,
body: { source_name: synthetic('import-employees'), employees },
});
expect(response.status, describeResponse(response)).toBeLessThan(300);
const rows = await dbQuery<{ id: string; external_ref: string }>(
'SELECT id, external_ref FROM staff_employees WHERE external_ref = ANY($1::text[])',
[employees.map((employee) => employee.external_ref)],
);
return employees.map((employee) => ({
...employee,
id: rows.find((row) => row.external_ref === employee.external_ref)!.id,
}));
}
export async function issueEmployeeToken(employeeId: string): Promise<string> {
const response = await api('POST', `/employees/${employeeId}/access-token`, {
token: ctx().managerToken,
});
expect(response.status, describeResponse(response)).toBe(200);
return response.body.access_token as string;
}
export async function clock(
employeeId: string,
eventType: 'check_in' | 'check_out',
occurredAt: Date,
orgUnitId?: string,
) {
return api('POST', '/attendance/events', {
token: ctx().terminalToken,
body: {
employee_id: employeeId,
event_type: eventType,
occurred_at: occurredAt.toISOString(),
source_event_id: `e2e-${randomUlid()}`,
...(orgUnitId ? { org_unit_id: orgUnitId } : {}),
},
});
}
/** Дата (UTC) N дней назад в полдень: всегда в окне терминала (<=180 дней) и не в будущем. */
export function daysAgo(days: number, hourUtc = 9): Date {
const date = new Date();
date.setUTCDate(date.getUTCDate() - days);
date.setUTCHours(hourUtc, 0, 0, 0);
return date;
}
export function isoDate(date: Date): string {
return date.toISOString().slice(0, 10);
}
/** Отработанная смена: check_in/check_out терминалом + сверка; возвращает id смены. */
export async function workedShift(
employeeId: string,
orgUnitId: string,
day: Date,
hours = 8,
): Promise<string> {
const start = new Date(day);
const end = new Date(start.getTime() + hours * 3_600_000);
const inResponse = await clock(employeeId, 'check_in', start, orgUnitId);
expect(inResponse.status, describeResponse(inResponse)).toBe(201);
const outResponse = await clock(employeeId, 'check_out', end, orgUnitId);
expect(outResponse.status, describeResponse(outResponse)).toBe(201);
const reconcile = await api('POST', '/work-sessions/reconcile', {
token: ctx().managerToken,
body: { date_from: isoDate(start), date_to: isoDate(start) },
});
expect(reconcile.status, describeResponse(reconcile)).toBe(200);
const rows = await dbQuery<{ id: string }>(
`SELECT id FROM staff_work_sessions
WHERE employee_id = $1 AND org_unit_id = $2 AND business_date = $3::date`,
[employeeId, orgUnitId, isoDate(start)],
);
expect(rows.length, 'сверка должна создать смену').toBe(1);
return rows[0]!.id;
}
export async function hourlyRuleId(): Promise<string> {
const response = await api('GET', '/pay-rules', { token: ctx().managerToken });
expect(response.status, describeResponse(response)).toBe(200);
const rules = response.body.pay_rules as Array<{ id: string; code: string }>;
const hourly = rules.find((rule) => rule.code === 'hourly') ?? rules[0];
expect(hourly, 'должно существовать хотя бы одно правило оплаты').toBeTruthy();
return hourly!.id;
}
/**
* Синтетическая ставка 123,00 ₽/ч (заведомо не реальная сумма). Кратна рублю: расчёт
* округляет строку до rounding_unit_minor (по умолчанию 100 коп.), и 8 ч × ставка
* должны совпадать с начислением без учёта округления.
*/
export const SYNTHETIC_RATE_MINOR = 12_300;
export async function assignHourly(employeeId: string, orgUnitId: string | null) {
const response = await api('PUT', `/employees/${employeeId}/pay-assignment`, {
token: ctx().managerToken,
body: {
pay_rule_id: await hourlyRuleId(),
org_unit_id: orgUnitId,
rate_minor: SYNTHETIC_RATE_MINOR,
effective_from: '2026-01-01',
},
});
expect(response.status, describeResponse(response)).toBe(200);
}