fix: делегат на параллельном шаге получает заявку сразу
Очередь не двигалась, пока на шаге был кто-то активный, поэтому делегат участника параллельного шага ждал решения второго участника. Текущий шаг теперь — наименьший order среди нерешённых строк. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import { APPROVAL_PHASE, REQUEST_STATUS } from './models/statuses';
|
||||
import { userSearchVariants } from './userSearchVariants';
|
||||
import { planRoutePatch } from './routePatchPlan';
|
||||
import { IStepOrderChange } from './queuedStepMove';
|
||||
import { pickRowsToActivate } from './queueActivation';
|
||||
|
||||
// Внутренние имена колонок списка согласования (создаются провижинингом).
|
||||
const FIELDS = {
|
||||
@@ -240,9 +241,9 @@ export class ApprovalListService {
|
||||
await execute();
|
||||
}
|
||||
|
||||
// Продвигает очередь: среди ещё не активных строк со статусом «Ждёт очереди»
|
||||
// в текущей фазе активирует те, что делят наименьший StepOrder (обычно одна
|
||||
// строка; несколько — параллельный шаг). Фаза исполнения активируется, только
|
||||
// Продвигает очередь: в текущей фазе активирует ждущие строки текущего шага —
|
||||
// наименьшего StepOrder среди нерешённых (обычно одна строка; несколько —
|
||||
// параллельный шаг; правило см. pickRowsToActivate). Фаза исполнения активируется, только
|
||||
// когда фаза согласования пройдена целиком (без Отклонено/Возвращена) — иначе
|
||||
// строки исполнения остаются «Ждёт очереди». Если фаза согласования уже
|
||||
// остановлена решением, ничего не активирует.
|
||||
@@ -260,13 +261,7 @@ export class ApprovalListService {
|
||||
approvalRows.length === 0 || approvalRows.every((r) => r.status === REQUEST_STATUS.approved);
|
||||
|
||||
const activate = async (phaseRows: IApprovalRow[], activeStatus: string): Promise<void> => {
|
||||
if (phaseRows.some((r) => r.status === activeStatus)) return;
|
||||
const queuedRows = phaseRows.filter((r) => r.status === REQUEST_STATUS.queued);
|
||||
if (queuedRows.length === 0) return;
|
||||
const minOrder = Math.min(...queuedRows.map((r) => r.order));
|
||||
await Promise.all(
|
||||
queuedRows.filter((r) => r.order === minOrder).map((r) => this.setActive(r.id, activeStatus))
|
||||
);
|
||||
await Promise.all(pickRowsToActivate(phaseRows).map((id) => this.setActive(id, activeStatus)));
|
||||
};
|
||||
|
||||
if (!approvalDone) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { IQueueRow, pickRowsToActivate } from './queueActivation';
|
||||
import { REQUEST_STATUS } from './models/statuses';
|
||||
|
||||
function row(id: number, order: number, status: string = REQUEST_STATUS.queued): IQueueRow {
|
||||
return { id, order, status };
|
||||
}
|
||||
|
||||
describe('pickRowsToActivate', () => {
|
||||
it('отправка: активируется первый шаг', () => {
|
||||
expect(pickRowsToActivate([row(1, 1), row(2, 2)])).toEqual([1]);
|
||||
});
|
||||
|
||||
it('отправка: параллельный первый шаг активируется целиком', () => {
|
||||
expect(pickRowsToActivate([row(1, 1), row(2, 1), row(3, 2)])).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('последовательный маршрут: пока шаг активен, очередь не двигается', () => {
|
||||
expect(pickRowsToActivate([row(1, 1, REQUEST_STATUS.onApproval), row(2, 2)])).toEqual([]);
|
||||
});
|
||||
|
||||
it('шаг согласован — активируется следующий', () => {
|
||||
expect(pickRowsToActivate([row(1, 1, REQUEST_STATUS.approved), row(2, 2)])).toEqual([2]);
|
||||
});
|
||||
|
||||
it('параллельный шаг: один согласовал, второй думает — следующий шаг ждёт', () => {
|
||||
const rows = [row(1, 1, REQUEST_STATUS.approved), row(2, 1, REQUEST_STATUS.onApproval), row(3, 2)];
|
||||
expect(pickRowsToActivate(rows)).toEqual([]);
|
||||
});
|
||||
|
||||
it('параллельный шаг: делегат активируется сразу, не дожидаясь второго участника', () => {
|
||||
// Аня (1) переназначила на Дину (0.5) и ушла в очередь, Борис (2) ещё думает.
|
||||
const rows = [row(4, 0.5), row(1, 1), row(2, 1, REQUEST_STATUS.onApproval), row(3, 2)];
|
||||
expect(pickRowsToActivate(rows)).toEqual([4]);
|
||||
});
|
||||
|
||||
it('параллельный шаг: делегат согласовал — Аня возвращается в пару к Борису', () => {
|
||||
const rows = [
|
||||
row(4, 0.5, REQUEST_STATUS.approved),
|
||||
row(1, 1),
|
||||
row(2, 1, REQUEST_STATUS.onApproval),
|
||||
row(3, 2),
|
||||
];
|
||||
expect(pickRowsToActivate(rows)).toEqual([1]);
|
||||
});
|
||||
|
||||
it('последовательный маршрут с делегатом: сначала делегат, потом делегировавший', () => {
|
||||
expect(pickRowsToActivate([row(4, 0.5), row(1, 1), row(3, 2)])).toEqual([4]);
|
||||
expect(pickRowsToActivate([row(4, 0.5, REQUEST_STATUS.approved), row(1, 1), row(3, 2)])).toEqual([1]);
|
||||
});
|
||||
|
||||
it('исполнение: тот же порядок', () => {
|
||||
const rows = [row(1, 3, REQUEST_STATUS.executed), row(2, 4)];
|
||||
expect(pickRowsToActivate(rows)).toEqual([2]);
|
||||
});
|
||||
|
||||
it('всё решено — активировать нечего', () => {
|
||||
expect(pickRowsToActivate([row(1, 1, REQUEST_STATUS.approved)])).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { IApprovalRow } from './models/IApprovalRow';
|
||||
import { REQUEST_STATUS } from './models/statuses';
|
||||
|
||||
export type IQueueRow = Pick<IApprovalRow, 'id' | 'status' | 'order'>;
|
||||
|
||||
/**
|
||||
* Какие строки ОДНОЙ фазы маршрута сделать активными. Чистая функция без
|
||||
* SharePoint, выполняет ApprovalListService.activateNextQueued.
|
||||
*
|
||||
* Текущий шаг — наименьший StepOrder среди ещё не решённых строк (ждущих и
|
||||
* активных). Активируются ждущие строки именно этого шага.
|
||||
*
|
||||
* Раньше правило было «есть активная строка — очередь не двигаем». На
|
||||
* параллельном шаге это ломалось: если участник (Аня) переназначал заявку
|
||||
* делегату (Дина встаёт перед Аней с дробным order), второй участник того же
|
||||
* шага (Борис) всё ещё «На согласовании», и Дина не активировалась, пока не
|
||||
* решит Борис. Теперь у Дины меньший order — она текущий шаг и активируется
|
||||
* сразу, а после её решения Аня возвращается в пару к Борису.
|
||||
*
|
||||
* На последовательном маршруте поведение прежнее: пока шаг активен, ждущих
|
||||
* строк с таким же или меньшим order нет — активировать нечего.
|
||||
*/
|
||||
export function pickRowsToActivate(phaseRows: IQueueRow[]): number[] {
|
||||
const pending = phaseRows.filter(
|
||||
(r) =>
|
||||
r.status === REQUEST_STATUS.queued ||
|
||||
r.status === REQUEST_STATUS.onApproval ||
|
||||
r.status === REQUEST_STATUS.onExecution
|
||||
);
|
||||
if (pending.length === 0) return [];
|
||||
|
||||
const currentOrder = Math.min(...pending.map((r) => r.order));
|
||||
return pending
|
||||
.filter((r) => r.order === currentOrder && r.status === REQUEST_STATUS.queued)
|
||||
.map((r) => r.id);
|
||||
}
|
||||
Reference in New Issue
Block a user