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