hierarchical-approval 2.3.0 → 2.5.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +27 -0
  3. package/dist/{ApprovalEngine-Ci9urTDI.d.ts → ApprovalEngine-BHWls8-z.d.ts} +59 -7
  4. package/dist/{ApprovalEngine-C1dUPnLM.d.cts → ApprovalEngine-CW7Uu1bV.d.cts} +59 -7
  5. package/dist/{IAuditAdapter-C4OASS6u.d.ts → IAuditAdapter-CMCirvY-.d.ts} +1 -1
  6. package/dist/{IAuditAdapter-BHVKMMv6.d.cts → IAuditAdapter-Dwx8ZP4J.d.cts} +1 -1
  7. package/dist/{IAuthorizationPolicy-CtLkwQ8I.d.cts → IAuthorizationPolicy-B3vaV69v.d.cts} +1 -1
  8. package/dist/{IAuthorizationPolicy-BA_m2Dg9.d.ts → IAuthorizationPolicy-BryQIKko.d.ts} +1 -1
  9. package/dist/{INotificationAdapter-JJAiMyMd.d.cts → INotificationAdapter-AwbhZEqz.d.cts} +1 -1
  10. package/dist/{INotificationAdapter-BdgfUrIn.d.ts → INotificationAdapter-BBq2czpO.d.ts} +1 -1
  11. package/dist/{IOperationMiddleware-D0ksiFM3.d.cts → IOperationMiddleware-3J-_NSGq.d.cts} +1 -1
  12. package/dist/{IOperationMiddleware-BGYrAqep.d.ts → IOperationMiddleware-CqzFklMn.d.ts} +1 -1
  13. package/dist/{IStorageAdapter-BU3sau5W.d.ts → IStorageAdapter-Dw2xXP9B.d.ts} +1 -1
  14. package/dist/{IStorageAdapter-DdHO4Rf1.d.cts → IStorageAdapter-Pq9J60xv.d.cts} +1 -1
  15. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  16. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  17. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  18. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  19. package/dist/index.cjs +117 -6
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +8 -8
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +117 -7
  24. package/dist/index.js.map +1 -1
  25. package/dist/{instance-DUJY_Axf.d.cts → instance-BO-i9-nq.d.cts} +13 -0
  26. package/dist/{instance-DUJY_Axf.d.ts → instance-BO-i9-nq.d.ts} +13 -0
  27. package/dist/nestjs.cjs +49 -6
  28. package/dist/nestjs.cjs.map +1 -1
  29. package/dist/nestjs.d.cts +7 -7
  30. package/dist/nestjs.d.ts +7 -7
  31. package/dist/nestjs.js +49 -6
  32. package/dist/nestjs.js.map +1 -1
  33. package/dist/plugins/audit.d.cts +2 -2
  34. package/dist/plugins/audit.d.ts +2 -2
  35. package/dist/plugins/notify.cjs +105 -9
  36. package/dist/plugins/notify.cjs.map +1 -1
  37. package/dist/plugins/notify.d.cts +91 -3
  38. package/dist/plugins/notify.d.ts +91 -3
  39. package/dist/plugins/notify.js +105 -10
  40. package/dist/plugins/notify.js.map +1 -1
  41. package/dist/plugins/resilience.d.cts +3 -3
  42. package/dist/plugins/resilience.d.ts +3 -3
  43. package/dist/plugins/tracing.d.cts +2 -2
  44. package/dist/plugins/tracing.d.ts +2 -2
  45. package/dist/plugins/webhook.d.cts +2 -2
  46. package/dist/plugins/webhook.d.ts +2 -2
  47. package/dist/testing.cjs +49 -6
  48. package/dist/testing.cjs.map +1 -1
  49. package/dist/testing.d.cts +7 -7
  50. package/dist/testing.d.ts +7 -7
  51. package/dist/testing.js +49 -6
  52. package/dist/testing.js.map +1 -1
  53. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,80 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [2.5.0] - 2026-09-04
