- types/api.ts: LineStatus, Severity, IssueKind, PeriodKind, FileWarningOut, IssueOut, EditionLineOut, IssueCountsOut, CellTarget, LinePatch, ColumnPatch; новые поля StatementHeaderOut, ColumnOut, ValueOut, LineOut, FailedCheckOut, StatementOut (form_variant, issues, edition_lines), DocumentBrief (issue_counts), DocumentOut (issues), JobBrief.warnings — FileWarningOut[]. - statementGrid.ts: GridRow получает статус строки, номер строки, код как распознан, флаги needs_review / is_confirmed / codeModified; cellFlags: внимание только по needs_review, новый флаг confirmed. - labels.ts: lineStatusLabel, severityLabel. - фикстура __fixtures__/statement-v2.json (БАЛАНС 2025 Q3, NEEDS_CHECK): 16 строк × 2 колонки, у каждой пары ValueOut. - тесты: переносы полей v2 и покрытие пар строка × колонка, cellFlags по needs_review, подписи; хелперы row() / v() / gridRow / doc() дополнены значениями по умолчанию. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
197 lines
8.3 KiB
TypeScript
197 lines
8.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import type { StatementOut, ValueOut } from '../../types/api';
|
||
import statementV2Json from './__fixtures__/statement-v2.json';
|
||
import type { GridRow } from './statementGrid';
|
||
import { applyDraftInput, buildStatementGrid, cellFlags, cellKey, draftToPatch, emptySublineKeys } from './statementGrid';
|
||
|
||
const v = (id: string | null, column_id: string, column_index: number, extra: Partial<ValueOut> = {}): ValueOut => ({
|
||
id,
|
||
column_id,
|
||
column_index,
|
||
amount: '1.00',
|
||
string_value: null,
|
||
text: null,
|
||
confidence: '100',
|
||
is_recognized: true,
|
||
is_modified: false,
|
||
original_amount: null,
|
||
needs_review: false,
|
||
is_confirmed: false,
|
||
...extra,
|
||
});
|
||
|
||
// Фикстура v5.19 (тип StatementOut): приведение нужно, т.к. импорт JSON расширяет строковые литералы до string
|
||
const statementV2 = statementV2Json as StatementOut;
|
||
|
||
const statement = {
|
||
columns: [
|
||
{ id: 'c2', column_index: 1, label: '31.12.2023', period_date: null, is_current: false },
|
||
{ id: 'c1', column_index: 0, label: '30.09.2024', period_date: null, is_current: true },
|
||
],
|
||
lines: [{ id: 'l1', origin: 'source', line_code: '1600', line_name: 'Баланс', values: [v('v2', 'c2', 1), v('v1', 'c1', 0)] }],
|
||
} as unknown as StatementOut;
|
||
|
||
describe('buildStatementGrid', () => {
|
||
it('колонки по column_index, ячейки по column_id', () => {
|
||
const grid = buildStatementGrid(statement);
|
||
expect(grid.columns.map((c) => c.id)).toEqual(['c1', 'c2']);
|
||
expect(grid.rows[0]).toMatchObject({ key: 'l1', lineId: 'l1', code: '1600', name: 'Баланс', origin: 'source' });
|
||
expect(grid.rows[0].cells.c1?.id).toBe('v1');
|
||
expect(grid.rows[0].cells.c2?.id).toBe('v2');
|
||
});
|
||
it('буквально переносит поля v2 из фикстуры', () => {
|
||
const { rows } = buildStatementGrid(statementV2);
|
||
const byKey = new Map(rows.map((row) => [row.key, row]));
|
||
expect(byKey.get('l-u7')).toMatchObject({
|
||
lineStatus: 'unreadable',
|
||
rowIndex: 7,
|
||
codeText: 'ИЧ,250',
|
||
code: null,
|
||
key: 'l-u7',
|
||
});
|
||
expect(byKey.get('l-d2')?.parentRowIndex).toBe(1);
|
||
expect(byKey.get('code-1250')).toMatchObject({
|
||
key: 'code-1250',
|
||
rowIndex: null,
|
||
referenceName: 'Денежные средства и денежные эквиваленты',
|
||
});
|
||
expect(byKey.get('l-1230')).toMatchObject({ needsReview: true, codeConfidence: '50' });
|
||
});
|
||
it('каждая пара «строка × колонка» фикстуры имеет ValueOut', () => {
|
||
const { columns, rows } = buildStatementGrid(statementV2);
|
||
for (const row of rows) {
|
||
for (const column of columns) {
|
||
expect(row.cells[column.id], `${row.key} × ${column.id}`).toBeDefined();
|
||
}
|
||
}
|
||
});
|
||
it('достроенная строка: ключ по коду, lineId = null', () => {
|
||
const withTemplate = {
|
||
...statement,
|
||
lines: [
|
||
...statement.lines,
|
||
{
|
||
id: null,
|
||
origin: 'template',
|
||
line_code: '1700',
|
||
line_name: 'БАЛАНС',
|
||
values: [v(null, 'c1', 0, { amount: null, confidence: null, is_recognized: null })],
|
||
},
|
||
],
|
||
} as unknown as StatementOut;
|
||
const row = buildStatementGrid(withTemplate).rows[1];
|
||
expect(row).toMatchObject({ key: 'code-1700', lineId: null, code: '1700', origin: 'template' });
|
||
});
|
||
});
|
||
|
||
describe('cellFlags — внимание только по needs_review', () => {
|
||
it('низкая уверенность и is_recognized=false без needs_review — не внимание', () => {
|
||
expect(cellFlags(v('a', 'c', 0, { confidence: '50', is_recognized: false, needs_review: false })).attention).toBe(false);
|
||
});
|
||
it('needs_review=true — внимание, даже при уверенности 100', () => {
|
||
expect(cellFlags(v('a', 'c', 0, { confidence: '100', needs_review: true })).attention).toBe(true);
|
||
});
|
||
it('is_confirmed — подтверждена, уверенность не меняется', () => {
|
||
expect(cellFlags(v('a', 'c', 0, { is_confirmed: true })).confirmed).toBe(true);
|
||
});
|
||
it('undefined — все флаги false', () => {
|
||
expect(cellFlags(undefined)).toEqual({ attention: false, modified: false, confirmed: false });
|
||
});
|
||
it('пустая достроенная ячейка не требует внимания', () => {
|
||
expect(cellFlags(v(null, 'c', 0, { amount: null, confidence: null, is_recognized: null }))).toEqual({
|
||
attention: false,
|
||
modified: false,
|
||
confirmed: false,
|
||
});
|
||
});
|
||
it('исправлено', () => {
|
||
expect(cellFlags(v('a', 'c', 0, { is_modified: true })).modified).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('draftToPatch', () => {
|
||
const rows = buildStatementGrid({
|
||
...statement,
|
||
lines: [...statement.lines, { id: null, origin: 'template', line_code: '1700', line_name: 'БАЛАНС', values: [] }],
|
||
} as unknown as StatementOut).rows;
|
||
it('строка из БД — line_id, достроенная — line_code', () => {
|
||
expect(draftToPatch({ [cellKey('l1', 'c1')]: '10.5', [cellKey('code-1700', 'c1')]: null }, rows)).toEqual([
|
||
{ line_id: 'l1', column_id: 'c1', amount: '10.5' },
|
||
{ line_code: '1700', column_id: 'c1', amount: null },
|
||
]);
|
||
});
|
||
it('неизвестная строка — ошибка, а не тихий пропуск', () => {
|
||
expect(() => draftToPatch({ [cellKey('ghost', 'c1')]: '1' }, rows)).toThrow();
|
||
});
|
||
});
|
||
|
||
describe('applyDraftInput', () => {
|
||
const parse = (s: string): { ok: true; value: string | null } | { ok: false } => {
|
||
const n = s.replace(/\s/g, '').replace(',', '.');
|
||
if (n === '') {
|
||
return { ok: true, value: null };
|
||
}
|
||
return /^-?\d+(\.\d+)?$/.test(n) ? { ok: true, value: n } : { ok: false };
|
||
};
|
||
it('изменение попадает в черновик', () => {
|
||
expect(applyDraftInput({}, 'k', '5', '1.00', parse)).toEqual({ k: '5' });
|
||
});
|
||
it('возврат к исходному убирает ключ (с учётом нулей дроби)', () => {
|
||
expect(applyDraftInput({ k: '5' }, 'k', '1', '1.00', parse)).toEqual({});
|
||
expect(applyDraftInput({ k: '5' }, 'k', '1,0', '1.00', parse)).toEqual({});
|
||
});
|
||
it('пустое при пустом исходном — не изменение', () => {
|
||
expect(applyDraftInput({ k: '5' }, 'k', '', null, parse)).toEqual({});
|
||
});
|
||
it('неверный формат остаётся в черновике', () => {
|
||
expect(applyDraftInput({}, 'k', '12,3,4', '1.00', parse)).toEqual({ k: '12,3,4' });
|
||
});
|
||
});
|
||
|
||
describe('parent_code и пустые строки детализации', () => {
|
||
it('buildStatementGrid переносит parent_code', () => {
|
||
const withChild = {
|
||
...statement,
|
||
lines: [{ id: 'l2', origin: 'source', line_code: '11101', line_name: 'п', parent_code: '1110', values: [] }, ...statement.lines],
|
||
} as unknown as StatementOut;
|
||
const grid = buildStatementGrid(withChild);
|
||
expect(grid.rows[0].parentCode).toBe('1110');
|
||
expect(grid.rows[1].parentCode).toBeNull(); // поля нет в ответе (старый сервер) — не подстрока
|
||
});
|
||
|
||
const cell = (extra: Partial<ValueOut>) => v('v', 'c1', 0, { amount: null, ...extra });
|
||
const gridRow = (key: string, parentCode: string | null, cells: GridRow['cells']): GridRow => ({
|
||
key,
|
||
lineId: key,
|
||
code: key,
|
||
name: null,
|
||
origin: 'source',
|
||
parentCode,
|
||
cells,
|
||
referenceName: null,
|
||
lineStatus: 'ok',
|
||
rowIndex: null,
|
||
parentRowIndex: null,
|
||
codeText: key,
|
||
codeConfidence: null,
|
||
needsReview: false,
|
||
isConfirmed: false,
|
||
codeModified: false,
|
||
});
|
||
it('пустая подстрока: все ячейки без суммы и без строки; родитель и нулевая сумма — не пустые', () => {
|
||
const rows = [
|
||
gridRow('11101', '1110', { c1: cell({}), c2: cell({}) }),
|
||
gridRow('11102', '1110', {}),
|
||
gridRow('11103', '1110', { c1: cell({ amount: '0.00' }) }),
|
||
gridRow('11104', '1110', { c1: cell({ string_value: 'нет' }) }),
|
||
gridRow('11105', '1110', { c1: cell({ amount: '5.00' }) }),
|
||
gridRow('1110', null, { c1: cell({}) }),
|
||
];
|
||
expect([...emptySublineKeys(rows, {})]).toEqual(['11101', '11102']);
|
||
});
|
||
it('подстрока с правкой в черновике не считается пустой', () => {
|
||
const rows = [gridRow('11101', '1110', { c1: cell({}) }), gridRow('11102', '1110', { c1: cell({}) })];
|
||
expect([...emptySublineKeys(rows, { '11101:c1': '7' })]).toEqual(['11102']);
|
||
});
|
||
});
|