Отпуск: документы компаний без сертификата Sigex сохраняются в ЭДО и ждут его загрузки, сообщение об этом в «Решение сохранено»

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-10-02 17:00:43 +05:00
parent 5fd8bfe453
commit 911ccfbcf4
6 changed files with 80 additions and 7 deletions
@@ -45,7 +45,9 @@
массиве `file_company_bins`, общий `company_bin` не передаётся. Подписант один — сам сотрудник, от
имени физического лица; БИН нужны ЭДО только чтобы выбрать сертификат Sigex (mTLS) нужной компании.
Запрос один на все файлы: ЭДО принимает его целиком или отклоняет целиком (например, если у какой-то
компании нет сертификата), поэтому повтор безопасен.
компании не распознан), поэтому повтор безопасен. Если у компании нет сертификата Sigex, ЭДО всё равно
сохраняет её документ (`awaiting_certificate: true`), но подписать его нельзя, пока администратор не
загрузит сертификат — тогда подписание откроется само. В зелёном сообщении такие компании названы отдельно.
- **ИИН** берётся из `GET /Employee/ByEmail` (поле `Iin`), **БИН компаний** — из `GET /Dictionary/Companies`
по `CompanyId` каждой записи сотрудника (`shared/edo/signingTarget.ts`). Компания без БИН в справочнике
пропускается и показывается в сообщении; остальные уходят.
@@ -28,6 +28,7 @@ import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
import { loadVacationRequest } from './load';
import { vacationApprovalService } from './submit';
import { IVacationSigningResult, sendVacationForSigning } from './sendToSign';
import { describeSigning } from './signingMessage';
import { RequestSection } from './sections/RequestSection';
import { useStableCallback } from '../../shared/useStableCallback';
@@ -154,7 +155,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
return sendVacationForSigning(context, { data, employeeName, itemId, code })
.then((result): string | undefined => {
setSigningResult(result);
return `Документы отправлены на подписание в ЭДО (${result.sent.join(', ')}): ссылка для подписания ушла на ${data.Employee}.`;
return describeSigning(result, data.Employee);
})
.catch((e): string | undefined => {
setSigningError(
@@ -21,6 +21,11 @@ export interface IVacationToSign {
export interface IVacationSigningResult {
/** Названия компаний, по которым заявление отправлено на подпись. */
sent: string[];
/**
* Компании, у которых в ЭДО нет сертификата Sigex: заявление там сохранено, но подписать его нельзя,
* пока администратор не загрузит сертификат. Подписание откроется само — это подмножество `sent`.
*/
awaitingCertificate: string[];
/** Компании сотрудника, по которым документ не собрать (нет БИН в справочнике), и почему. */
skipped: ISkippedCompany[];
}
@@ -100,11 +105,17 @@ export async function sendVacationForSigning(
}))
);
await submitSigningRequest(context, {
const accepted = await submitSigningRequest(context, {
recipient: { iin: targets.iin, email: data.Employee, name: targets.name },
initiatorEmail: data.Employee,
files
});
return { sent: targets.companies.map((c) => c.name), skipped: targets.skipped };
const waitingBins = accepted.documents.filter((d) => d.awaitingCertificate).map((d) => d.companyBin);
return {
sent: targets.companies.map((c) => c.name),
awaitingCertificate: targets.companies.filter((c) => waitingBins.indexOf(c.bin) >= 0).map((c) => c.name),
skipped: targets.skipped
};
}
@@ -0,0 +1,23 @@
import type { IVacationSigningResult } from './sendToSign';
/**
* Фраза для зелёного сообщения «Решение сохранено…»: что ушло на подпись, а что сохранено и ждёт
* сертификата Sigex своей компании (подписание откроется после его загрузки).
*/
export function describeSigning(result: IVacationSigningResult, employeeEmail: string): string {
const ready = result.sent.filter((name) => result.awaitingCertificate.indexOf(name) < 0);
const parts: string[] = [];
if (ready.length > 0) {
parts.push(`Документы отправлены на подписание в ЭДО (${ready.join(', ')}): ссылка для подписания ушла на ${employeeEmail}.`);
}
if (result.awaitingCertificate.length > 0) {
parts.push(
`Документы сохранены в ЭДО, но ждут загрузки сертификата Sigex по компаниям: ${result.awaitingCertificate.join(', ')}. ` +
`Подписание откроется само, когда администратор загрузит сертификат; ссылка придёт на ${employeeEmail}.`
);
}
return parts.join(' ');
}
@@ -28,13 +28,20 @@ export interface IEdoSigningRequest {
export interface IEdoSigningResult {
/** Кабинет получателя без ключа: войти по ЭЦП или через Microsoft. */
cabinetUrl: string;
documents: Array<{ id: string; fileName: string }>;
documents: Array<{
id: string;
fileName: string;
/** БИН компании файла. */
companyBin: string;
/** У компании нет сертификата Sigex: файл сохранён, но подписать его нельзя до загрузки сертификата. */
awaitingCertificate: boolean;
}>;
}
/** Ответ ЭДО в snake_case — только поля, которые читаем. */
interface IEdoResponseBody {
cabinet_url?: string;
documents?: Array<{ id?: string; file_name?: string }>;
documents?: Array<{ id?: string; file_name?: string; company_bin?: string; awaiting_certificate?: boolean }>;
}
/**
@@ -84,6 +91,11 @@ export async function submitSigningRequest(
const body = (await response.json()) as IEdoResponseBody;
return {
cabinetUrl: body.cabinet_url || '',
documents: (body.documents || []).map((d) => ({ id: d.id || '', fileName: d.file_name || '' }))
documents: (body.documents || []).map((d) => ({
id: d.id || '',
fileName: d.file_name || '',
companyBin: d.company_bin || '',
awaitingCertificate: d.awaiting_certificate === true
}))
};
}
@@ -3,6 +3,7 @@ import { IEmployeeOrganization } from '../dictionary/models/IEmployeeOrganizatio
import { describeEdoError } from './edoErrors';
import { splitFio } from './fio';
import { resolveSigningTargets } from './signingTarget';
import { describeSigning } from '../../forms/Vacation/signingMessage';
const TANSU_ID = '9de064a4-44c8-88ef-11ec-8284930fad96';
@@ -150,3 +151,26 @@ describe('текст ошибки ЭДО', () => {
expect(describeEdoError(403, '')).toContain('403');
});
});
describe('сообщение об отправке в ЭДО', () => {
it('всё ушло на подпись', () => {
const text = describeSigning({ sent: ['ТОО А', 'ТОО Б'], awaitingCertificate: [], skipped: [] }, 'a@b.kz');
expect(text).toContain('отправлены на подписание в ЭДО (ТОО А, ТОО Б)');
expect(text).not.toContain('сертификат');
});
it('у части компаний нет сертификата — документы сохранены и ждут', () => {
const text = describeSigning({ sent: ['ТОО А', 'ТОО Б'], awaitingCertificate: ['ТОО Б'], skipped: [] }, 'a@b.kz');
expect(text).toContain('отправлены на подписание в ЭДО (ТОО А)');
expect(text).toContain('ждут загрузки сертификата Sigex по компаниям: ТОО Б');
});
it('сертификата нет ни у кого', () => {
const text = describeSigning({ sent: ['ТОО А'], awaitingCertificate: ['ТОО А'], skipped: [] }, 'a@b.kz');
expect(text).not.toContain('отправлены на подписание');
expect(text).toContain('ждут загрузки сертификата Sigex');
});
});