11
+
12
+ ### Added — deadlines in working hours
13
+
14
+ - **`escalationAfterHours` on a level and `slaDeadlineHours` on a template.**
15
+ Deadlines were whole days, but ERP SLAs are quoted in hours far more often —
16
+ "respond within four working hours" — and approximating that as a fraction of
17
+ a day counted evenings and weekends, so a request submitted at 16:00 on a
18
+ Friday was overdue before anybody could have looked at it.
19
+
20
+ - **`businessHoursCalendar({ workdayStartHour, workdayEndHour, weekendDays,
21
+ holidays })`** advances the clock only through the configured working window.
22
+ A `from` outside working hours is first moved to the next working moment, so a
23
+ deadline never starts counting from a time nobody was at work.
24
+
25
+ ```ts
26
+ const engine = new ApprovalEngine({
27
+ adapter,
28
+ calendar: businessHoursCalendar({ workdayStartHour: 9, workdayEndHour: 17 }),
29
+ });
30
+ // Friday 16:00 + 4 working hours -> Monday 12:00
31
+ ```
32
+
33
+ - **`BusinessCalendar.addBusinessHours` is optional.** `weekendCalendar` knows
34
+ whole days only; given an hour-based deadline the engine falls back to elapsed
35
+ clock time rather than quietly pretending the calendar was applied. A custom
36
+ calendar opts in by implementing the method.
37
+
38
+ - `validateTemplate()` rejects setting both units on the same level or template,
39
+ and non-positive hour values.
40
+
41
+ New exports: `businessHoursCalendar`, `BusinessHoursCalendarOptions`.
42
+
43
+ ## [2.4.0] - 2026-09-04
44
+
45
+ ### Added — `DigestNotificationAdapter`
46
+
47
+ - **Batches notifications per recipient instead of sending one per event.** An
48
+ approver on twenty documents received twenty separate messages a day, which is
49
+ how approval email ends up filtered into a folder nobody reads — the
50
+ notifications defeat themselves.
51
+
52
+ ```ts
53
+ import { DigestNotificationAdapter } from 'hierarchical-approval/plugins/notify';
54
+
55
+ const digest = new DigestNotificationAdapter({
56
+ intervalMs: 15 * 60_000,
57
+ send: async ({ recipient, events }) => mailer.send(recipient, summarise(events)),
58
+ });
59
+ ```
60
+
61
+ - **Urgent events still go straight through.** Batching a rejection or a
62
+ completed approval behind a digest window would make the library's own
63
+ notifications the reason a decision was late, so `approval:rejected`,
64
+ `approval:completed`, `approval:sla_breached` and `approval:expired` bypass
65
+ the buffer by default — configurable via `passthrough`.
66
+
67
+ - **`maxBatchSize`** (default 50) flushes a recipient early under a burst so the
68
+ buffer stays bounded, and flushes only *that* recipient: a burst aimed at one
69
+ person must not force everybody else's digest out early. Omit `intervalMs` to
70
+ disable the timer and drive `flush()` from a cron job or queue worker instead.
71
+
72
+ - A failed `send` is logged and swallowed, as the notification-adapter contract
73
+ requires, and the buffer is cleared before sending so a failure cannot replay
74
+ the same events into every subsequent digest.
75
+
76
+ - Buffers are in memory: a restart drops what has not been flushed. That is the
77
+ right trade for a convenience digest but not for delivery guarantees — put
78
+ `OutboxNotificationAdapter` underneath when an event must not be lost.
79
+
80
+ New exports from `hierarchical-approval/plugins/notify`:
81
+ `DigestNotificationAdapter`, `DigestNotificationAdapterOptions`, `Digest`,
82
+ `DigestSendFn`.
83
+
10
84
  ## [2.3.0] - 2026-09-04
11
85
 
12
86
  ### Added — retention
package/README.md CHANGED
@@ -468,6 +468,33 @@ The sweep is **not atomic**: it reports per-instance failures rather than rollin
468
468
  back. A partial transfer is the useful outcome — the approvals that can move
469
469
  should move, and the ones that cannot are named so a human can look at them.
470
470
 
