hierarchical-approval 0.1.1 → 0.2.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/LICENSE +21 -0
- package/README.md +113 -9
- package/dist/{ApprovalEngine-DdZtyeB5.d.ts → ApprovalEngine-CSxLWZ5h.d.cts} +82 -4
- package/dist/{ApprovalEngine-BcnLzfAU.d.cts → ApprovalEngine-DICBgdIp.d.ts} +82 -4
- package/dist/{IStorageAdapter-RAiLF8bc.d.cts → IStorageAdapter-B2xGuaKi.d.cts} +25 -2
- package/dist/{IStorageAdapter-RAiLF8bc.d.ts → IStorageAdapter-B2xGuaKi.d.ts} +25 -2
- package/dist/adapters/MemoryAdapter.d.cts +1 -1
- package/dist/adapters/MemoryAdapter.d.ts +1 -1
- package/dist/adapters/PostgresAdapter.d.cts +1 -1
- package/dist/adapters/PostgresAdapter.d.ts +1 -1
- package/dist/index.cjs +220 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +220 -16
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +191 -15
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +191 -15
- package/dist/testing.js.map +1 -1
- package/package.json +10 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthews Wong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# hierarchical-approval
|
|
2
4
|
|
|
3
|
-
TypeScript-first multi-level approval workflows for enterprise systems
|
|
5
|
+
**TypeScript-first multi-level approval workflows for enterprise systems.**
|
|
6
|
+
Multi-tenant · audit-ready · fully pluggable · zero runtime dependencies you don't opt into.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/hierarchical-approval)
|
|
9
|
+
[](https://www.npmjs.com/package/hierarchical-approval)
|
|
10
|
+
[](https://github.com/matthews-wong/hierarchical-approval/actions/workflows/ci.yml)
|
|
11
|
+
[](https://www.typescriptlang.org/)
|
|
12
|
+
[](https://bundlephobia.com/package/hierarchical-approval)
|
|
13
|
+
[](./LICENSE)
|
|
14
|
+
[](./tests)
|
|
15
|
+
|
|
16
|
+
[**Documentation**](https://hierarchical-approval.matthewswong.com) ·
|
|
17
|
+
[**npm**](https://www.npmjs.com/package/hierarchical-approval) ·
|
|
18
|
+
[**Changelog**](./IMPROVEMENTS.md) ·
|
|
19
|
+
[**Examples**](./examples)
|
|
20
|
+
|
|
21
|
+
</div>
|
|
4
22
|
|
|
5
23
|
```sh
|
|
6
24
|
npm install hierarchical-approval
|
|
@@ -8,6 +26,8 @@ npm install hierarchical-approval
|
|
|
8
26
|
npm install pg @types/pg
|
|
9
27
|
```
|
|
10
28
|
|
|
29
|
+
> 📖 Full docs & live guides: **[hierarchical-approval.matthewswong.com](https://hierarchical-approval.matthewswong.com)**
|
|
30
|
+
|
|
11
31
|
---
|
|
12
32
|
|
|
13
33
|
## Why another approval library?
|
|
@@ -33,7 +53,7 @@ Approval workflows are deceptively simple until they aren't. Most teams start wi
|
|
|
33
53
|
**`approval-flow`** — Single-level only; no multi-tenancy; no audit trail; last published 2020.
|
|
34
54
|
**`workflow-engine`** — Generic state machine; you implement every guard, every condition, every audit entry yourself.
|
|
35
55
|
**`node-approval`** — No TypeScript; no idempotency; no optimistic locking.
|
|
36
|
-
**Hand-rolled** — You *will* hit the concurrent-approval race condition eventually. This library has
|
|
56
|
+
**Hand-rolled** — You *will* hit the concurrent-approval race condition eventually. This library has 195 tests covering it.
|
|
37
57
|
|
|
38
58
|
---
|
|
39
59
|
|
|
@@ -137,11 +157,44 @@ An **instance** is a single document moving through a template. Key fields:
|
|
|
137
157
|
|
|
138
158
|
Each level's `mode` field controls how many approvers are required:
|
|
139
159
|
|
|
140
|
-
| Mode | Required |
|
|
141
|
-
|
|
142
|
-
| `'any'` | One approver is enough |
|
|
143
|
-
| `'all'` | Every listed approver must act |
|
|
144
|
-
| `'majority'` | More than half must approve |
|
|
160
|
+
| Mode | Required | Extra config |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `'any'` | One approver is enough | — |
|
|
163
|
+
| `'all'` | Every listed approver must act | — |
|
|
164
|
+
| `'majority'` | More than half must approve | — |
|
|
165
|
+
| `'quorum'` | A fixed **N-of-M** threshold approves | `minApprovals` |
|
|
166
|
+
| `'weighted'` | Cumulative approver **weight** meets a threshold | `threshold`, optional `weights` |
|
|
167
|
+
|
|
168
|
+
A level is **rejected** as soon as the outcome becomes mathematically impossible — e.g. a `quorum` of 2-of-3 is rejected after the second rejection (only one approver remains), and a `weighted` level is rejected once the weight still achievable falls below `threshold`.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// Quorum: any 2 of these 3 directors must approve
|
|
172
|
+
{
|
|
173
|
+
level: 1,
|
|
174
|
+
name: 'Board',
|
|
175
|
+
mode: 'quorum',
|
|
176
|
+
minApprovals: 2,
|
|
177
|
+
approvers: [
|
|
178
|
+
{ type: 'user', userId: 'd1' },
|
|
179
|
+
{ type: 'user', userId: 'd2' },
|
|
180
|
+
{ type: 'user', userId: 'd3' },
|
|
181
|
+
],
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Weighted: the CFO's vote (weight 3) clears the threshold alone;
|
|
185
|
+
// otherwise three default-weight (1) approvers are needed.
|
|
186
|
+
{
|
|
187
|
+
level: 1,
|
|
188
|
+
name: 'Exec Committee',
|
|
189
|
+
mode: 'weighted',
|
|
190
|
+
threshold: 3,
|
|
191
|
+
weights: { cfo: 3 }, // unlisted approvers default to weight 1
|
|
192
|
+
approvers: [
|
|
193
|
+
{ type: 'user', userId: 'cfo' },
|
|
194
|
+
{ type: 'user', userId: 'mgr' },
|
|
195
|
+
],
|
|
196
|
+
}
|
|
197
|
+
```
|
|
145
198
|
|
|
146
199
|
### Conditional chains
|
|
147
200
|
|
|
@@ -344,6 +397,21 @@ await engine.delegate(instanceId, {
|
|
|
344
397
|
});
|
|
345
398
|
```
|
|
346
399
|
|
|
400
|
+
### Reassign
|
|
401
|
+
|
|
402
|
+
Administratively swap an approver on the current level — for when an assigned approver is unavailable and a third party (e.g. an admin) needs to hand the task to someone else. Unlike `delegate`, the original approver doesn't initiate it. The approver being replaced must still be pending (one who has already approved or rejected cannot be reassigned), and the new approver must not already be on the level.
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
await engine.reassign(instanceId, {
|
|
406
|
+
reassignedBy: 'workflow-admin',
|
|
407
|
+
fromApprover: 'mgr-1',
|
|
408
|
+
toApprover: 'mgr-2',
|
|
409
|
+
reason: 'Approver left the company',
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Emits `approval:reassigned` and records a `reassigned` audit entry.
|
|
414
|
+
|
|
347
415
|
### Escalate
|
|
348
416
|
|
|
349
417
|
```ts
|
|
@@ -352,6 +420,24 @@ await engine.escalate(instanceId, { escalatedBy: 'system' });
|
|
|
352
420
|
|
|
353
421
|
Adds the escalation approver (from `template.escalation.escalateTo`) to the current level's approver list. Also fires automatically via the scheduler when `escalationAfterDays` elapses.
|
|
354
422
|
|
|
423
|
+
#### Business-day deadlines
|
|
424
|
+
|
|
425
|
+
By default `escalationAfterDays` and `slaDeadlineDays` count plain calendar days. Pass a `calendar` to the engine to count **business days** instead — skipping weekends and holidays:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
import { ApprovalEngine, weekendCalendar } from 'hierarchical-approval';
|
|
429
|
+
|
|
430
|
+
const engine = new ApprovalEngine({
|
|
431
|
+
adapter,
|
|
432
|
+
calendar: weekendCalendar({
|
|
433
|
+
holidays: [new Date('2026-12-25'), new Date('2027-01-01')],
|
|
434
|
+
// weekendDays: [5, 6], // optional — e.g. Fri/Sat weekend
|
|
435
|
+
}),
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
With this calendar, a level whose `escalationAfterDays: 2` is submitted on a Friday becomes due the following Tuesday rather than Sunday. Provide your own `BusinessCalendar` implementation for region-specific rules.
|
|
440
|
+
|
|
355
441
|
### Cancel
|
|
356
442
|
|
|
357
443
|
```ts
|
|
@@ -535,6 +621,20 @@ const result = await engine.canApprove(instanceId, 'mgr-1');
|
|
|
535
621
|
|
|
536
622
|
Never throws — always returns a structured result.
|
|
537
623
|
|
|
624
|
+
### Statistics
|
|
625
|
+
|
|
626
|
+
Aggregate counts for dashboards. Pass an optional filter (`documentType`, `submittedBy`, `fromDate`/`toDate`) to scope the numbers; `status` is ignored since every status is counted.
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
const stats = await engine.getStatistics({ documentType: 'purchase_order' });
|
|
630
|
+
// {
|
|
631
|
+
// total: number,
|
|
632
|
+
// byStatus: { pending, approved, rejected, cancelled, expired },
|
|
633
|
+
// overdue: number, // pending past an escalation/expiry deadline
|
|
634
|
+
// approvalRate: number, // approved / (approved + rejected); 0 when none resolved
|
|
635
|
+
// }
|
|
636
|
+
```
|
|
637
|
+
|
|
538
638
|
### Health check
|
|
539
639
|
|
|
540
640
|
```ts
|
|
@@ -567,6 +667,7 @@ engine.on('approval:approved', (payload) => { /* level or fully approved *
|
|
|
567
667
|
engine.on('approval:rejected', (payload) => { /* level rejected */ });
|
|
568
668
|
engine.on('approval:level_advanced', (payload) => { /* moved to next level */ });
|
|
569
669
|
engine.on('approval:delegated', (payload) => { /* approver delegated */ });
|
|
670
|
+
engine.on('approval:reassigned', (payload) => { /* approver reassigned by admin */ });
|
|
570
671
|
engine.on('approval:escalated', (payload) => { /* escalated to new approver */ });
|
|
571
672
|
engine.on('approval:cancelled', (payload) => { /* cancelled */ });
|
|
572
673
|
engine.on('approval:expired', (payload) => { /* deadline passed */ });
|
|
@@ -681,7 +782,7 @@ import { ApprovalForbiddenError } from 'hierarchical-approval';
|
|
|
681
782
|
|
|
682
783
|
class SigningAuthorityPolicy implements IAuthorizationPolicy {
|
|
683
784
|
async authorize(ctx: AuthorizationContext): Promise<string | undefined> {
|
|
684
|
-
// ctx.operation — 'approve' | 'reject' | 'delegate' | 'cancel' | 'escalate'
|
|
785
|
+
// ctx.operation — 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate'
|
|
685
786
|
// | 'override' | 'resubmit' | 'addComment' | 'submit'
|
|
686
787
|
// ctx.actorId — who is performing the action
|
|
687
788
|
// ctx.instance — current instance (read-only)
|
|
@@ -956,7 +1057,10 @@ interface ApprovalLevelConfig {
|
|
|
956
1057
|
level: number; // execution order (must be unique within template)
|
|
957
1058
|
name: string; // display name
|
|
958
1059
|
approvers: ApproverConfig[]; // at least one required
|
|
959
|
-
mode: 'any' | 'all' | 'majority';
|
|
1060
|
+
mode: 'any' | 'all' | 'majority' | 'quorum' | 'weighted';
|
|
1061
|
+
minApprovals?: number; // required when mode is 'quorum' (N-of-M threshold)
|
|
1062
|
+
threshold?: number; // required when mode is 'weighted' (cumulative weight to pass)
|
|
1063
|
+
weights?: Record<string, number>; // optional per-approver weights for 'weighted'; default 1
|
|
960
1064
|
escalationAfterDays?: number; // per-level escalation (overrides template.escalation timing)
|
|
961
1065
|
}
|
|
962
1066
|
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as AuditEntry, g as ApprovalLevelInstance, I as IStorageAdapter, q as ResolverFn, j as ApprovalTemplateConfig, A as ApprovalTemplate, m as AuditContext, h as ApprovalMode, P as PaginationOpts, b as PaginatedResult, c as InstanceFilter, C as CursorPaginationOpts, e as CursorPaginatedResult } from './IStorageAdapter-
|
|
1
|
+
import { a as ApprovalInstance, d as AuditEntry, g as ApprovalLevelInstance, I as IStorageAdapter, q as ResolverFn, j as ApprovalTemplateConfig, A as ApprovalTemplate, m as AuditContext, h as ApprovalMode, P as PaginationOpts, b as PaginatedResult, c as InstanceFilter, C as CursorPaginationOpts, e as CursorPaginatedResult } from './IStorageAdapter-B2xGuaKi.cjs';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
interface ApprovalEvent {
|
|
@@ -29,6 +29,13 @@ interface DelegatedEvent extends ApprovalEvent {
|
|
|
29
29
|
level: number;
|
|
30
30
|
reason: string;
|
|
31
31
|
}
|
|
32
|
+
interface ReassignedEvent extends ApprovalEvent {
|
|
33
|
+
reassignedBy: string;
|
|
34
|
+
fromApprover: string;
|
|
35
|
+
toApprover: string;
|
|
36
|
+
level: number;
|
|
37
|
+
reason: string;
|
|
38
|
+
}
|
|
32
39
|
interface EscalatedEvent extends ApprovalEvent {
|
|
33
40
|
level: number;
|
|
34
41
|
escalatedTo: string;
|
|
@@ -61,6 +68,7 @@ interface ApprovalEventMap {
|
|
|
61
68
|
'approval:approved': ApprovedEvent;
|
|
62
69
|
'approval:rejected': RejectedEvent;
|
|
63
70
|
'approval:delegated': DelegatedEvent;
|
|
71
|
+
'approval:reassigned': ReassignedEvent;
|
|
64
72
|
'approval:escalated': EscalatedEvent;
|
|
65
73
|
'approval:cancelled': CancelledEvent;
|
|
66
74
|
'approval:completed': ApprovalInstance;
|
|
@@ -106,6 +114,12 @@ declare const DelegateOptionsSchema: z.ZodObject<{
|
|
|
106
114
|
reason: z.ZodString;
|
|
107
115
|
until: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
108
116
|
}, z.core.$strip>;
|
|
117
|
+
declare const ReassignOptionsSchema: z.ZodObject<{
|
|
118
|
+
reassignedBy: z.ZodString;
|
|
119
|
+
fromApprover: z.ZodString;
|
|
120
|
+
toApprover: z.ZodString;
|
|
121
|
+
reason: z.ZodString;
|
|
122
|
+
}, z.core.$strip>;
|
|
109
123
|
declare const CancelOptionsSchema: z.ZodObject<{
|
|
110
124
|
cancelledBy: z.ZodString;
|
|
111
125
|
reason: z.ZodString;
|
|
@@ -130,6 +144,7 @@ type SubmitOptions = z.infer<typeof SubmitOptionsSchema>;
|
|
|
130
144
|
type ApproveOptions = z.infer<typeof ApproveOptionsSchema>;
|
|
131
145
|
type RejectOptions = z.infer<typeof RejectOptionsSchema>;
|
|
132
146
|
type DelegateOptions = z.infer<typeof DelegateOptionsSchema>;
|
|
147
|
+
type ReassignOptions = z.infer<typeof ReassignOptionsSchema>;
|
|
133
148
|
type CancelOptions = z.infer<typeof CancelOptionsSchema>;
|
|
134
149
|
type EscalateOptions = z.infer<typeof EscalateOptionsSchema>;
|
|
135
150
|
type ResubmitOptions = z.infer<typeof ResubmitOptionsSchema>;
|
|
@@ -149,6 +164,36 @@ interface Clock {
|
|
|
149
164
|
}
|
|
150
165
|
declare const systemClock: Clock;
|
|
151
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Computes deadline dates from a number of days. The default engine behaviour
|
|
169
|
+
* treats day offsets (escalationAfterDays, slaDeadlineDays) as plain calendar
|
|
170
|
+
* days; provide a BusinessCalendar on ApprovalEngine to interpret them as
|
|
171
|
+
* business days instead — skipping weekends and configured holidays.
|
|
172
|
+
*/
|
|
173
|
+
interface BusinessCalendar {
|
|
174
|
+
/**
|
|
175
|
+
* Return the date that is `days` business days after `from`. A fractional
|
|
176
|
+
* `days` adds whole business days first, then the remaining fraction as
|
|
177
|
+
* elapsed clock time within the resulting business day.
|
|
178
|
+
*/
|
|
179
|
+
addBusinessDays(from: Date, days: number): Date;
|
|
180
|
+
}
|
|
181
|
+
interface WeekendCalendarOptions {
|
|
182
|
+
/** Dates to treat as non-working days (compared by local calendar date). */
|
|
183
|
+
holidays?: Date[];
|
|
184
|
+
/**
|
|
185
|
+
* Weekday numbers (0 = Sunday … 6 = Saturday) that count as weekend.
|
|
186
|
+
* Defaults to [0, 6]. Override for regions with different work weeks
|
|
187
|
+
* (e.g. [5, 6] for a Friday/Saturday weekend).
|
|
188
|
+
*/
|
|
189
|
+
weekendDays?: number[];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A calendar that skips weekends (Sat/Sun by default) and any supplied
|
|
193
|
+
* holidays. Day arithmetic is performed in the host's local timezone.
|
|
194
|
+
*/
|
|
195
|
+
declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
|
|
196
|
+
|
|
152
197
|
type IdGeneratorFn = (prefix: 'inst' | 'tpl') => string;
|
|
153
198
|
declare const defaultIdGenerator: IdGeneratorFn;
|
|
154
199
|
|
|
@@ -224,7 +269,7 @@ interface IAuditAdapter {
|
|
|
224
269
|
append(tenantId: string, instanceId: string, entry: AuditEntry, instance: Readonly<ApprovalInstance>): Promise<void>;
|
|
225
270
|
}
|
|
226
271
|
|
|
227
|
-
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.overridden' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
272
|
+
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
228
273
|
interface IMetricsAdapter {
|
|
229
274
|
/** Increment a counter. Synchronous — never awaited. */
|
|
230
275
|
increment(metric: MetricName, labels?: Record<string, string>): void;
|
|
@@ -245,7 +290,7 @@ interface ISchedulerAdapter {
|
|
|
245
290
|
}
|
|
246
291
|
|
|
247
292
|
interface AuthorizationContext {
|
|
248
|
-
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
|
|
293
|
+
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
|
|
249
294
|
actorId: string;
|
|
250
295
|
instance: Readonly<ApprovalInstance>;
|
|
251
296
|
level?: Readonly<ApprovalLevelInstance>;
|
|
@@ -306,6 +351,16 @@ interface BulkResult {
|
|
|
306
351
|
}>;
|
|
307
352
|
total: number;
|
|
308
353
|
}
|
|
354
|
+
interface ApprovalStatistics {
|
|
355
|
+
/** Total instances matching the filter (across all statuses). */
|
|
356
|
+
total: number;
|
|
357
|
+
/** Count per status. */
|
|
358
|
+
byStatus: Record<ApprovalInstance['status'], number>;
|
|
359
|
+
/** Instances still pending past their escalation/expiry deadline. */
|
|
360
|
+
overdue: number;
|
|
361
|
+
/** approved / (approved + rejected); 0 when nothing has been resolved. */
|
|
362
|
+
approvalRate: number;
|
|
363
|
+
}
|
|
309
364
|
interface HealthResult {
|
|
310
365
|
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
311
366
|
adapter: 'connected' | 'error';
|
|
@@ -331,6 +386,12 @@ interface ApprovalEngineOptions {
|
|
|
331
386
|
maxBulkItems?: number;
|
|
332
387
|
/** Injectable clock — defaults to system clock. Enables deterministic tests and custom time sources. */
|
|
333
388
|
clock?: Clock;
|
|
389
|
+
/**
|
|
390
|
+
* Optional business-day calendar. When provided, escalationAfterDays and
|
|
391
|
+
* slaDeadlineDays are interpreted as business days (skipping weekends and
|
|
392
|
+
* holidays) instead of plain calendar days. See weekendCalendar().
|
|
393
|
+
*/
|
|
394
|
+
calendar?: BusinessCalendar;
|
|
334
395
|
/** Custom ID generator for instances and templates. Defaults to timestamp+random. */
|
|
335
396
|
generateId?: IdGeneratorFn;
|
|
336
397
|
/** Custom optimistic locking retry policy. */
|
|
@@ -359,6 +420,7 @@ declare class ApprovalEngine {
|
|
|
359
420
|
private readonly tenantId;
|
|
360
421
|
private readonly logger;
|
|
361
422
|
private readonly clock;
|
|
423
|
+
private readonly calendar?;
|
|
362
424
|
private readonly generateId;
|
|
363
425
|
private readonly maxBulkItems;
|
|
364
426
|
private readonly retryPolicy;
|
|
@@ -380,6 +442,14 @@ declare class ApprovalEngine {
|
|
|
380
442
|
approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
381
443
|
reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
382
444
|
delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
|
|
445
|
+
/**
|
|
446
|
+
* Administratively replace an approver on the current level. Unlike delegate(),
|
|
447
|
+
* this is performed by a third party (e.g. an admin handling an unavailable
|
|
448
|
+
* approver) and does not require the original approver to initiate it. The
|
|
449
|
+
* target approver must still be pending — an approver who has already acted
|
|
450
|
+
* cannot be reassigned.
|
|
451
|
+
*/
|
|
452
|
+
reassign(instanceId: string, raw: ReassignOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
383
453
|
cancel(instanceId: string, raw: CancelOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
384
454
|
escalate(instanceId: string, raw: EscalateOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
385
455
|
/** Add a comment to an instance without approving or rejecting. */
|
|
@@ -404,6 +474,12 @@ declare class ApprovalEngine {
|
|
|
404
474
|
getCurrentApprovers(instanceId: string): Promise<string[]>;
|
|
405
475
|
/** Check adapter connectivity and escalation scheduler health. */
|
|
406
476
|
healthCheck(): Promise<HealthResult>;
|
|
477
|
+
/**
|
|
478
|
+
* Aggregate counts for dashboards. Accepts an optional filter (documentType,
|
|
479
|
+
* submittedBy, date range) — `status` is ignored since every status is counted.
|
|
480
|
+
* Adapter-agnostic: issues one cheap count query per status plus an overdue scan.
|
|
481
|
+
*/
|
|
482
|
+
getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
|
|
407
483
|
shutdown(): Promise<void>;
|
|
408
484
|
private escalateInternal;
|
|
409
485
|
private expireInstance;
|
|
@@ -411,6 +487,8 @@ declare class ApprovalEngine {
|
|
|
411
487
|
private revertDelegation;
|
|
412
488
|
/** Read-modify-write with optimistic locking retry. */
|
|
413
489
|
private withOptimisticRetry;
|
|
490
|
+
/** Compute a deadline `days` from `from`, honouring the business calendar if one is configured. */
|
|
491
|
+
private deadlineFrom;
|
|
414
492
|
private requireInstance;
|
|
415
493
|
private currentLevelInstance;
|
|
416
494
|
private findNextLevel;
|
|
@@ -423,4 +501,4 @@ declare class ApprovalEngine {
|
|
|
423
501
|
private runExternalAudit;
|
|
424
502
|
}
|
|
425
503
|
|
|
426
|
-
export { type
|
|
504
|
+
export { type ResubmitOptions as $, type AddCommentOptions as A, type BulkResult as B, type Clock as C, type DelegateOptions as D, type EscalateOptions as E, type IMetricsAdapter as F, type INotificationAdapter as G, type HealthResult as H, type IAuditAdapter as I, type IOperationMiddleware as J, type ISchedulerAdapter as K, type Logger as L, type IdGeneratorFn as M, type IdempotencyKeyFn as N, type LevelAdvancedEvent as O, type MetricName as P, type NotificationEvent as Q, type OperationContext as R, type OrgProvider as S, type OverriddenEvent as T, type OverrideOptions as U, type PreviewChainLevel as V, type PreviewResult as W, type ReassignOptions as X, type ReassignedEvent as Y, type RejectOptions as Z, type RejectedEvent as _, ApprovalConflictError as a, type ResubmittedEvent as a0, type RetryPolicy as a1, type SlaBreachedEvent as a2, type SubmitOptions as a3, type SubmittedEvent as a4, type ValidationResult as a5, type WeekendCalendarOptions as a6, defaultIdGenerator as a7, noopLogger as a8, systemClock as a9, weekendCalendar as aa, ApprovalEngine as b, type ApprovalEngineOptions as c, ApprovalError as d, type ApprovalEvent as e, type ApprovalEventMap as f, type ApprovalEventName as g, ApprovalForbiddenError as h, ApprovalNotFoundError as i, type ApprovalStatistics as j, ApprovalTemplateNotFoundError as k, ApprovalValidationError as l, type ApproveOptions as m, type ApprovedEvent as n, type ApproverResolverFn as o, type AuthorizationContext as p, type BusinessCalendar as q, type CanApproveResult as r, type CancelOptions as s, type CancelledEvent as t, type ConditionOperatorFn as u, type DelegatedEvent as v, type EscalatedEvent as w, type ExpiredEvent as x, type HistoryEntry as y, type IAuthorizationPolicy as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as AuditEntry, g as ApprovalLevelInstance, I as IStorageAdapter, q as ResolverFn, j as ApprovalTemplateConfig, A as ApprovalTemplate, m as AuditContext, h as ApprovalMode, P as PaginationOpts, b as PaginatedResult, c as InstanceFilter, C as CursorPaginationOpts, e as CursorPaginatedResult } from './IStorageAdapter-
|
|
1
|
+
import { a as ApprovalInstance, d as AuditEntry, g as ApprovalLevelInstance, I as IStorageAdapter, q as ResolverFn, j as ApprovalTemplateConfig, A as ApprovalTemplate, m as AuditContext, h as ApprovalMode, P as PaginationOpts, b as PaginatedResult, c as InstanceFilter, C as CursorPaginationOpts, e as CursorPaginatedResult } from './IStorageAdapter-B2xGuaKi.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
interface ApprovalEvent {
|
|
@@ -29,6 +29,13 @@ interface DelegatedEvent extends ApprovalEvent {
|
|
|
29
29
|
level: number;
|
|
30
30
|
reason: string;
|
|
31
31
|
}
|
|
32
|
+
interface ReassignedEvent extends ApprovalEvent {
|
|
33
|
+
reassignedBy: string;
|
|
34
|
+
fromApprover: string;
|
|
35
|
+
toApprover: string;
|
|
36
|
+
level: number;
|
|
37
|
+
reason: string;
|
|
38
|
+
}
|
|
32
39
|
interface EscalatedEvent extends ApprovalEvent {
|
|
33
40
|
level: number;
|
|
34
41
|
escalatedTo: string;
|
|
@@ -61,6 +68,7 @@ interface ApprovalEventMap {
|
|
|
61
68
|
'approval:approved': ApprovedEvent;
|
|
62
69
|
'approval:rejected': RejectedEvent;
|
|
63
70
|
'approval:delegated': DelegatedEvent;
|
|
71
|
+
'approval:reassigned': ReassignedEvent;
|
|
64
72
|
'approval:escalated': EscalatedEvent;
|
|
65
73
|
'approval:cancelled': CancelledEvent;
|
|
66
74
|
'approval:completed': ApprovalInstance;
|
|
@@ -106,6 +114,12 @@ declare const DelegateOptionsSchema: z.ZodObject<{
|
|
|
106
114
|
reason: z.ZodString;
|
|
107
115
|
until: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
108
116
|
}, z.core.$strip>;
|
|
117
|
+
declare const ReassignOptionsSchema: z.ZodObject<{
|
|
118
|
+
reassignedBy: z.ZodString;
|
|
119
|
+
fromApprover: z.ZodString;
|
|
120
|
+
toApprover: z.ZodString;
|
|
121
|
+
reason: z.ZodString;
|
|
122
|
+
}, z.core.$strip>;
|
|
109
123
|
declare const CancelOptionsSchema: z.ZodObject<{
|
|
110
124
|
cancelledBy: z.ZodString;
|
|
111
125
|
reason: z.ZodString;
|
|
@@ -130,6 +144,7 @@ type SubmitOptions = z.infer<typeof SubmitOptionsSchema>;
|
|
|
130
144
|
type ApproveOptions = z.infer<typeof ApproveOptionsSchema>;
|
|
131
145
|
type RejectOptions = z.infer<typeof RejectOptionsSchema>;
|
|
132
146
|
type DelegateOptions = z.infer<typeof DelegateOptionsSchema>;
|
|
147
|
+
type ReassignOptions = z.infer<typeof ReassignOptionsSchema>;
|
|
133
148
|
type CancelOptions = z.infer<typeof CancelOptionsSchema>;
|
|
134
149
|
type EscalateOptions = z.infer<typeof EscalateOptionsSchema>;
|
|
135
150
|
type ResubmitOptions = z.infer<typeof ResubmitOptionsSchema>;
|
|
@@ -149,6 +164,36 @@ interface Clock {
|
|
|
149
164
|
}
|
|
150
165
|
declare const systemClock: Clock;
|
|
151
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Computes deadline dates from a number of days. The default engine behaviour
|
|
169
|
+
* treats day offsets (escalationAfterDays, slaDeadlineDays) as plain calendar
|
|
170
|
+
* days; provide a BusinessCalendar on ApprovalEngine to interpret them as
|
|
171
|
+
* business days instead — skipping weekends and configured holidays.
|
|
172
|
+
*/
|
|
173
|
+
interface BusinessCalendar {
|
|
174
|
+
/**
|
|
175
|
+
* Return the date that is `days` business days after `from`. A fractional
|
|
176
|
+
* `days` adds whole business days first, then the remaining fraction as
|
|
177
|
+
* elapsed clock time within the resulting business day.
|
|
178
|
+
*/
|
|
179
|
+
addBusinessDays(from: Date, days: number): Date;
|
|
180
|
+
}
|
|
181
|
+
interface WeekendCalendarOptions {
|
|
182
|
+
/** Dates to treat as non-working days (compared by local calendar date). */
|
|
183
|
+
holidays?: Date[];
|
|
184
|
+
/**
|
|
185
|
+
* Weekday numbers (0 = Sunday … 6 = Saturday) that count as weekend.
|
|
186
|
+
* Defaults to [0, 6]. Override for regions with different work weeks
|
|
187
|
+
* (e.g. [5, 6] for a Friday/Saturday weekend).
|
|
188
|
+
*/
|
|
189
|
+
weekendDays?: number[];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A calendar that skips weekends (Sat/Sun by default) and any supplied
|
|
193
|
+
* holidays. Day arithmetic is performed in the host's local timezone.
|
|
194
|
+
*/
|
|
195
|
+
declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
|
|
196
|
+
|
|
152
197
|
type IdGeneratorFn = (prefix: 'inst' | 'tpl') => string;
|
|
153
198
|
declare const defaultIdGenerator: IdGeneratorFn;
|
|
154
199
|
|
|
@@ -224,7 +269,7 @@ interface IAuditAdapter {
|
|
|
224
269
|
append(tenantId: string, instanceId: string, entry: AuditEntry, instance: Readonly<ApprovalInstance>): Promise<void>;
|
|
225
270
|
}
|
|
226
271
|
|
|
227
|
-
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.overridden' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
272
|
+
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
228
273
|
interface IMetricsAdapter {
|
|
229
274
|
/** Increment a counter. Synchronous — never awaited. */
|
|
230
275
|
increment(metric: MetricName, labels?: Record<string, string>): void;
|
|
@@ -245,7 +290,7 @@ interface ISchedulerAdapter {
|
|
|
245
290
|
}
|
|
246
291
|
|
|
247
292
|
interface AuthorizationContext {
|
|
248
|
-
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
|
|
293
|
+
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
|
|
249
294
|
actorId: string;
|
|
250
295
|
instance: Readonly<ApprovalInstance>;
|
|
251
296
|
level?: Readonly<ApprovalLevelInstance>;
|
|
@@ -306,6 +351,16 @@ interface BulkResult {
|
|
|
306
351
|
}>;
|
|
307
352
|
total: number;
|
|
308
353
|
}
|
|
354
|
+
interface ApprovalStatistics {
|
|
355
|
+
/** Total instances matching the filter (across all statuses). */
|
|
356
|
+
total: number;
|
|
357
|
+
/** Count per status. */
|
|
358
|
+
byStatus: Record<ApprovalInstance['status'], number>;
|
|
359
|
+
/** Instances still pending past their escalation/expiry deadline. */
|
|
360
|
+
overdue: number;
|
|
361
|
+
/** approved / (approved + rejected); 0 when nothing has been resolved. */
|
|
362
|
+
approvalRate: number;
|
|
363
|
+
}
|
|
309
364
|
interface HealthResult {
|
|
310
365
|
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
311
366
|
adapter: 'connected' | 'error';
|
|
@@ -331,6 +386,12 @@ interface ApprovalEngineOptions {
|
|
|
331
386
|
maxBulkItems?: number;
|
|
332
387
|
/** Injectable clock — defaults to system clock. Enables deterministic tests and custom time sources. */
|
|
333
388
|
clock?: Clock;
|
|
389
|
+
/**
|
|
390
|
+
* Optional business-day calendar. When provided, escalationAfterDays and
|
|
391
|
+
* slaDeadlineDays are interpreted as business days (skipping weekends and
|
|
392
|
+
* holidays) instead of plain calendar days. See weekendCalendar().
|
|
393
|
+
*/
|
|
394
|
+
calendar?: BusinessCalendar;
|
|
334
395
|
/** Custom ID generator for instances and templates. Defaults to timestamp+random. */
|
|
335
396
|
generateId?: IdGeneratorFn;
|
|
336
397
|
/** Custom optimistic locking retry policy. */
|
|
@@ -359,6 +420,7 @@ declare class ApprovalEngine {
|
|
|
359
420
|
private readonly tenantId;
|
|
360
421
|
private readonly logger;
|
|
361
422
|
private readonly clock;
|
|
423
|
+
private readonly calendar?;
|
|
362
424
|
private readonly generateId;
|
|
363
425
|
private readonly maxBulkItems;
|
|
364
426
|
private readonly retryPolicy;
|
|
@@ -380,6 +442,14 @@ declare class ApprovalEngine {
|
|
|
380
442
|
approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
381
443
|
reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
382
444
|
delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
|
|
445
|
+
/**
|
|
446
|
+
* Administratively replace an approver on the current level. Unlike delegate(),
|
|
447
|
+
* this is performed by a third party (e.g. an admin handling an unavailable
|
|
448
|
+
* approver) and does not require the original approver to initiate it. The
|
|
449
|
+
* target approver must still be pending — an approver who has already acted
|
|
450
|
+
* cannot be reassigned.
|
|
451
|
+
*/
|
|
452
|
+
reassign(instanceId: string, raw: ReassignOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
383
453
|
cancel(instanceId: string, raw: CancelOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
384
454
|
escalate(instanceId: string, raw: EscalateOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
385
455
|
/** Add a comment to an instance without approving or rejecting. */
|
|
@@ -404,6 +474,12 @@ declare class ApprovalEngine {
|
|
|
404
474
|
getCurrentApprovers(instanceId: string): Promise<string[]>;
|
|
405
475
|
/** Check adapter connectivity and escalation scheduler health. */
|
|
406
476
|
healthCheck(): Promise<HealthResult>;
|
|
477
|
+
/**
|
|
478
|
+
* Aggregate counts for dashboards. Accepts an optional filter (documentType,
|
|
479
|
+
* submittedBy, date range) — `status` is ignored since every status is counted.
|
|
480
|
+
* Adapter-agnostic: issues one cheap count query per status plus an overdue scan.
|
|
481
|
+
*/
|
|
482
|
+
getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
|
|
407
483
|
shutdown(): Promise<void>;
|
|
408
484
|
private escalateInternal;
|
|
409
485
|
private expireInstance;
|
|
@@ -411,6 +487,8 @@ declare class ApprovalEngine {
|
|
|
411
487
|
private revertDelegation;
|
|
412
488
|
/** Read-modify-write with optimistic locking retry. */
|
|
413
489
|
private withOptimisticRetry;
|
|
490
|
+
/** Compute a deadline `days` from `from`, honouring the business calendar if one is configured. */
|
|
491
|
+
private deadlineFrom;
|
|
414
492
|
private requireInstance;
|
|
415
493
|
private currentLevelInstance;
|
|
416
494
|
private findNextLevel;
|
|
@@ -423,4 +501,4 @@ declare class ApprovalEngine {
|
|
|
423
501
|
private runExternalAudit;
|
|
424
502
|
}
|
|
425
503
|
|
|
426
|
-
export { type
|
|
504
|
+
export { type ResubmitOptions as $, type AddCommentOptions as A, type BulkResult as B, type Clock as C, type DelegateOptions as D, type EscalateOptions as E, type IMetricsAdapter as F, type INotificationAdapter as G, type HealthResult as H, type IAuditAdapter as I, type IOperationMiddleware as J, type ISchedulerAdapter as K, type Logger as L, type IdGeneratorFn as M, type IdempotencyKeyFn as N, type LevelAdvancedEvent as O, type MetricName as P, type NotificationEvent as Q, type OperationContext as R, type OrgProvider as S, type OverriddenEvent as T, type OverrideOptions as U, type PreviewChainLevel as V, type PreviewResult as W, type ReassignOptions as X, type ReassignedEvent as Y, type RejectOptions as Z, type RejectedEvent as _, ApprovalConflictError as a, type ResubmittedEvent as a0, type RetryPolicy as a1, type SlaBreachedEvent as a2, type SubmitOptions as a3, type SubmittedEvent as a4, type ValidationResult as a5, type WeekendCalendarOptions as a6, defaultIdGenerator as a7, noopLogger as a8, systemClock as a9, weekendCalendar as aa, ApprovalEngine as b, type ApprovalEngineOptions as c, ApprovalError as d, type ApprovalEvent as e, type ApprovalEventMap as f, type ApprovalEventName as g, ApprovalForbiddenError as h, ApprovalNotFoundError as i, type ApprovalStatistics as j, ApprovalTemplateNotFoundError as k, ApprovalValidationError as l, type ApproveOptions as m, type ApprovedEvent as n, type ApproverResolverFn as o, type AuthorizationContext as p, type BusinessCalendar as q, type CanApproveResult as r, type CancelOptions as s, type CancelledEvent as t, type ConditionOperatorFn as u, type DelegatedEvent as v, type EscalatedEvent as w, type ExpiredEvent as x, type HistoryEntry as y, type IAuthorizationPolicy as z };
|
|
@@ -19,7 +19,7 @@ interface ResolvedApprover {
|
|
|
19
19
|
}
|
|
20
20
|
type ResolverFn = (submittedBy: string, data: Record<string, unknown>) => Promise<string> | string;
|
|
21
21
|
|
|
22
|
-
type ApprovalMode = 'all' | 'any' | 'majority';
|
|
22
|
+
type ApprovalMode = 'all' | 'any' | 'majority' | 'quorum' | 'weighted';
|
|
23
23
|
/** Built-in operators. Use engine.registerConditionOperator() to add custom ones. */
|
|
24
24
|
type ConditionOperator = '>' | '<' | '>=' | '<=' | '==' | '!=' | 'in' | 'not_in' | (string & {});
|
|
25
25
|
interface Condition {
|
|
@@ -33,6 +33,23 @@ interface ApprovalLevelConfig {
|
|
|
33
33
|
approvers: ApproverConfig[];
|
|
34
34
|
mode: ApprovalMode;
|
|
35
35
|
escalationAfterDays?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Required when mode is 'quorum'. The minimum number of approvals needed to
|
|
38
|
+
* pass this level (an N-of-M threshold). The level is rejected as soon as it
|
|
39
|
+
* becomes impossible to reach this count.
|
|
40
|
+
*/
|
|
41
|
+
minApprovals?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Required when mode is 'weighted'. The cumulative approver weight needed to
|
|
44
|
+
* pass this level. The level is rejected once the remaining achievable weight
|
|
45
|
+
* can no longer reach this threshold.
|
|
46
|
+
*/
|
|
47
|
+
threshold?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Optional per-approver voting weights for 'weighted' mode, keyed by approver
|
|
50
|
+
* id. Approvers not listed default to a weight of 1.
|
|
51
|
+
*/
|
|
52
|
+
weights?: Record<string, number>;
|
|
36
53
|
}
|
|
37
54
|
interface ConditionRule {
|
|
38
55
|
when: Condition | Condition[];
|
|
@@ -66,7 +83,7 @@ interface ApprovalTemplate extends ApprovalTemplateConfig {
|
|
|
66
83
|
|
|
67
84
|
type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'cancelled' | 'expired';
|
|
68
85
|
type LevelStatus = 'waiting' | 'pending' | 'approved' | 'rejected' | 'skipped';
|
|
69
|
-
type AuditAction = 'submitted' | 'approved' | 'rejected' | 'delegated' | 'escalated' | 'cancelled' | 'level_advanced' | 'commented' | 'resubmitted' | 'overridden' | 'expired';
|
|
86
|
+
type AuditAction = 'submitted' | 'approved' | 'rejected' | 'delegated' | 'reassigned' | 'escalated' | 'cancelled' | 'level_advanced' | 'commented' | 'resubmitted' | 'overridden' | 'expired';
|
|
70
87
|
interface AuditEntry {
|
|
71
88
|
action: AuditAction;
|
|
72
89
|
actorId: string;
|
|
@@ -98,6 +115,12 @@ interface ApprovalLevelInstance {
|
|
|
98
115
|
approvedBy: string[];
|
|
99
116
|
rejectedBy: string[];
|
|
100
117
|
status: LevelStatus;
|
|
118
|
+
/** Minimum approvals required to pass this level (set when mode is 'quorum'). */
|
|
119
|
+
minApprovals?: number;
|
|
120
|
+
/** Cumulative approver weight required to pass this level (set when mode is 'weighted'). */
|
|
121
|
+
threshold?: number;
|
|
122
|
+
/** Per-approver voting weights for 'weighted' mode; unlisted approvers default to 1. */
|
|
123
|
+
weights?: Record<string, number>;
|
|
101
124
|
escalationDueAt?: Date;
|
|
102
125
|
escalationAfterDays?: number;
|
|
103
126
|
/** Set when a delegation has a time limit — the original approver is restored when this date passes. */
|