feat: заявление на отпуск уходит на подпись в ЭДО после финального согласования

Когда последний согласующий согласовывает заявку на отпуск, заявление собирается в браузере
как DOCX и отправляется в ЭДО (POST /api/v1/integration/signing-requests). Подписывает сотрудник
из заявки, его же почта передаётся как initiator_email. ИИН берётся из Employee/ByEmail,
БИН компании — из Dictionary/Companies. Неудача отправки не отменяет согласование и
показывается сообщением с кнопкой повтора.

Свойство веб-части «С каким ЭДО работать»: стейдж (https://10.0.1.225:8443) или локальный
бэкенд (https://localhost:7016). jszip объявлен зависимостью явно (раньше был транзитивным).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-10-01 16:46:43 +05:00
parent 4f292dde3d
commit bed190835e
20 changed files with 777 additions and 10 deletions
@@ -0,0 +1,80 @@
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { EDO_API_CLIENT_ID, getEdoApiBaseUrl } from '../config';
import { describeEdoError } from './edoErrors';
import { IFioParts } from './fio';
/** Путь приёма документов на подпись от интеграций (TANSU ЭДО, multipart/form-data). */
const SIGNING_REQUESTS_PATH = '/api/v1/integration/signing-requests';
/** Что отправляем в ЭДО: одного получателя, он же подписант, и один файл. */
export interface IEdoSigningRequest {
/** Подписывающий: ЭДО заводит его по ИИН, если нет, и зовёт подписать письмом. */
recipient: { iin: string; email: string; name: IFioParts };
/** БИН компании, от имени которой документ идёт на подпись (нужен сертификат Sigex этой компании в ЭДО). */
companyBin: string;
/** Кому сообщать о подписи. */
initiatorEmail: string;
file: { name: string; content: Blob };
}
/** Ответ ЭДО: то, что нужно показать пользователю. */
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);
form.append('company_bin', request.companyBin);
form.append('initiator_email', request.initiatorEmail);
form.append('files', request.file.content, request.file.name);
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 Error(describeEdoError(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 || '' }))
};
}