fix(ct): поток уведомлений получает строку того, кто вернул на доработку или отклонил

Без активного согласующего форма отправляла потоку последнюю строку маршрута — при возврате на
доработку это мог быть следующий согласующий в статусе «Ждёт очереди», и в письме инициатору
нельзя было указать, кто вернул и с каким комментарием. Теперь (pickRowToNotify): активный
согласующий → его строка; иначе строка «На доработке» или «Отклонено»; иначе последняя.
This commit is contained in:
Тимур Городецкий
2026-10-02 11:13:07 +05:00
parent 90759fc4bc
commit 34da9ae165
3 changed files with 44 additions and 9 deletions
@@ -1,4 +1,4 @@
import { findReworkRow, isUserInApprovalChain } from './approvalAccess'; import { findReworkRow, isUserInApprovalChain, pickRowToNotify } from './approvalAccess';
import { IMatrixRow } from '../../../../shared/approval'; import { IMatrixRow } from '../../../../shared/approval';
function makeRow(overrides: Partial<IMatrixRow>): IMatrixRow { function makeRow(overrides: Partial<IMatrixRow>): IMatrixRow {
@@ -48,3 +48,29 @@ describe('findReworkRow (кто вернул СТ на доработку и ч
expect(findReworkRow([makeRow({ status: 'Согласовано' })])).toBeUndefined(); expect(findReworkRow([makeRow({ status: 'Согласовано' })])).toBeUndefined();
}); });
}); });
describe('pickRowToNotify (какую строку маршрута передать потоку уведомлений)', () => {
const row = (id: number, status: string): { id: number; status: string } => ({ id, status });
it('есть активный согласующий — его строку («поступила на согласование»)', () => {
expect(pickRowToNotify([row(1, 'Согласовано'), row(2, 'На согласовании'), row(3, 'Ждёт очереди')])?.id).toBe(2);
});
// Раньше при возврате уходила последняя строка (напр. «Ждёт очереди»), и поток не знал, кто вернул и с каким
// комментарием.
it('возврат на доработку — строку того, кто вернул', () => {
expect(pickRowToNotify([row(1, 'Согласовано'), row(2, 'На доработке'), row(3, 'Ждёт очереди')])?.id).toBe(2);
});
it('отклонение — строку того, кто отклонил', () => {
expect(pickRowToNotify([row(1, 'Отклонено'), row(2, 'Ждёт очереди')])?.id).toBe(1);
});
it('все согласовали — последнюю строку', () => {
expect(pickRowToNotify([row(1, 'Согласовано'), row(2, 'Согласовано')])?.id).toBe(2);
});
it('пустой маршрут — нечего отправлять', () => {
expect(pickRowToNotify([])).toBeUndefined();
});
});
@@ -22,3 +22,13 @@ export function isUserInApprovalChain(matrix: IMatrixRow[], user: CurrentUser):
export function findReworkRow(matrix: IMatrixRow[]): IMatrixRow | undefined { export function findReworkRow(matrix: IMatrixRow[]): IMatrixRow | undefined {
return matrix.find(row => row.status === REQUEST_STATUS.rework); return matrix.find(row => row.status === REQUEST_STATUS.rework);
} }
// Строка маршрута для потока уведомлений (он по ней выходит на СТ, её автора и комментарий):
// активный согласующий — «поступила на согласование»; иначе строка того, кто вернул на доработку или
// отклонил, — чтобы в письме инициатору были его имя и комментарий; иначе (все согласовали) — последняя.
export function pickRowToNotify<T extends { status: string }>(rows: T[]): T | undefined {
return rows.find(r => r.status === REQUEST_STATUS.onApproval)
?? rows.find(r => r.status === REQUEST_STATUS.rework)
?? rows.find(r => r.status === REQUEST_STATUS.rejected)
?? rows[rows.length - 1];
}
@@ -31,6 +31,7 @@ import {
parseOfferItemIndex, parseOfferItemIndex,
} from "../../../shared/offers/offerRecords"; } from "../../../shared/offers/offerRecords";
import { CriteriaRecord, plainTextFromSp } from "../components/utils/criteria"; import { CriteriaRecord, plainTextFromSp } from "../components/utils/criteria";
import { pickRowToNotify } from "../components/utils/approvalAccess";
import { import {
NoOfferSupplierEntry, NoOfferSupplierEntry,
NoOfferSupplierRecord, NoOfferSupplierRecord,
@@ -746,14 +747,12 @@ export class CtService {
async notifyActiveApprover(ctId: string): Promise<void> { async notifyActiveApprover(ctId: string): Promise<void> {
const rows = await this.approval.getForRequest(parseInt(ctId, 10)); const rows = await this.approval.getForRequest(parseInt(ctId, 10));
if (rows.length === 0) return; if (rows.length === 0) return;
// Есть активный согласующий → уведомляем его («поступила на согласование»). // Есть активный согласующий → уведомляем его («поступила на согласование»). Иначе — строка того,
// Иначе финал (все согласовали / кто-то отклонил): активной строки уже нет, но поток // кто вернул на доработку или отклонил (поток берёт из неё имя и комментарий для письма инициатору),
// сам выбирает получателя (инициатора) по статусу СТ — ему нужен лишь любой RowID этой СТ, // а если все согласовали — последняя: поток по ней выходит на СТ и её автора. Раньше при возврате
// чтобы выйти на саму СТ и её автора. Берём последнюю строку маршрута. Без этого // уходила последняя строка (напр. «Ждёт очереди»), и кто вернул и почему, поток не знал.
// «Согласовано»/«Отклонено» инициатору не уходили (поток вообще не вызывался). const rowToNotify = pickRowToNotify(rows);
const active = rows.filter((r) => r.status === REQUEST_STATUS.onApproval)[0]; if (rowToNotify) await this.triggerApprovalNotification(rowToNotify.id);
const rowToNotify = active ?? rows[rows.length - 1];
await this.triggerApprovalNotification(rowToNotify.id);
} }
async triggerApprovalNotification(spItemId: number): Promise<void> { async triggerApprovalNotification(spItemId: number): Promise<void> {