import { describe, expect, it } from 'vitest'; import type { StatementOut } from '../../types/api'; import statementV2Json from './__fixtures__/statement-v2.json'; import { type StatementDraft, buildStatementPatch, emptyDraft, isDraftEmpty } from './statementDraft'; import { buildStatementGrid, cellKey } from './statementGrid'; const statement = statementV2Json as StatementOut; const { rows } = buildStatementGrid(statement); const ctx = { rows, header: statement.header }; const draftOf = (part: Partial): StatementDraft => ({ ...emptyDraft(), ...part }); const build = (part: Partial) => buildStatementPatch(draftOf(part), ctx); const bodyOf = (part: Partial) => { const result = build(part); if (!result.ok) throw new Error(`ожидали тело, получили ошибки: ${JSON.stringify(result.errors)}`); return result.body; }; const errorsOf = (part: Partial) => { const result = build(part); if (result.ok) throw new Error('ожидали ошибки'); return result.errors; }; describe('суммы как раньше', () => { it('строка из БД — line_id, шаблонная — line_code', () => { expect(bodyOf({ amounts: { [cellKey('l-1210', 'c1')]: '120' } })?.values).toEqual([ { line_id: 'l-1210', column_id: 'c1', amount: '120' }, ]); expect(bodyOf({ amounts: { [cellKey('code-1250', 'c1')]: '5' } })?.values).toEqual([ { line_code: '1250', column_id: 'c1', amount: '5' }, ]); }); it('пустой текст — очистка суммы (null)', () => { expect(bodyOf({ amounts: { [cellKey('l-1210', 'c1')]: '' } })?.values).toEqual([{ line_id: 'l-1210', column_id: 'c1', amount: null }]); }); it('неверный формат — ошибка amount с ключом ячейки', () => { const key = cellKey('l-1210', 'c1'); expect(errorsOf({ amounts: { [key]: '12,3,4' } })).toEqual([expect.objectContaining({ kind: 'amount', key })]); }); it('неизвестная строка — ошибка, а не тихий пропуск', () => { expect(errorsOf({ amounts: { [cellKey('ghost', 'c1')]: '1' } })[0].kind).toBe('amount'); }); }); describe('подтверждение ячейки', () => { it('confirms → элемент confirmed', () => { expect(bodyOf({ confirms: { [cellKey('l-u7', 'c1')]: true } })?.values).toEqual([ { line_id: 'l-u7', column_id: 'c1', confirmed: true }, ]); }); it('сумма той же ячейки отменяет подтверждение (в элементе ровно одно из amount / confirmed)', () => { const key = cellKey('l-u7', 'c1'); expect(bodyOf({ amounts: { [key]: '7' }, confirms: { [key]: true } })?.values).toEqual([ { line_id: 'l-u7', column_id: 'c1', amount: '7' }, ]); }); }); describe('подтверждение ячейки без id значения', () => { it('пропускается молча (P6-6)', () => { const row = rows.find((r) => r.key === 'l-u7'); if (!row?.cells.c1) throw new Error('нет ячейки в фикстуре'); const noId = { ...row, cells: { ...row.cells, c1: { ...row.cells.c1, id: null } } }; const result = buildStatementPatch(draftOf({ confirms: { [cellKey('l-u7', 'c1')]: true } }), { rows: rows.map((r) => (r === row ? noId : r)), header: statement.header, }); expect(result).toEqual({ ok: true, body: null }); }); }); describe('действия строки', () => { it('код, расшифровка, подтверждение кода', () => { expect(bodyOf({ lines: { 'l-u7': { kind: 'code', code: '1250' } } })?.lines).toEqual([{ line_id: 'l-u7', line_code: '1250' }]); expect(bodyOf({ lines: { 'l-m8': { kind: 'detail' } } })?.lines).toEqual([{ line_id: 'l-m8', line_status: 'detail' }]); expect(bodyOf({ lines: { 'l-1230': { kind: 'confirm', confirmed: true } } })?.lines).toEqual([{ line_id: 'l-1230', confirmed: true }]); }); it('пустой код и код, равный текущему, — не действие', () => { expect(bodyOf({ lines: { 'l-u7': { kind: 'code', code: '' } } })).toBeNull(); expect(bodyOf({ lines: { 'l-1230': { kind: 'code', code: '1230' } } })).toBeNull(); }); it('код — 4 или 5 цифр', () => { for (const code of ['12a4', '123']) { expect(errorsOf({ lines: { 'l-u7': { kind: 'code', code } } })).toEqual([ { kind: 'code', key: 'l-u7', message: 'Код — 4 или 5 цифр' }, ]); } expect(bodyOf({ lines: { 'l-u7': { kind: 'code', code: '11501' } } })?.lines).toEqual([{ line_id: 'l-u7', line_code: '11501' }]); }); it('действие строки и сумма той же строки — оба в теле', () => { const body = bodyOf({ lines: { 'l-m8': { kind: 'detail' } }, amounts: { [cellKey('l-m8', 'c1')]: '8' } }); expect(body?.lines).toEqual([{ line_id: 'l-m8', line_status: 'detail' }]); expect(body?.values).toEqual([{ line_id: 'l-m8', column_id: 'c1', amount: '8' }]); }); it('сумма в шаблонную строку 1250 и назначение 1250 другой строке — конфликт', () => { expect(errorsOf({ amounts: { [cellKey('code-1250', 'c1')]: '5' }, lines: { 'l-u7': { kind: 'code', code: '1250' } } })).toEqual([ { kind: 'conflict', key: 'l-u7', message: 'Код 1250 назначается строке, а в шаблонную строку 1250 введены суммы — сохраните по очереди', }, ]); }); }); describe('колонки', () => { it('дата колонки ГГГГ-ММ-ДД', () => { expect(bodyOf({ columns: { c2: '2024-12-31' } })?.columns).toEqual([{ column_id: 'c2', period_date: '2024-12-31' }]); }); it('другой формат и несуществующая дата — ошибка date; пусто — не отправляется', () => { for (const text of ['31.12.2024', '2024-02-30']) { expect(errorsOf({ columns: { c2: text } })).toEqual([{ kind: 'date', key: 'c2', message: 'Дата в формате ГГГГ-ММ-ДД' }]); } expect(bodyOf({ columns: { c2: '' } })).toBeNull(); }); }); describe('шапка', () => { it('organization_name обрезается', () => { expect(bodyOf({ header: { organization_name: ' ООО Ромашка-2 ' } })?.header).toEqual({ organization_name: 'ООО Ромашка-2' }); }); it('значение, равное текущему (после обрезки), не отправляется', () => { expect(bodyOf({ header: { organization_name: ' ООО Ромашка ' } })).toBeNull(); expect(bodyOf({ header: { inn: '7700000000', units: ' тыс. руб. ' } })).toBeNull(); }); it('введено и стёрто — тела нет', () => { expect(bodyOf({ header: { inn: ' ', organization_name: '' } })).toBeNull(); }); it('дата из needs_review_fields с тем же значением — подтверждение, уходит', () => { expect(bodyOf({ header: { report_date: '2025-09-30' } })?.header).toEqual({ report_date: '2025-09-30' }); const noReview = { rows, header: { ...statement.header, needs_review_fields: [] } }; const res = buildStatementPatch(draftOf({ header: { report_date: '2025-09-30' } }), noReview); expect(res).toEqual({ ok: true, body: null }); }); it('длиннее 1000 символов — ошибка header', () => { expect(errorsOf({ header: { organization_name: 'я'.repeat(1001) } })[0]).toMatchObject({ kind: 'header', key: 'organization_name' }); expect(bodyOf({ header: { organization_name: 'я'.repeat(1000) } })).not.toBeNull(); }); it('подтверждение даты отчёта отправляет текущее значение', () => { expect(bodyOf({ header: { confirm_report_date: true } })?.header).toEqual({ report_date: '2025-09-30' }); expect(bodyOf({ header: { confirm_report_date: true, report_date: '2025-06-30' } })?.header).toEqual({ report_date: '2025-06-30' }); }); it('пустое поле не отправляется', () => { expect(bodyOf({ header: { inn: '' } })).toBeNull(); expect(bodyOf({ header: { inn: ' 7700000001 ', units: '', report_date: '2025-09-30' } })?.header).toEqual({ inn: '7700000001', report_date: '2025-09-30', }); }); }); describe('пустой черновик', () => { it('isDraftEmpty и body null', () => { expect(isDraftEmpty(emptyDraft())).toBe(true); expect(buildStatementPatch(emptyDraft(), ctx)).toEqual({ ok: true, body: null }); }); it('любая часть делает черновик непустым', () => { expect(isDraftEmpty(draftOf({ header: { inn: '1' } }))).toBe(false); expect(isDraftEmpty(draftOf({ lines: { 'l-m8': { kind: 'detail' } } }))).toBe(false); }); });