hierarchical-approval 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.md +54 -0
- package/dist/{ApprovalEngine-C1dUPnLM.d.cts → ApprovalEngine-BWrblNQ0.d.cts} +81 -7
- package/dist/{ApprovalEngine-Ci9urTDI.d.ts → ApprovalEngine-D4DdSx-M.d.ts} +81 -7
- package/dist/{IAuditAdapter-BHVKMMv6.d.cts → IAuditAdapter-2UU5edKG.d.cts} +1 -1
- package/dist/{IAuditAdapter-C4OASS6u.d.ts → IAuditAdapter-DSor54iY.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-BA_m2Dg9.d.ts → IAuthorizationPolicy-BOWg-3AH.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-CtLkwQ8I.d.cts → IAuthorizationPolicy-DPpiKzyh.d.cts} +1 -1
- package/dist/{INotificationAdapter-BdgfUrIn.d.ts → INotificationAdapter-BNGivLgU.d.ts} +1 -1
- package/dist/{INotificationAdapter-JJAiMyMd.d.cts → INotificationAdapter-C6HPrzh6.d.cts} +1 -1
- package/dist/{IOperationMiddleware-BGYrAqep.d.ts → IOperationMiddleware-CXtg2wzF.d.ts} +1 -1
- package/dist/{IOperationMiddleware-D0ksiFM3.d.cts → IOperationMiddleware-CxnPzpqL.d.cts} +1 -1
- package/dist/{IStorageAdapter-BU3sau5W.d.ts → IStorageAdapter-ChPT7ZDp.d.ts} +1 -1
- package/dist/{IStorageAdapter-DdHO4Rf1.d.cts → IStorageAdapter-yX9ERfQE.d.cts} +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +2 -2
- package/dist/adapters/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.d.cts +2 -2
- package/dist/adapters/PostgresAdapter.d.ts +2 -2
- package/dist/index.cjs +179 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +179 -12
- package/dist/index.js.map +1 -1
- package/dist/{instance-DUJY_Axf.d.cts → instance-weh2w7Ji.d.cts} +39 -1
- package/dist/{instance-DUJY_Axf.d.ts → instance-weh2w7Ji.d.ts} +39 -1
- package/dist/nestjs.cjs +111 -11
- package/dist/nestjs.cjs.map +1 -1
- package/dist/nestjs.d.cts +7 -7
- package/dist/nestjs.d.ts +7 -7
- package/dist/nestjs.js +111 -11
- package/dist/nestjs.js.map +1 -1
- package/dist/plugins/audit.d.cts +2 -2
- package/dist/plugins/audit.d.ts +2 -2
- package/dist/plugins/notify.d.cts +2 -2
- package/dist/plugins/notify.d.ts +2 -2
- package/dist/plugins/resilience.d.cts +3 -3
- package/dist/plugins/resilience.d.ts +3 -3
- package/dist/plugins/tracing.d.cts +2 -2
- package/dist/plugins/tracing.d.ts +2 -2
- package/dist/plugins/webhook.d.cts +2 -2
- package/dist/plugins/webhook.d.ts +2 -2
- package/dist/testing.cjs +111 -11
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +7 -7
- package/dist/testing.d.ts +7 -7
- package/dist/testing.js +111 -11
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
|
@@ -59,6 +59,12 @@ interface ApprovalLevelConfig {
|
|
|
59
59
|
*/
|
|
60
60
|
subWorkflow?: SubWorkflowConfig;
|
|
61
61
|
escalationAfterDays?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Escalate this level after this many **working hours**, for SLAs quoted in
|
|
64
|
+
* hours rather than days. Mutually exclusive with
|
|
65
|
+
* {@link escalationAfterDays}.
|
|
66
|
+
*/
|
|
67
|
+
escalationAfterHours?: number;
|
|
62
68
|
/**
|
|
63
69
|
* Send a reminder to this level's pending approvers this many days after the
|
|
64
70
|
* level opens. Escalation reassigns work; a reminder only nudges, which is
|
|
@@ -143,6 +149,15 @@ interface EscalationConfig {
|
|
|
143
149
|
afterDays: number;
|
|
144
150
|
escalateTo: ApproverConfig;
|
|
145
151
|
}
|
|
152
|
+
/** One rung of an escalation ladder. See {@link ApprovalTemplateConfig.escalationSteps}. */
|
|
153
|
+
interface EscalationStep {
|
|
154
|
+
/** Delay from the level opening, in calendar or business days. */
|
|
155
|
+
afterDays?: number;
|
|
156
|
+
/** Delay from the level opening, in working hours. Mutually exclusive with {@link afterDays}. */
|
|
157
|
+
afterHours?: number;
|
|
158
|
+
/** Who is added to the level when this rung fires. */
|
|
159
|
+
escalateTo: ApproverConfig;
|
|
160
|
+
}
|
|
146
161
|
interface ApprovalTemplateConfig {
|
|
147
162
|
name: string;
|
|
148
163
|
/**
|
|
@@ -164,8 +179,26 @@ interface ApprovalTemplateConfig {
|
|
|
164
179
|
levels: ApprovalLevelConfig[];
|
|
165
180
|
conditions?: ConditionRule[];
|
|
166
181
|
escalation?: EscalationConfig;
|
|
182
|
+
/**
|
|
183
|
+
* An escalation ladder: successive rungs, each measured from the moment the
|
|
184
|
+
* level opened, each adding another approver.
|
|
185
|
+
*
|
|
186
|
+
* A single {@link escalation} could only ever fire once, so a request that
|
|
187
|
+
* stalled past its second deadline had nowhere further to go — the usual
|
|
188
|
+
* "chase the manager, then the director, then the VP" pattern had to be built
|
|
189
|
+
* outside the engine. Steps are sorted by delay and fire in order; each adds
|
|
190
|
+
* its approver to the level without removing anyone already there.
|
|
191
|
+
*
|
|
192
|
+
* Takes precedence over {@link escalation} when both are set.
|
|
193
|
+
*/
|
|
194
|
+
escalationSteps?: EscalationStep[];
|
|
167
195
|
/** Overall SLA for the entire workflow in days. Emits approval:sla_breached when elapsed. */
|
|
168
196
|
slaDeadlineDays?: number;
|
|
197
|
+
/**
|
|
198
|
+
* Overall SLA in **working hours**. Mutually exclusive with
|
|
199
|
+
* {@link slaDeadlineDays}.
|
|
200
|
+
*/
|
|
201
|
+
slaDeadlineHours?: number;
|
|
169
202
|
/** Allow emergency override (bypass remaining levels). Must be true to use engine.override(). */
|
|
170
203
|
allowOverride?: boolean;
|
|
171
204
|
}
|
|
@@ -227,6 +260,9 @@ interface ApprovalLevelInstance {
|
|
|
227
260
|
weights?: Record<string, number>;
|
|
228
261
|
escalationDueAt?: Date;
|
|
229
262
|
escalationAfterDays?: number;
|
|
263
|
+
escalationAfterHours?: number;
|
|
264
|
+
/** How many escalation rungs have fired on this level. */
|
|
265
|
+
escalationStep?: number;
|
|
230
266
|
/** When the next reminder for this level is due; cleared when the level closes. */
|
|
231
267
|
reminderDueAt?: Date;
|
|
232
268
|
/** How many reminders have already been sent for this level. */
|
|
@@ -280,7 +316,9 @@ interface InfoRequest {
|
|
|
280
316
|
/** Snapshot of template configuration captured at submit time to insulate in-flight instances from template updates. */
|
|
281
317
|
interface TemplateSnapshot {
|
|
282
318
|
escalation?: EscalationConfig;
|
|
319
|
+
escalationSteps?: EscalationStep[];
|
|
283
320
|
slaDeadlineDays?: number;
|
|
321
|
+
slaDeadlineHours?: number;
|
|
284
322
|
allowOverride?: boolean;
|
|
285
323
|
}
|
|
286
324
|
interface ApprovalInstance {
|
|
@@ -329,4 +367,4 @@ interface ApprovalInstance {
|
|
|
329
367
|
attachments?: Attachment[];
|
|
330
368
|
}
|
|
331
369
|
|
|
332
|
-
export type { ApprovalTemplate as A, Condition as C, EscalationConfig as E, InfoRequest as I, LevelStatus as L, ResolvedApprover as R, SubWorkflowConfig as S, TemplateSnapshot as T, ApprovalInstance as a, AuditEntry as b, ApprovalLevelConfig as c, ApprovalLevelInstance as d, ApprovalMode as e, ApprovalStatus as f, ApprovalTemplateConfig as g, ApproverConfig as h, Attachment as i, AuditAction as j, AuditContext as k, ConditionExpression as l, ConditionGroup as m, ConditionOperator as n, ConditionRule as o,
|
|
370
|
+
export type { ApprovalTemplate as A, Condition as C, EscalationConfig as E, InfoRequest as I, LevelStatus as L, ResolvedApprover as R, SubWorkflowConfig as S, TemplateSnapshot as T, ApprovalInstance as a, AuditEntry as b, ApprovalLevelConfig as c, ApprovalLevelInstance as d, ApprovalMode as e, ApprovalStatus as f, ApprovalTemplateConfig as g, ApproverConfig as h, Attachment as i, AuditAction as j, AuditContext as k, ConditionExpression as l, ConditionGroup as m, ConditionOperator as n, ConditionRule as o, EscalationStep as p, ResolverFn as q };
|
|
@@ -59,6 +59,12 @@ interface ApprovalLevelConfig {
|
|
|
59
59
|
*/
|
|
60
60
|
subWorkflow?: SubWorkflowConfig;
|
|
61
61
|
escalationAfterDays?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Escalate this level after this many **working hours**, for SLAs quoted in
|
|
64
|
+
* hours rather than days. Mutually exclusive with
|
|
65
|
+
* {@link escalationAfterDays}.
|
|
66
|
+
*/
|
|
67
|
+
escalationAfterHours?: number;
|
|
62
68
|
/**
|
|
63
69
|
* Send a reminder to this level's pending approvers this many days after the
|
|
64
70
|
* level opens. Escalation reassigns work; a reminder only nudges, which is
|
|
@@ -143,6 +149,15 @@ interface EscalationConfig {
|
|
|
143
149
|
afterDays: number;
|
|
144
150
|
escalateTo: ApproverConfig;
|
|
145
151
|
}
|
|
152
|
+
/** One rung of an escalation ladder. See {@link ApprovalTemplateConfig.escalationSteps}. */
|
|
153
|
+
interface EscalationStep {
|
|
154
|
+
/** Delay from the level opening, in calendar or business days. */
|
|
155
|
+
afterDays?: number;
|
|
156
|
+
/** Delay from the level opening, in working hours. Mutually exclusive with {@link afterDays}. */
|
|
157
|
+
afterHours?: number;
|
|
158
|
+
/** Who is added to the level when this rung fires. */
|
|
159
|
+
escalateTo: ApproverConfig;
|
|
160
|
+
}
|
|
146
161
|
interface ApprovalTemplateConfig {
|
|
147
162
|
name: string;
|
|
148
163
|
/**
|
|
@@ -164,8 +179,26 @@ interface ApprovalTemplateConfig {
|
|
|
164
179
|
levels: ApprovalLevelConfig[];
|
|
165
180
|
conditions?: ConditionRule[];
|
|
166
181
|
escalation?: EscalationConfig;
|
|
182
|
+
/**
|
|
183
|
+
* An escalation ladder: successive rungs, each measured from the moment the
|
|
184
|
+
* level opened, each adding another approver.
|
|
185
|
+
*
|
|
186
|
+
* A single {@link escalation} could only ever fire once, so a request that
|
|
187
|
+
* stalled past its second deadline had nowhere further to go — the usual
|
|
188
|
+
* "chase the manager, then the director, then the VP" pattern had to be built
|
|
189
|
+
* outside the engine. Steps are sorted by delay and fire in order; each adds
|
|
190
|
+
* its approver to the level without removing anyone already there.
|
|
191
|
+
*
|
|
192
|
+
* Takes precedence over {@link escalation} when both are set.
|
|
193
|
+
*/
|
|
194
|
+
escalationSteps?: EscalationStep[];
|
|
167
195
|
/** Overall SLA for the entire workflow in days. Emits approval:sla_breached when elapsed. */
|
|
168
196
|
slaDeadlineDays?: number;
|
|
197
|
+
/**
|
|
198
|
+
* Overall SLA in **working hours**. Mutually exclusive with
|
|
199
|
+
* {@link slaDeadlineDays}.
|
|
200
|
+
*/
|
|
201
|
+
slaDeadlineHours?: number;
|
|
169
202
|
/** Allow emergency override (bypass remaining levels). Must be true to use engine.override(). */
|
|
170
203
|
allowOverride?: boolean;
|
|
171
204
|
}
|
|
@@ -227,6 +260,9 @@ interface ApprovalLevelInstance {
|
|
|
227
260
|
weights?: Record<string, number>;
|
|
228
261
|
escalationDueAt?: Date;
|
|
229
262
|
escalationAfterDays?: number;
|
|
263
|
+
escalationAfterHours?: number;
|
|
264
|
+
/** How many escalation rungs have fired on this level. */
|
|
265
|
+
escalationStep?: number;
|
|
230
266
|
/** When the next reminder for this level is due; cleared when the level closes. */
|
|
231
267
|
reminderDueAt?: Date;
|
|
232
268
|
/** How many reminders have already been sent for this level. */
|
|
@@ -280,7 +316,9 @@ interface InfoRequest {
|
|
|
280
316
|
/** Snapshot of template configuration captured at submit time to insulate in-flight instances from template updates. */
|
|
281
317
|
interface TemplateSnapshot {
|
|
282
318
|
escalation?: EscalationConfig;
|
|
319
|
+
escalationSteps?: EscalationStep[];
|
|
283
320
|
slaDeadlineDays?: number;
|
|
321
|
+
slaDeadlineHours?: number;
|
|
284
322
|
allowOverride?: boolean;
|
|
285
323
|
}
|
|
286
324
|
interface ApprovalInstance {
|
|
@@ -329,4 +367,4 @@ interface ApprovalInstance {
|
|
|
329
367
|
attachments?: Attachment[];
|
|
330
368
|
}
|
|
331
369
|
|
|
332
|
-
export type { ApprovalTemplate as A, Condition as C, EscalationConfig as E, InfoRequest as I, LevelStatus as L, ResolvedApprover as R, SubWorkflowConfig as S, TemplateSnapshot as T, ApprovalInstance as a, AuditEntry as b, ApprovalLevelConfig as c, ApprovalLevelInstance as d, ApprovalMode as e, ApprovalStatus as f, ApprovalTemplateConfig as g, ApproverConfig as h, Attachment as i, AuditAction as j, AuditContext as k, ConditionExpression as l, ConditionGroup as m, ConditionOperator as n, ConditionRule as o,
|
|
370
|
+
export type { ApprovalTemplate as A, Condition as C, EscalationConfig as E, InfoRequest as I, LevelStatus as L, ResolvedApprover as R, SubWorkflowConfig as S, TemplateSnapshot as T, ApprovalInstance as a, AuditEntry as b, ApprovalLevelConfig as c, ApprovalLevelInstance as d, ApprovalMode as e, ApprovalStatus as f, ApprovalTemplateConfig as g, ApproverConfig as h, Attachment as i, AuditAction as j, AuditContext as k, ConditionExpression as l, ConditionGroup as m, ConditionOperator as n, ConditionRule as o, EscalationStep as p, ResolverFn as q };
|
package/dist/nestjs.cjs
CHANGED
|
@@ -1072,6 +1072,18 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
1072
1072
|
message: `Level ${l.level} sets reminderEveryDays without reminderAfterDays, so no reminder would ever be sent.`
|
|
1073
1073
|
});
|
|
1074
1074
|
}
|
|
1075
|
+
if (l.escalationAfterHours !== void 0 && l.escalationAfterHours <= 0) {
|
|
1076
|
+
errors.push({
|
|
1077
|
+
field: `levels[${i}].escalationAfterHours`,
|
|
1078
|
+
message: `Level ${l.level} escalationAfterHours must be a positive number.`
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
if (l.escalationAfterDays !== void 0 && l.escalationAfterHours !== void 0) {
|
|
1082
|
+
errors.push({
|
|
1083
|
+
field: `levels[${i}].escalationAfterHours`,
|
|
1084
|
+
message: `Level ${l.level} sets both escalationAfterDays and escalationAfterHours; pick one so the deadline is unambiguous.`
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1075
1087
|
if (l.escalationAfterDays !== void 0 && l.escalationAfterDays <= 0) {
|
|
1076
1088
|
errors.push({
|
|
1077
1089
|
field: `levels[${i}].escalationAfterDays`,
|
|
@@ -1136,6 +1148,18 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
1136
1148
|
}
|
|
1137
1149
|
});
|
|
1138
1150
|
}
|
|
1151
|
+
if (config.slaDeadlineDays !== void 0 && config.slaDeadlineHours !== void 0) {
|
|
1152
|
+
errors.push({
|
|
1153
|
+
field: "slaDeadlineHours",
|
|
1154
|
+
message: "Template sets both slaDeadlineDays and slaDeadlineHours; pick one so the SLA is unambiguous."
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
if (config.slaDeadlineHours !== void 0 && config.slaDeadlineHours <= 0) {
|
|
1158
|
+
errors.push({
|
|
1159
|
+
field: "slaDeadlineHours",
|
|
1160
|
+
message: "slaDeadlineHours must be a positive number."
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1139
1163
|
if (config.conditions) {
|
|
1140
1164
|
config.conditions.forEach((rule, ruleIdx) => {
|
|
1141
1165
|
errors.push(...validateConditionExpression(rule.when, `conditions[${ruleIdx}].when`));
|
|
@@ -1249,7 +1273,9 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
1249
1273
|
threshold: cfg.threshold,
|
|
1250
1274
|
weights: cfg.weights,
|
|
1251
1275
|
escalationAfterDays: cfg.escalationAfterDays,
|
|
1252
|
-
|
|
1276
|
+
escalationAfterHours: cfg.escalationAfterHours,
|
|
1277
|
+
escalationStep: 0,
|
|
1278
|
+
escalationDueAt: inFirstGroup ? this.levelEscalationDue(now, cfg, this.firstRungOf(template.escalationSteps)) : void 0,
|
|
1253
1279
|
subWorkflowTemplate: cfg.subWorkflow?.templateName,
|
|
1254
1280
|
reminderAfterDays: cfg.reminderAfterDays,
|
|
1255
1281
|
reminderEveryDays: cfg.reminderEveryDays,
|
|
@@ -1279,7 +1305,7 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
1279
1305
|
timestamp: now,
|
|
1280
1306
|
...auditCtx
|
|
1281
1307
|
};
|
|
1282
|
-
const slaDeadlineAt = template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
|
|
1308
|
+
const slaDeadlineAt = template.slaDeadlineHours ? this.deadlineFromHours(now, template.slaDeadlineHours) : template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
|
|
1283
1309
|
const instance = {
|
|
1284
1310
|
id: instanceId,
|
|
1285
1311
|
tenantId: this.tenantId,
|
|
@@ -1306,7 +1332,9 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
1306
1332
|
slaDeadlineAt,
|
|
1307
1333
|
templateSnapshot: {
|
|
1308
1334
|
escalation: template.escalation,
|
|
1335
|
+
escalationSteps: template.escalationSteps,
|
|
1309
1336
|
slaDeadlineDays: template.slaDeadlineDays,
|
|
1337
|
+
slaDeadlineHours: template.slaDeadlineHours,
|
|
1310
1338
|
allowOverride: template.allowOverride
|
|
1311
1339
|
}
|
|
1312
1340
|
};
|
|
@@ -2578,7 +2606,7 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
2578
2606
|
reason: opts.reason,
|
|
2579
2607
|
...auditCtx
|
|
2580
2608
|
};
|
|
2581
|
-
const slaDeadlineAt = template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
|
|
2609
|
+
const slaDeadlineAt = template.slaDeadlineHours ? this.deadlineFromHours(now, template.slaDeadlineHours) : template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
|
|
2582
2610
|
const newInstance = {
|
|
2583
2611
|
id: newInstanceId,
|
|
2584
2612
|
tenantId: this.tenantId,
|
|
@@ -2600,7 +2628,9 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
2600
2628
|
slaDeadlineAt,
|
|
2601
2629
|
templateSnapshot: {
|
|
2602
2630
|
escalation: template.escalation,
|
|
2631
|
+
escalationSteps: template.escalationSteps,
|
|
2603
2632
|
slaDeadlineDays: template.slaDeadlineDays,
|
|
2633
|
+
slaDeadlineHours: template.slaDeadlineHours,
|
|
2604
2634
|
allowOverride: template.allowOverride
|
|
2605
2635
|
}
|
|
2606
2636
|
};
|
|
@@ -3347,10 +3377,14 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3347
3377
|
async escalateInternal(instanceId, escalatedBy = "system", auditCtx, levelNumber) {
|
|
3348
3378
|
return this.withOptimisticRetry(instanceId, async (instance) => {
|
|
3349
3379
|
if (instance.status !== "pending") return instance;
|
|
3350
|
-
const
|
|
3351
|
-
|
|
3380
|
+
const ladder = await this.escalationLadder(instance);
|
|
3381
|
+
const targetLevel = levelNumber === void 0 ? this.currentLevelInstance(instance) : instance.levels.find((l) => l.level === levelNumber) ?? this.currentLevelInstance(instance);
|
|
3382
|
+
const rungIndex = targetLevel.escalationStep ?? 0;
|
|
3383
|
+
const rung = ladder[rungIndex];
|
|
3384
|
+
const escalateTo = rung?.escalateTo ?? (instance.templateSnapshot?.escalation ?? (await this.registry.get(instance.templateName)).escalation)?.escalateTo;
|
|
3385
|
+
if (!escalateTo) return instance;
|
|
3352
3386
|
const newApprovers = await this.resolver.resolveApprovers(
|
|
3353
|
-
[
|
|
3387
|
+
[escalateTo],
|
|
3354
3388
|
instance.submittedBy,
|
|
3355
3389
|
instance.data,
|
|
3356
3390
|
this.opts.orgProvider,
|
|
@@ -3368,10 +3402,13 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3368
3402
|
);
|
|
3369
3403
|
return instance;
|
|
3370
3404
|
}
|
|
3371
|
-
const level =
|
|
3405
|
+
const level = targetLevel;
|
|
3372
3406
|
level.approverIds = [.../* @__PURE__ */ new Set([...level.approverIds, ...filteredApprovers])];
|
|
3373
|
-
level.escalationDueAt = void 0;
|
|
3374
3407
|
const now = this.clock.now();
|
|
3408
|
+
level.escalationStep = rungIndex + 1;
|
|
3409
|
+
const nextRung = ladder[rungIndex + 1];
|
|
3410
|
+
const levelOpenedAt = this.levelOpenedAt(instance, level, now);
|
|
3411
|
+
level.escalationDueAt = nextRung ? this.stepDueAt(levelOpenedAt, nextRung) : void 0;
|
|
3375
3412
|
const escalatedTo = filteredApprovers[0] ?? "unknown";
|
|
3376
3413
|
const auditEntry = {
|
|
3377
3414
|
action: "escalated",
|
|
@@ -3543,6 +3580,69 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3543
3580
|
deadlineFrom(from, days) {
|
|
3544
3581
|
return this.calendar ? this.calendar.addBusinessDays(from, days) : new Date(from.getTime() + days * 864e5);
|
|
3545
3582
|
}
|
|
3583
|
+
/**
|
|
3584
|
+
* Resolve an hour-based deadline.
|
|
3585
|
+
*
|
|
3586
|
+
* A calendar that only knows whole days — `weekendCalendar`, say — has no
|
|
3587
|
+
* `addBusinessHours`, and this falls back to elapsed clock time rather than
|
|
3588
|
+
* quietly pretending the calendar was applied. Configure
|
|
3589
|
+
* `businessHoursCalendar` to have hours skip evenings and weekends.
|
|
3590
|
+
*/
|
|
3591
|
+
deadlineFromHours(from, hours) {
|
|
3592
|
+
const addHours = this.calendar?.addBusinessHours?.bind(this.calendar);
|
|
3593
|
+
return addHours ? addHours(from, hours) : new Date(from.getTime() + hours * 36e5);
|
|
3594
|
+
}
|
|
3595
|
+
/** Level deadline from whichever of days/hours the template configured. */
|
|
3596
|
+
levelEscalationDue(from, level, firstRung) {
|
|
3597
|
+
if (level.escalationAfterHours) return this.deadlineFromHours(from, level.escalationAfterHours);
|
|
3598
|
+
if (level.escalationAfterDays) return this.deadlineFrom(from, level.escalationAfterDays);
|
|
3599
|
+
return firstRung ? this.stepDueAt(from, firstRung) : void 0;
|
|
3600
|
+
}
|
|
3601
|
+
/** First rung of a ladder, sorted by delay, or undefined when there is none. */
|
|
3602
|
+
firstRungOf(steps) {
|
|
3603
|
+
if (!steps || steps.length === 0) return void 0;
|
|
3604
|
+
return [...steps].sort(
|
|
3605
|
+
(a, b) => (a.afterHours ?? (a.afterDays ?? 0) * 24) - (b.afterHours ?? (b.afterDays ?? 0) * 24)
|
|
3606
|
+
)[0];
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* The escalation ladder for an instance, sorted by delay.
|
|
3610
|
+
*
|
|
3611
|
+
* Read from the instance's template snapshot so an in-flight approval keeps
|
|
3612
|
+
* the ladder it was submitted under, exactly as the single-step
|
|
3613
|
+
* {@link EscalationConfig} already did.
|
|
3614
|
+
*/
|
|
3615
|
+
async escalationLadder(instance) {
|
|
3616
|
+
const snapshot = instance.templateSnapshot;
|
|
3617
|
+
const steps = snapshot?.escalationSteps ?? (await this.registry.get(instance.templateName)).escalationSteps ?? [];
|
|
3618
|
+
return [...steps].sort(
|
|
3619
|
+
(a, b) => (a.afterHours ?? (a.afterDays ?? 0) * 24) - (b.afterHours ?? (b.afterDays ?? 0) * 24)
|
|
3620
|
+
);
|
|
3621
|
+
}
|
|
3622
|
+
/**
|
|
3623
|
+
* When a level started collecting decisions.
|
|
3624
|
+
*
|
|
3625
|
+
* Escalation rungs are measured from this, not from the previous rung, so a
|
|
3626
|
+
* ladder reads the way it is written. Recovered from the audit trail — the
|
|
3627
|
+
* `submitted` entry for the opening level, the `level_advanced` entry
|
|
3628
|
+
* otherwise — and falls back to `now` when no entry exists, which only leaves
|
|
3629
|
+
* the ladder no worse off than the single-step behaviour it replaces.
|
|
3630
|
+
*/
|
|
3631
|
+
levelOpenedAt(instance, level, fallback) {
|
|
3632
|
+
for (let i = instance.auditLog.length - 1; i >= 0; i--) {
|
|
3633
|
+
const entry = instance.auditLog[i];
|
|
3634
|
+
if (!entry) continue;
|
|
3635
|
+
const opensThisLevel = (entry.action === "level_advanced" || entry.action === "submitted") && entry.level === level.level;
|
|
3636
|
+
if (opensThisLevel) return new Date(entry.timestamp);
|
|
3637
|
+
}
|
|
3638
|
+
return fallback;
|
|
3639
|
+
}
|
|
3640
|
+
/** Deadline for one rung, measured from when the level opened. */
|
|
3641
|
+
stepDueAt(from, step) {
|
|
3642
|
+
if (step.afterHours) return this.deadlineFromHours(from, step.afterHours);
|
|
3643
|
+
if (step.afterDays) return this.deadlineFrom(from, step.afterDays);
|
|
3644
|
+
return void 0;
|
|
3645
|
+
}
|
|
3546
3646
|
async requireInstance(id) {
|
|
3547
3647
|
const instance = await this.opts.adapter.getInstance(this.tenantId, id);
|
|
3548
3648
|
if (!instance) throw new ApprovalNotFoundError("Instance", id);
|
|
@@ -3635,6 +3735,7 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3635
3735
|
}
|
|
3636
3736
|
/** Resolve approvers for a group, set its deadlines, and mark it pending. */
|
|
3637
3737
|
async activateGroup(instance, group, now) {
|
|
3738
|
+
const firstRung = this.firstRungOf(instance.templateSnapshot?.escalationSteps);
|
|
3638
3739
|
for (const lvl of group) {
|
|
3639
3740
|
if (lvl.subWorkflowTemplate) {
|
|
3640
3741
|
lvl.approverIds = [];
|
|
@@ -3648,9 +3749,8 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3648
3749
|
now
|
|
3649
3750
|
);
|
|
3650
3751
|
}
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
}
|
|
3752
|
+
lvl.escalationDueAt = this.levelEscalationDue(now, lvl, firstRung);
|
|
3753
|
+
lvl.escalationStep = 0;
|
|
3654
3754
|
this.scheduleReminder(lvl, now);
|
|
3655
3755
|
lvl.status = "pending";
|
|
3656
3756
|
}
|