471
+ ### SLAs in working hours
472
+
473
+ Deadlines were whole days, so "respond within four working hours" could only be
474
+ approximated. Levels take `escalationAfterHours` and templates take
475
+ `slaDeadlineHours`, counted through a working-hours calendar:
476
+
477
+ ```ts
478
+ import { businessHoursCalendar } from 'hierarchical-approval';
479
+
480
+ const engine = new ApprovalEngine({
481
+ adapter,
482
+ calendar: businessHoursCalendar({ workdayStartHour: 9, workdayEndHour: 17 }),
483
+ });
484
+
485
+ // Submitted Friday 16:00, escalationAfterHours: 4 -> due Monday 12:00
486
+ ```
487
+
488
+ Evenings, weekends and holidays are skipped, and a request arriving outside
489
+ working hours starts counting from the next working moment — so one submitted at
490
+ 20:00 is not already overdue by morning.
491
+
492
+ `weekendCalendar` knows whole days only. Give it an hour-based deadline and the
493
+ engine falls back to **elapsed clock time**, rather than quietly pretending the
494
+ calendar applied; use `businessHoursCalendar` when hours should skip
495
+ non-working time. Days and hours are mutually exclusive on the same level or
496
+ template, and `validateTemplate()` rejects setting both.
497
+
471
498
  ### Retention
472
499
 
473
500
  Approval tables only grow, and data-minimisation rules eventually require old
@@ -1,14 +1,14 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-BU3sau5W.js';
2
- import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-DUJY_Axf.js';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BdgfUrIn.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-Dw2xXP9B.js';
2
+ import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BO-i9-nq.js';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BBq2czpO.js';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.js';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.js';
7
- import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BGYrAqep.js';
8
- import { I as IAuditAdapter } from './IAuditAdapter-C4OASS6u.js';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-CqzFklMn.js';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-CMCirvY-.js';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-DWq8IFaf.js';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BA_m2Dg9.js';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BryQIKko.js';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -141,6 +141,16 @@ interface BusinessCalendar {
141
141
  * elapsed clock time within the resulting business day.
142
142
  */
143
143
  addBusinessDays(from: Date, days: number): Date;
144
+ /**
145
+ * Return the date that is `hours` **working** hours after `from`, skipping
146
+ * non-working hours, weekends and holidays.
147
+ *
148
+ * Optional. A calendar that only knows about whole days — such as
149
+ * {@link weekendCalendar} — omits it, and the engine falls back to elapsed
150
+ * clock time for any hour-based deadline rather than silently pretending the
151
+ * calendar was applied.
152
+ */
153
+ addBusinessHours?(from: Date, hours: number): Date;
144
154
  }
