Панель «Первичные документы»: рабочий стол консолидации и загрузка в консолидацию (#26) #27

Merged
aep merged 7 commits from feat/26-console-files into test 2026-10-10 09:00:48 +03:00
2 changed files with 43 additions and 16 deletions
Showing only changes of commit 79278589d6 - Show all commits

View File

@ -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');
});
});

View File

@ -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<UploadResult> {
/** Маршрут файлов ЮЛ: с консолидацией — файлы в контексте консолидации, без неё — файлы ЮЛ (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<UploadResult> {
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<UploadResult>(`/legal-entities/${legalEntityId}/files${query}`, { method: 'POST', body: form });
return await apiFetch<UploadResult>(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<FileOut[]> {
const page = await apiFetch<Page<FileOut>>(`/legal-entities/${legalEntityId}/files?limit=100`);
export async function listLegalEntityFiles(legalEntityId: string, consolidationId: string | null): Promise<FileOut[]> {
const page = await apiFetch<Page<FileOut>>(`${filesPath(legalEntityId, consolidationId)}?limit=100`);
return page.items;
}