api: загрузка и список файлов по маршрутам консолидации (#26)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
5b7b02a3f2
commit
79278589d6
@ -1,7 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { ApiError } from '../../api/client';
|
import { ApiError } from '../../api/client';
|
||||||
import type { FileOut, UploadResult } from '../../types/api';
|
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 = {
|
const result: UploadResult = {
|
||||||
accepted: [{ file_id: 'f1', job_id: 'j1', name: 'osv.xlsx' }],
|
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', () => {
|
describe('uploadFiles', () => {
|
||||||
afterEach(() => vi.restoreAllMocks());
|
afterEach(() => vi.restoreAllMocks());
|
||||||
|
|
||||||
it('422 с UploadResult возвращается как результат', async () => {
|
it('422 с UploadResult возвращается как результат (и с консолидацией)', async () => {
|
||||||
const client = await import('../../api/client');
|
const client = await import('../../api/client');
|
||||||
vi.spyOn(client, 'apiFetch').mockRejectedValue(new ApiError(422, undefined, 'x', { accepted: [], rejected: result.rejected }));
|
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 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);
|
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 client = await import('../../api/client');
|
||||||
const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result);
|
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];
|
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;
|
const form = init?.body as FormData;
|
||||||
expect(form.getAll('files')).toHaveLength(2);
|
expect(form.getAll('files')).toHaveLength(2);
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
@ -78,10 +87,22 @@ describe('uploadFiles', () => {
|
|||||||
expect(keys.every((k) => k === 'files')).toBe(true);
|
expect(keys.every((k) => k === 'files')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('без версии — без query', async () => {
|
it('без консолидации — маршрут ЮЛ без query', async () => {
|
||||||
const client = await import('../../api/client');
|
const client = await import('../../api/client');
|
||||||
const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result);
|
const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result);
|
||||||
await uploadFiles('le', [new File(['a'], 'a.xlsx')], null);
|
await uploadFiles('le', [new File(['a'], 'a.xlsx')], null);
|
||||||
expect(spy.mock.calls[0][0]).toBe('/legal-entities/le/files');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -10,15 +10,21 @@ export function isUploadResult(body: unknown): body is UploadResult {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Загрузка файлов ЮЛ одним запросом; 422 «ни один не принят» возвращается как результат, прочие ошибки — throw. */
|
/** Маршрут файлов ЮЛ: с консолидацией — файлы в контексте консолидации, без неё — файлы ЮЛ (design D-2). */
|
||||||
export async function uploadFiles(legalEntityId: string, files: File[], versionId: string | null): Promise<UploadResult> {
|
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();
|
const form = new FormData();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
form.append('files', file);
|
form.append('files', file);
|
||||||
}
|
}
|
||||||
const query = versionId ? `?consolidation_version_id=${encodeURIComponent(versionId)}` : '';
|
|
||||||
try {
|
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) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof ApiError && e.status === 422 && isUploadResult(e.body)) {
|
if (e instanceof ApiError && e.status === 422 && isUploadResult(e.body)) {
|
||||||
return 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[]> {
|
export async function listLegalEntityFiles(legalEntityId: string, consolidationId: string | null): Promise<FileOut[]> {
|
||||||
const page = await apiFetch<Page<FileOut>>(`/legal-entities/${legalEntityId}/files?limit=100`);
|
const page = await apiFetch<Page<FileOut>>(`${filesPath(legalEntityId, consolidationId)}?limit=100`);
|
||||||
return page.items;
|
return page.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user