145
155
  interface WeekendCalendarOptions {
146
156
  /** Dates to treat as non-working days (compared by local calendar date). */
@@ -157,6 +167,37 @@ interface WeekendCalendarOptions {
157
167
  * holidays. Day arithmetic is performed in the host's local timezone.
158
168
  */
159
169
  declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
170
+ interface BusinessHoursCalendarOptions extends WeekendCalendarOptions {
171
+ /** First working hour of the day, 0-23 in local time. Defaults to 9. */
172
+ workdayStartHour?: number;
173
+ /**
174
+ * Hour at which the working day ends, 0-24 in local time. Defaults to 17.
175
+ * Must be greater than {@link workdayStartHour}.
176
+ */
177
+ workdayEndHour?: number;
178
+ }
179
+ /**
180
+ * A calendar that counts only working hours within working days.
181
+ *
182
+ * SLAs in an ERP are quoted in hours far more often than days — "respond within
183
+ * four working hours" — and counting those as elapsed clock time makes a
184
+ * request submitted at 16:00 on a Friday overdue before anybody could have
185
+ * looked at it. This advances the clock only through the configured working
186
+ * window, skipping evenings, weekends and holidays.
187
+ *
188
+ * A `from` outside working hours is first moved forward to the next working
189
+ * moment, so a deadline never starts counting from a time nobody was at work.
190
+ *
191
+ * Arithmetic is performed in the host's local timezone, matching
192
+ * {@link weekendCalendar}.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * const calendar = businessHoursCalendar({ workdayStartHour: 9, workdayEndHour: 17 });
197
+ * // Friday 16:00 + 4 working hours -> Monday 12:00
198
+ * ```
199
+ */
200
+ declare function businessHoursCalendar(options?: BusinessHoursCalendarOptions): Required<Pick<BusinessCalendar, 'addBusinessDays' | 'addBusinessHours'>>;
160
201
 
161
202
  type IdGeneratorPrefix = 'inst' | 'tpl' | 'att';
162
203
  type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
@@ -800,6 +841,17 @@ declare class ApprovalEngine {
800
841
  private withOptimisticRetry;
801
842
  /** Compute a deadline `days` from `from`, honouring the business calendar if one is configured. */
802
843
  private deadlineFrom;
844
+ /**
845
+ * Resolve an hour-based deadline.
846
+ *
847
+ * A calendar that only knows whole days — `weekendCalendar`, say — has no
848
+ * `addBusinessHours`, and this falls back to elapsed clock time rather than
849
+ * quietly pretending the calendar was applied. Configure
850
+ * `businessHoursCalendar` to have hours skip evenings and weekends.
851
+ */
852
+ private deadlineFromHours;
853
+ /** Level deadline from whichever of days/hours the template configured. */
854
+ private levelEscalationDue;
803
855
  private requireInstance;
804
856
  /**
805
857
  * Identity of the parallel branch group a level belongs to.
@@ -890,4 +942,4 @@ declare class ApprovalEngine {
890
942
  private runExternalAudit;
891
943
  }
892
944
 
893
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, weekendCalendar as F, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, TEMPLATE_BUNDLE_VERSION as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type ApproverWorkload as f, type BusinessCalendar as g, type CancelOptions as h, type ConditionOperatorFn as i, type CycleTimeStats as j, type IdempotencyKeyFn as k, type ImportResult as l, type OutOfOfficeProvider as m, type OverrideOptions as n, type PreviewResult as o, type ProvideInfoOptions as p, type PurgeResult as q, type RejectOptions as r, type RequestInfoOptions as s, type ResubmitOptions as t, type RetryPolicy as u, type TemplateBundle as v, type TransferResult as w, defaultIdGenerator as x, toComparableNumber as y, validateConditionExpression as z };
945
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, toComparableNumber as F, validateConditionExpression as G, type HealthResult as H, type IdGeneratorFn as I, weekendCalendar as J, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, TEMPLATE_BUNDLE_VERSION as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type ApproverWorkload as f, type BusinessCalendar as g, type BusinessHoursCalendarOptions as h, type CancelOptions as i, type ConditionOperatorFn as j, type CycleTimeStats as k, type IdempotencyKeyFn as l, type ImportResult as m, type OutOfOfficeProvider as n, type OverrideOptions as o, type PreviewResult as p, type ProvideInfoOptions as q, type PurgeResult as r, type RejectOptions as s, type RequestInfoOptions as t, type ResubmitOptions as u, type RetryPolicy as v, type TemplateBundle as w, type TransferResult as x, businessHoursCalendar as y, defaultIdGenerator as z };
@@ -1,14 +1,14 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-DdHO4Rf1.cjs';
2
- import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-DUJY_Axf.cjs';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-JJAiMyMd.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-Pq9J60xv.cjs';
2
+ import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BO-i9-nq.cjs';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-AwbhZEqz.cjs';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.cjs';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.cjs';
7
- import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-D0ksiFM3.cjs';
8
- import { I as IAuditAdapter } from './IAuditAdapter-BHVKMMv6.cjs';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-3J-_NSGq.cjs';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-Dwx8ZP4J.cjs';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-DWq8IFaf.cjs';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CtLkwQ8I.cjs';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-B3vaV69v.cjs';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -141,6 +141,16 @@ interface BusinessCalendar {
141
141
  * elapsed clock time within the resulting business day.
142
142
  */
143
143
  addBusinessDays(from: Date, days: number): Date;
144
+ /**
145
+ * Return the date that is `hours` **working** hours after `from`, skipping
146
+ * non-working hours, weekends and holidays.
147
+ *
148
+ * Optional. A calendar that only knows about whole days — such as
149
+ * {@link weekendCalendar} — omits it, and the engine falls back to elapsed
150
+ * clock time for any hour-based deadline rather than silently pretending the
151
+ * calendar was applied.
152
+ */
153
+ addBusinessHours?(from: Date, hours: number): Date;
144
154
  }
145
155
  interface WeekendCalendarOptions {
146
156
  /** Dates to treat as non-working days (compared by local calendar date). */
@@ -157,6 +167,37 @@ interface WeekendCalendarOptions {
157
167
  * holidays. Day arithmetic is performed in the host's local timezone.
158
168
  */
159
169
  declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
170
+ interface BusinessHoursCalendarOptions extends WeekendCalendarOptions {
171
+ /** First working hour of the day, 0-23 in local time. Defaults to 9. */
172
+ workdayStartHour?: number;
173
+ /**
174
+ * Hour at which the working day ends, 0-24 in local time. Defaults to 17.
175
+ * Must be greater than {@link workdayStartHour}.
176
+ */
177
+ workdayEndHour?: number;
178
+ }
179
+ /**
180
+ * A calendar that counts only working hours within working days.
181
+ *
182
+ * SLAs in an ERP are quoted in hours far more often than days — "respond within
183
+ * four working hours" — and counting those as elapsed clock time makes a
184
+ * request submitted at 16:00 on a Friday overdue before anybody could have
185
+ * looked at it. This advances the clock only through the configured working
186
+ * window, skipping evenings, weekends and holidays.
187
+ *
188
+ * A `from` outside working hours is first moved forward to the next working
189
+ * moment, so a deadline never starts counting from a time nobody was at work.
190
+ *
191
+ * Arithmetic is performed in the host's local timezone, matching
192
+ * {@link weekendCalendar}.
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * const calendar = businessHoursCalendar({ workdayStartHour: 9, workdayEndHour: 17 });
197
+ * // Friday 16:00 + 4 working hours -> Monday 12:00
198
+ * ```
199
+ */
200
+ declare function businessHoursCalendar(options?: BusinessHoursCalendarOptions): Required<Pick<BusinessCalendar, 'addBusinessDays' | 'addBusinessHours'>>;
160
201
 
161
202
  type IdGeneratorPrefix = 'inst' | 'tpl' | 'att';
162
203
  type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
@@ -800,6 +841,17 @@ declare class ApprovalEngine {
800
841
  private withOptimisticRetry;
801
842
  /** Compute a deadline `days` from `from`, honouring the business calendar if one is configured. */
802
843
  private deadlineFrom;
844
+ /**
845
+ * Resolve an hour-based deadline.
846
+ *
847
+ * A calendar that only knows whole days — `weekendCalendar`, say — has no
848
+ * `addBusinessHours`, and this falls back to elapsed clock time rather than
849
+ * quietly pretending the calendar was applied. Configure
850
+ * `businessHoursCalendar` to have hours skip evenings and weekends.
851
+ */
852
+ private deadlineFromHours;
853
+ /** Level deadline from whichever of days/hours the template configured. */
854
+ private levelEscalationDue;
803
855
  private requireInstance;
804
856
  /**
805
857
  * Identity of the parallel branch group a level belongs to.
@@ -890,4 +942,4 @@ declare class ApprovalEngine {
890
942
  private runExternalAudit;
891
943
  }
892
944
 
893
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, weekendCalendar as F, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, TEMPLATE_BUNDLE_VERSION as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type ApproverWorkload as f, type BusinessCalendar as g, type CancelOptions as h, type ConditionOperatorFn as i, type CycleTimeStats as j, type IdempotencyKeyFn as k, type ImportResult as l, type OutOfOfficeProvider as m, type OverrideOptions as n, type PreviewResult as o, type ProvideInfoOptions as p, type PurgeResult as q, type RejectOptions as r, type RequestInfoOptions as s, type ResubmitOptions as t, type RetryPolicy as u, type TemplateBundle as v, type TransferResult as w, defaultIdGenerator as x, toComparableNumber as y, validateConditionExpression as z };
945
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, toComparableNumber as F, validateConditionExpression as G, type HealthResult as H, type IdGeneratorFn as I, weekendCalendar as J, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, TEMPLATE_BUNDLE_VERSION as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type ApproverWorkload as f, type BusinessCalendar as g, type BusinessHoursCalendarOptions as h, type CancelOptions as i, type ConditionOperatorFn as j, type CycleTimeStats as k, type IdempotencyKeyFn as l, type ImportResult as m, type OutOfOfficeProvider as n, type OverrideOptions as o, type PreviewResult as p, type ProvideInfoOptions as q, type PurgeResult as r, type RejectOptions as s, type RequestInfoOptions as t, type ResubmitOptions as u, type RetryPolicy as v, type TemplateBundle as w, type TransferResult as x, businessHoursCalendar as y, defaultIdGenerator as z };
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-DUJY_Axf.js';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-BO-i9-nq.js';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-DUJY_Axf.cjs';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-BO-i9-nq.cjs';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-DUJY_Axf.cjs';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BO-i9-nq.cjs';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-DUJY_Axf.js';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BO-i9-nq.js';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-DUJY_Axf.cjs';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-BO-i9-nq.cjs';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-DUJY_Axf.js';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-BO-i9-nq.js';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-DUJY_Axf.cjs';
1
+ import { a as ApprovalInstance } from './instance-BO-i9-nq.cjs';
2
2
 
3
3
  declare class ApprovalError extends Error {
4
4
  readonly code: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-DUJY_Axf.js';
1
+ import { a as ApprovalInstance } from './instance-BO-i9-nq.js';
2
2
 
3
3
  declare class ApprovalError extends Error {
4
4
  readonly code: string;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-DUJY_Axf.js';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BO-i9-nq.js';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-DUJY_Axf.cjs';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BO-i9-nq.cjs';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -1,5 +1,5 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DdHO4Rf1.cjs';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DUJY_Axf.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Pq9J60xv.cjs';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BO-i9-nq.cjs';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -1,5 +1,5 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-BU3sau5W.js';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DUJY_Axf.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Dw2xXP9B.js';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BO-i9-nq.js';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -1,7 +1,7 @@
1
1
  import * as tls from 'tls';
2
2
  import * as pg from 'pg';
3
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DdHO4Rf1.cjs';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DUJY_Axf.cjs';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Pq9J60xv.cjs';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BO-i9-nq.cjs';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;
@@ -1,7 +1,7 @@
1
1
  import * as tls from 'tls';
2
2
  import * as pg from 'pg';
3
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-BU3sau5W.js';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DUJY_Axf.js';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Dw2xXP9B.js';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BO-i9-nq.js';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;
package/dist/index.cjs CHANGED
@@ -1071,6 +1071,18 @@ var ApprovalEngine = class _ApprovalEngine {
1071
1071
  message: `Level ${l.level} sets reminderEveryDays without reminderAfterDays, so no reminder would ever be sent.`
1072
1072
  });
1073
1073
  }
1074
+ if (l.escalationAfterHours !== void 0 && l.escalationAfterHours <= 0) {
1075
+ errors.push({
1076
+ field: `levels[${i}].escalationAfterHours`,
1077
+ message: `Level ${l.level} escalationAfterHours must be a positive number.`
1078
+ });
1079
+ }
1080
+ if (l.escalationAfterDays !== void 0 && l.escalationAfterHours !== void 0) {
1081
+ errors.push({
1082
+ field: `levels[${i}].escalationAfterHours`,
1083
+ message: `Level ${l.level} sets both escalationAfterDays and escalationAfterHours; pick one so the deadline is unambiguous.`
1084
+ });
1085
+ }
1074
1086
  if (l.escalationAfterDays !== void 0 && l.escalationAfterDays <= 0) {
1075
1087
  errors.push({
1076
1088
  field: `levels[${i}].escalationAfterDays`,
@@ -1135,6 +1147,18 @@ var ApprovalEngine = class _ApprovalEngine {
1135
1147
  }
1136
1148
  });
1137
1149
  }
1150
+ if (config.slaDeadlineDays !== void 0 && config.slaDeadlineHours !== void 0) {
1151
+ errors.push({
1152
+ field: "slaDeadlineHours",
1153
+ message: "Template sets both slaDeadlineDays and slaDeadlineHours; pick one so the SLA is unambiguous."
1154
+ });
1155
+ }
1156
+ if (config.slaDeadlineHours !== void 0 && config.slaDeadlineHours <= 0) {
1157
+ errors.push({
1158
+ field: "slaDeadlineHours",
1159
+ message: "slaDeadlineHours must be a positive number."
1160
+ });
1161
+ }
1138
1162
  if (config.conditions) {
1139
1163
  config.conditions.forEach((rule, ruleIdx) => {
1140
1164
  errors.push(...validateConditionExpression(rule.when, `conditions[${ruleIdx}].when`));
@@ -1248,7 +1272,8 @@ var ApprovalEngine = class _ApprovalEngine {
1248
1272
  threshold: cfg.threshold,
1249
1273
  weights: cfg.weights,
1250
1274
  escalationAfterDays: cfg.escalationAfterDays,
1251
- escalationDueAt: inFirstGroup && cfg.escalationAfterDays ? this.deadlineFrom(now, cfg.escalationAfterDays) : void 0,
1275
+ escalationAfterHours: cfg.escalationAfterHours,
1276
+ escalationDueAt: inFirstGroup ? this.levelEscalationDue(now, cfg) : void 0,
1252
1277
  subWorkflowTemplate: cfg.subWorkflow?.templateName,
1253
1278
  reminderAfterDays: cfg.reminderAfterDays,
1254
1279
  reminderEveryDays: cfg.reminderEveryDays,
@@ -1278,7 +1303,7 @@ var ApprovalEngine = class _ApprovalEngine {
1278
1303
  timestamp: now,
1279
1304
  ...auditCtx
1280
1305
  };
1281
- const slaDeadlineAt = template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
1306
+ const slaDeadlineAt = template.slaDeadlineHours ? this.deadlineFromHours(now, template.slaDeadlineHours) : template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
1282
1307
  const instance = {
1283
1308
  id: instanceId,
1284
1309
  tenantId: this.tenantId,
@@ -1306,6 +1331,7 @@ var ApprovalEngine = class _ApprovalEngine {
1306
1331
  templateSnapshot: {
1307
1332
  escalation: template.escalation,
1308
1333
  slaDeadlineDays: template.slaDeadlineDays,
1334
+ slaDeadlineHours: template.slaDeadlineHours,
1309
1335
  allowOverride: template.allowOverride
1310
1336
  }
1311
1337
  };
@@ -2577,7 +2603,7 @@ var ApprovalEngine = class _ApprovalEngine {
2577
2603
  reason: opts.reason,
2578
2604
  ...auditCtx
2579
2605
  };
2580
- const slaDeadlineAt = template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
2606
+ const slaDeadlineAt = template.slaDeadlineHours ? this.deadlineFromHours(now, template.slaDeadlineHours) : template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
2581
2607
  const newInstance = {
2582
2608
  id: newInstanceId,
2583
2609
  tenantId: this.tenantId,
@@ -2600,6 +2626,7 @@ var ApprovalEngine = class _ApprovalEngine {
2600
2626
  templateSnapshot: {
2601
2627
  escalation: template.escalation,
2602
2628
  slaDeadlineDays: template.slaDeadlineDays,
2629
+ slaDeadlineHours: template.slaDeadlineHours,
2603
2630
  allowOverride: template.allowOverride
2604
2631
  }
2605
2632
  };
@@ -3542,6 +3569,24 @@ var ApprovalEngine = class _ApprovalEngine {
3542
3569
  deadlineFrom(from, days) {
3543
3570
  return this.calendar ? this.calendar.addBusinessDays(from, days) : new Date(from.getTime() + days * 864e5);
3544
3571
  }
3572
+ /**
3573
+ * Resolve an hour-based deadline.
3574
+ *
3575
+ * A calendar that only knows whole days — `weekendCalendar`, say — has no
3576
+ * `addBusinessHours`, and this falls back to elapsed clock time rather than
3577
+ * quietly pretending the calendar was applied. Configure
3578
+ * `businessHoursCalendar` to have hours skip evenings and weekends.
3579
+ */
3580
+ deadlineFromHours(from, hours) {
3581
+ const addHours = this.calendar?.addBusinessHours?.bind(this.calendar);
3582
+ return addHours ? addHours(from, hours) : new Date(from.getTime() + hours * 36e5);
3583
+ }
3584
+ /** Level deadline from whichever of days/hours the template configured. */
3585
+ levelEscalationDue(from, level) {
3586
+ if (level.escalationAfterHours) return this.deadlineFromHours(from, level.escalationAfterHours);
3587
+ if (level.escalationAfterDays) return this.deadlineFrom(from, level.escalationAfterDays);
3588
+ return void 0;
3589
+ }
3545
3590
  async requireInstance(id) {
3546
3591
  const instance = await this.opts.adapter.getInstance(this.tenantId, id);
3547
3592
  if (!instance) throw new ApprovalNotFoundError("Instance", id);
@@ -3647,9 +3692,7 @@ var ApprovalEngine = class _ApprovalEngine {
3647
3692
  now
3648
3693
  );
3649
3694
  }
3650
- if (lvl.escalationAfterDays) {
3651
- lvl.escalationDueAt = this.deadlineFrom(now, lvl.escalationAfterDays);
3652
- }
3695
+ lvl.escalationDueAt = this.levelEscalationDue(now, lvl);
3653
3696
  this.scheduleReminder(lvl, now);
3654
3697
  lvl.status = "pending";
3655
3698
  }
@@ -4290,6 +4333,73 @@ function weekendCalendar(options = {}) {
4290
4333
  }
4291
4334
  };
4292
4335
  }
4336
+ var HOUR_MS = 36e5;
4337
+ function businessHoursCalendar(options = {}) {
4338
+ const startHour = options.workdayStartHour ?? 9;
4339
+ const endHour = options.workdayEndHour ?? 17;
4340
+ if (!(endHour > startHour)) {
4341
+ throw new Error(
4342
+ `businessHoursCalendar: workdayEndHour (${endHour}) must be greater than workdayStartHour (${startHour}).`
4343
+ );
4344
+ }
4345
+ const weekend = new Set(options.weekendDays ?? [0, 6]);
4346
+ const holidays = new Set((options.holidays ?? []).map(dayKey));
4347
+ const isBusinessDay = (d) => !weekend.has(d.getDay()) && !holidays.has(dayKey(d));
4348
+ const startOfWorkday = (d) => {
4349
+ const out = new Date(d.getTime());
4350
+ out.setHours(startHour, 0, 0, 0);
4351
+ return out;
4352
+ };
4353
+ const endOfWorkday = (d) => {
4354
+ const out = new Date(d.getTime());
4355
+ out.setHours(endHour, 0, 0, 0);
4356
+ return out;
4357
+ };
4358
+ const toWorkingMoment = (from) => {
4359
+ const cursor = new Date(from.getTime());
4360
+ for (let guard = 0; guard < 3660; guard++) {
4361
+ if (!isBusinessDay(cursor)) {
4362
+ cursor.setDate(cursor.getDate() + 1);
4363
+ cursor.setHours(startHour, 0, 0, 0);
4364
+ continue;
4365
+ }
4366
+ if (cursor.getTime() < startOfWorkday(cursor).getTime()) return startOfWorkday(cursor);
4367
+ if (cursor.getTime() >= endOfWorkday(cursor).getTime()) {
4368
+ cursor.setDate(cursor.getDate() + 1);
4369
+ cursor.setHours(startHour, 0, 0, 0);
4370
+ continue;
4371
+ }
4372
+ return cursor;
4373
+ }
4374
+ throw new Error(
4375
+ "businessHoursCalendar: no working day found within 10 years of the start date."
4376
+ );
4377
+ };
4378
+ const base = weekendCalendar(options);
4379
+ return {
4380
+ addBusinessDays: base.addBusinessDays,
4381
+ addBusinessHours(from, hours) {
4382
+ if (hours <= 0 || Number.isNaN(hours)) return new Date(from.getTime());
4383
+ let cursor = toWorkingMoment(from);
4384
+ let remainingMs = Math.round(hours * HOUR_MS);
4385
+ for (let guard = 0; remainingMs > 0 && guard < 3660; guard++) {
4386
+ const dayEnd = endOfWorkday(cursor);
4387
+ const availableMs = dayEnd.getTime() - cursor.getTime();
4388
+ if (remainingMs <= availableMs) {
4389
+ return new Date(cursor.getTime() + remainingMs);
4390
+ }
4391
+ remainingMs -= availableMs;
4392
+ const next = new Date(cursor.getTime());
4393
+ next.setDate(next.getDate() + 1);
4394
+ next.setHours(startHour, 0, 0, 0);
4395
+ cursor = toWorkingMoment(next);
4396
+ }
4397
+ throw new Error(
4398
+ `businessHoursCalendar: ${hours} working hours could not be scheduled within 10 years \u2014 check weekendDays and holidays.`
4399
+ );
4400
+ }
4401
+ };
4402
+ }
4293
4403
 
4294
4404
  exports.ApprovalConflictError = ApprovalConflictError;
4295
4405
  exports.ApprovalEngine = ApprovalEngine;
@@ -4301,6 +4411,7 @@ exports.ApprovalValidationError = ApprovalValidationError;
4301
4411
  exports.EscalationScheduler = EscalationScheduler;
4302
4412
  exports.MemoryAdapter = MemoryAdapter;
4303
4413
  exports.TEMPLATE_BUNDLE_VERSION = TEMPLATE_BUNDLE_VERSION;
4414
+ exports.businessHoursCalendar = businessHoursCalendar;
4304
4415
  exports.defaultIdGenerator = defaultIdGenerator;
4305
4416
  exports.noopLogger = noopLogger;
4306
4417
  exports.systemClock = systemClock;