From 79278589d62f54aa759c5bf6932bf7ed9de3b4ad Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 10 Oct 2026 00:53:59 +0400 Subject: [PATCH] =?UTF-8?q?api:=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B8=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=BF=D0=BE=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B0=D0=BC=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=81=D0=BE=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- src/services/api/files.test.ts | 41 +++++++++++++++++++++++++--------- src/services/api/files.ts | 18 ++++++++++----- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/services/api/files.test.ts b/src/services/api/files.test.ts index ef60fa3..07fce57 100644 --- a/src/services/api/files.test.ts +++ b/src/services/api/files.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { ApiError } from '../../api/client'; import type { FileOut, UploadResult } from '../../types/api'; -import { hasPendingFiles, isUploadResult, summarizeUpload, uploadFiles } from './files'; +import { filesPath, hasPendingFiles, isUploadResult, listLegalEntityFiles, summarizeUpload, uploadFiles } from './files'; const result: UploadResult = { accepted: [{ file_id: 'f1', job_id: 'j1', name: 'osv.xlsx' }], @@ -49,28 +49,37 @@ describe('hasPendingFiles', () => { }); }); +describe('filesPath', () => { + it('с консолидацией — маршрут консолидации, без неё — маршрут ЮЛ', () => { + expect(filesPath('le', 'c1')).toBe('/consolidations/c1/legal-entities/le/files'); + expect(filesPath('le', null)).toBe('/legal-entities/le/files'); + }); +}); + describe('uploadFiles', () => { afterEach(() => vi.restoreAllMocks()); - it('422 с UploadResult возвращается как результат', async () => { + it('422 с UploadResult возвращается как результат (и с консолидацией)', async () => { const client = await import('../../api/client'); vi.spyOn(client, 'apiFetch').mockRejectedValue(new ApiError(422, undefined, 'x', { accepted: [], rejected: result.rejected })); - await expect(uploadFiles('le', [new File(['a'], 'dup.xlsx')], null)).resolves.toEqual({ accepted: [], rejected: result.rejected }); + await expect(uploadFiles('le', [new File(['a'], 'dup.xlsx')], 'c1')).resolves.toEqual({ accepted: [], rejected: result.rejected }); }); - it('422 с detail пробрасывается', async () => { + it('422 с detail и 409 пробрасываются', async () => { const client = await import('../../api/client'); - const error = new ApiError(422, 'ENTITY_NOT_IN_PERIMETER', 'ЮЛ вне периметра', { detail: { code: 'ENTITY_NOT_IN_PERIMETER' } }); + const error = new ApiError(409, 'CONSOLIDATION_NOT_EDITABLE', 'нет версии в работе', { + detail: { code: 'CONSOLIDATION_NOT_EDITABLE' }, + }); vi.spyOn(client, 'apiFetch').mockRejectedValue(error); - await expect(uploadFiles('le', [new File(['a'], 'a.xlsx')], 'v1')).rejects.toBe(error); + await expect(uploadFiles('le', [new File(['a'], 'a.xlsx')], 'c1')).rejects.toBe(error); }); - it('версия попадает в query, лишних полей формы нет', async () => { + it('консолидация — в пути, query нет, лишних полей формы нет', async () => { const client = await import('../../api/client'); const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result); - await uploadFiles('le', [new File(['a'], 'a.xlsx'), new File(['b'], 'b.pdf')], 'v1'); + await uploadFiles('le', [new File(['a'], 'a.xlsx'), new File(['b'], 'b.pdf')], 'c1'); const [path, init] = spy.mock.calls[0]; - expect(path).toBe('/legal-entities/le/files?consolidation_version_id=v1'); + expect(path).toBe('/consolidations/c1/legal-entities/le/files'); const form = init?.body as FormData; expect(form.getAll('files')).toHaveLength(2); const keys: string[] = []; @@ -78,10 +87,22 @@ describe('uploadFiles', () => { expect(keys.every((k) => k === 'files')).toBe(true); }); - it('без версии — без query', async () => { + it('без консолидации — маршрут ЮЛ без query', async () => { const client = await import('../../api/client'); const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result); await uploadFiles('le', [new File(['a'], 'a.xlsx')], null); expect(spy.mock.calls[0][0]).toBe('/legal-entities/le/files'); }); }); + +describe('listLegalEntityFiles', () => { + afterEach(() => vi.restoreAllMocks()); + it('стол консолидации и список ЮЛ', async () => { + const client = await import('../../api/client'); + const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 }); + await listLegalEntityFiles('le', 'c1'); + await listLegalEntityFiles('le', null); + expect(spy.mock.calls[0][0]).toBe('/consolidations/c1/legal-entities/le/files?limit=100'); + expect(spy.mock.calls[1][0]).toBe('/legal-entities/le/files?limit=100'); + }); +}); diff --git a/src/services/api/files.ts b/src/services/api/files.ts index 9ff317e..921f864 100644 --- a/src/services/api/files.ts +++ b/src/services/api/files.ts @@ -10,15 +10,21 @@ export function isUploadResult(body: unknown): body is UploadResult { ); } -/** Загрузка файлов ЮЛ одним запросом; 422 «ни один не принят» возвращается как результат, прочие ошибки — throw. */ -export async function uploadFiles(legalEntityId: string, files: File[], versionId: string | null): Promise { +/** Маршрут файлов ЮЛ: с консолидацией — файлы в контексте консолидации, без неё — файлы ЮЛ (design D-2). */ +export function filesPath(legalEntityId: string, consolidationId: string | null): string { + return consolidationId + ? `/consolidations/${encodeURIComponent(consolidationId)}/legal-entities/${legalEntityId}/files` + : `/legal-entities/${legalEntityId}/files`; +} + +/** Загрузка файлов ЮЛ одним запросом (в консолидацию или без контекста); 422 «ни один не принят» возвращается как результат, прочие ошибки — throw. */ +export async function uploadFiles(legalEntityId: string, files: File[], consolidationId: string | null): Promise { const form = new FormData(); for (const file of files) { form.append('files', file); } - const query = versionId ? `?consolidation_version_id=${encodeURIComponent(versionId)}` : ''; try { - return await apiFetch(`/legal-entities/${legalEntityId}/files${query}`, { method: 'POST', body: form }); + return await apiFetch(filesPath(legalEntityId, consolidationId), { method: 'POST', body: form }); } catch (e: unknown) { if (e instanceof ApiError && e.status === 422 && isUploadResult(e.body)) { return e.body; @@ -34,8 +40,8 @@ export function summarizeUpload(result: UploadResult): { accepted: string[]; rej }; } -export async function listLegalEntityFiles(legalEntityId: string): Promise { - const page = await apiFetch>(`/legal-entities/${legalEntityId}/files?limit=100`); +export async function listLegalEntityFiles(legalEntityId: string, consolidationId: string | null): Promise { + const page = await apiFetch>(`${filesPath(legalEntityId, consolidationId)}?limit=100`); return page.items; }