Files
request-portal/src/webparts/requestPortal/shared/edo/edo.service.ts
T
gorbenko.pavel 637e51d7f9
Tests / frontend-test (push) Successful in 1m24s
Feature/vacation edo signing (#23)
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/23
2026-10-05 08:53:18 +00:00

102 lines
5.2 KiB
TypeScript

import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { EDO_API_CLIENT_ID, getEdoApiBaseUrl } from '../config';
import { EdoRequestError } from './edoErrors';
import { IFioParts } from './fio';
/** Путь приёма документов на подпись от интеграций (TANSU ЭДО, multipart/form-data). */
const SIGNING_REQUESTS_PATH = '/api/v1/integration/signing-requests';
/** Файл на подпись и компания, от имени которой он идёт (по её сертификату Sigex в ЭДО). */
/** Как портал называет себя в ЭДО. */
export const EDO_SOURCE_NAME = 'Портал заявок';
export interface IEdoSigningFile {
name: string;
content: Blob;
/** БИН компании файла. У сотрудника-совместителя файлы одного запроса идут от разных компаний. */
companyBin: string;
}
/** Что отправляем в ЭДО: одного получателя, он же подписант (физлицо), и файлы. */
export interface IEdoSigningRequest {
/** Подписывающий: ЭДО заводит его по ИИН, если нет, и зовёт подписать письмом. */
recipient: { iin: string; email: string; name: IFioParts };
/** Кому сообщать о подписи; пусто — никому. */
initiatorEmail?: string;
files: IEdoSigningFile[];
}
/** Ответ ЭДО: то, что нужно показать пользователю. */
export interface IEdoSigningResult {
/** Кабинет получателя без ключа: войти по ЭЦП или через Microsoft. */
cabinetUrl: string;
documents: Array<{
id: string;
fileName: string;
}>;
}
/** Ответ ЭДО в snake_case — только поля, которые читаем. */
interface IEdoResponseBody {
cabinet_url?: string;
documents?: Array<{ id?: string; file_name?: string }>;
}
/**
* Отправляет документы на подпись в ЭДО одним запросом: либо принимаются все, либо ни один.
*
* Токен берётся для приложения ЭДО текущего пользователя (AadHttpClient), поэтому в ЭДО
* запрос приходит от того, кто нажал «Согласовать», — он заводится там автоматически при
* первом обращении. Адрес ЭДО — свойство веб-части edoApiBaseUrl.
*/
export async function submitSigningRequest(
context: WebPartContext,
request: IEdoSigningRequest
): Promise<IEdoSigningResult> {
const baseUrl = getEdoApiBaseUrl();
const client = await context.aadHttpClientFactory.getClient(EDO_API_CLIENT_ID);
// Content-Type не задаём: браузер сам допишет boundary для multipart.
const form = new FormData();
form.append('iin', request.recipient.iin);
form.append('email', request.recipient.email);
form.append('last_name', request.recipient.name.lastName);
form.append('first_name', request.recipient.name.firstName);
if (request.recipient.name.middleName) form.append('middle_name', request.recipient.name.middleName);
if (request.initiatorEmail) form.append('initiator_email', request.initiatorEmail);
// Так ЭДО подпишет документ в колонке «Кто отправил», а не техническим client_id.
form.append('service_initiator_name', EDO_SOURCE_NAME);
// БИН файлов — одним полем JSON-массивом строк, по порядку файлов: общий company_bin не нужен,
// подписант один (физлицо). Повторяющиеся поля ЭДО не принимает (400 invalid_file_company_bins_format).
for (const file of request.files) {
form.append('files', file.content, file.name);
}
form.append('file_company_bins', JSON.stringify(request.files.map((file) => file.companyBin)));
let response: HttpClientResponse;
try {
response = await client.post(`${baseUrl}${SIGNING_REQUESTS_PATH}`, AadHttpClient.configurations.v1, { body: form });
} catch (e) {
// Сеть/сертификат: у браузера для таких случаев одно сообщение «Failed to fetch»,
// а причина (недоверенный сертификат, ЭДО выключен, адрес не тот) видна только в консоли.
console.error('ЭДО недоступен', e);
throw new Error(
`ЭДО (${baseUrl}) недоступен. Проверьте, что сервер запущен и браузер доверяет его сертификату`
);
}
if (!response.ok) {
throw new EdoRequestError(response.status, await response.text());
}
const body = (await response.json()) as IEdoResponseBody;
return {
cabinetUrl: body.cabinet_url || '',
documents: (body.documents || []).map((d) => ({
id: d.id || '',
fileName: d.file_name || ''
}))
};
}