@rytass/bpm-core-nestjs-module 0.5.1 → 0.7.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 (46) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +2 -0
  3. package/package.json +10 -3
  4. package/src/index.d.ts +1 -0
  5. package/src/index.js +1 -0
  6. package/src/index.js.map +1 -1
  7. package/src/lib/bpm/bpm-root.module.d.ts +18 -0
  8. package/src/lib/bpm/bpm-root.module.js +8 -0
  9. package/src/lib/bpm/bpm-root.module.js.map +1 -1
  10. package/src/lib/calendar/business-calendar.provider.d.ts +11 -0
  11. package/src/lib/calendar/business-calendar.provider.js +20 -0
  12. package/src/lib/calendar/business-calendar.provider.js.map +1 -0
  13. package/src/lib/calendar/business-calendar.token.d.ts +38 -0
  14. package/src/lib/calendar/business-calendar.token.js +5 -0
  15. package/src/lib/calendar/business-calendar.token.js.map +1 -0
  16. package/src/lib/calendar/calendar.module.d.ts +18 -0
  17. package/src/lib/calendar/calendar.module.js +32 -0
  18. package/src/lib/calendar/calendar.module.js.map +1 -0
  19. package/src/lib/calendar/index.d.ts +5 -0
  20. package/src/lib/calendar/index.js +9 -0
  21. package/src/lib/calendar/index.js.map +1 -0
  22. package/src/lib/calendar/sla-schedule.service.d.ts +33 -0
  23. package/src/lib/calendar/sla-schedule.service.js +94 -0
  24. package/src/lib/calendar/sla-schedule.service.js.map +1 -0
  25. package/src/lib/calendar/weekday-business-calendar.d.ts +15 -0
  26. package/src/lib/calendar/weekday-business-calendar.js +28 -0
  27. package/src/lib/calendar/weekday-business-calendar.js.map +1 -0
  28. package/src/lib/common/iso-duration.d.ts +11 -0
  29. package/src/lib/common/iso-duration.js +13 -4
  30. package/src/lib/common/iso-duration.js.map +1 -1
  31. package/src/lib/notification/notification-options.d.ts +10 -0
  32. package/src/lib/notification/notification-options.js +19 -0
  33. package/src/lib/notification/notification-options.js.map +1 -1
  34. package/src/lib/notification/notification.enums.d.ts +13 -0
  35. package/src/lib/notification/notification.enums.js +14 -1
  36. package/src/lib/notification/notification.enums.js.map +1 -1
  37. package/src/lib/notification/notification.service.d.ts +8 -0
  38. package/src/lib/notification/notification.service.js +19 -1
  39. package/src/lib/notification/notification.service.js.map +1 -1
  40. package/src/lib/template/workflow-definition.validator.js +75 -1
  41. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  42. package/src/lib/workflow-engine/workflow-engine.service.d.ts +21 -3
  43. package/src/lib/workflow-engine/workflow-engine.service.js +115 -41
  44. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
  45. package/src/lib/workflow-engine/workflow-engine.tokens.d.ts +2 -1
  46. package/src/lib/workflow-engine/workflow-engine.tokens.js.map +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WorkflowEngineService = void 0;
3
+ exports.WorkflowEngineService = exports.MANUAL_TRANSFER_DELEGATION_REASON = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@nestjs/common");
6
6
  const typeorm_1 = require("@nestjs/typeorm");
@@ -9,6 +9,7 @@ const attachment_service_1 = require("../attachment/attachment.service");
9
9
  const delegation_service_1 = require("../delegation/delegation.service");
10
10
  const form_definition_version_entity_1 = require("../form/form-definition-version.entity");
11
11
  const form_enums_1 = require("../form/form.enums");
12
+ const sla_schedule_service_1 = require("../calendar/sla-schedule.service");
12
13
  const notification_service_1 = require("../notification/notification.service");
13
14
  const notification_entity_1 = require("../notification/notification.entity");
14
15
  const notification_enums_1 = require("../notification/notification.enums");
@@ -36,6 +37,11 @@ const workflow_token_entity_1 = require("./workflow-token.entity");
36
37
  const MAX_PROCESSING_STEPS = 500;
37
38
  const DEFAULT_APPROVAL_HISTORY_TASK_LIMIT = 50;
38
39
  const SYSTEM_DRY_RUN_INSTANCE_ID = 'dry-run-instance';
40
+ /**
41
+ * Delegation-chain reason recorded for an approver-initiated transfer, as
42
+ * opposed to {@link SLA_ESCALATION_DELEGATION_REASON}.
43
+ */
44
+ exports.MANUAL_TRANSFER_DELEGATION_REASON = 'MANUAL_TRANSFER';
39
45
  const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
40
46
  'bpm:*',
41
47
  'bpm.workflow.read_all',
@@ -43,7 +49,7 @@ const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
43
49
  'workflow.read_all',
44
50
  ]);
45
51
  let WorkflowEngineService = class WorkflowEngineService {
46
- constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, adhocDirectiveRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, serviceTaskDispatcher = new workflow_service_task_dispatcher_token_1.DefaultWorkflowServiceTaskDispatcher()) {
52
+ constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, adhocDirectiveRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, slaScheduleService, serviceTaskDispatcher = new workflow_service_task_dispatcher_token_1.DefaultWorkflowServiceTaskDispatcher()) {
47
53
  this.approvalInstanceRepository = approvalInstanceRepository;
48
54
  this.workflowTokenRepository = workflowTokenRepository;
49
55
  this.taskRepository = taskRepository;
@@ -60,6 +66,7 @@ let WorkflowEngineService = class WorkflowEngineService {
60
66
  this.delegationService = delegationService;
61
67
  this.notificationService = notificationService;
62
68
  this.signatureService = signatureService;
69
+ this.slaScheduleService = slaScheduleService;
63
70
  this.serviceTaskDispatcher = serviceTaskDispatcher;
64
71
  }
65
72
  async submitApprovalInstance(input) {
@@ -169,7 +176,7 @@ let WorkflowEngineService = class WorkflowEngineService {
169
176
  await this.processRunningInstance(manager, instance);
170
177
  });
171
178
  }
172
- async decideTask(input) {
179
+ async decideTask(input, options = {}) {
173
180
  return this.approvalInstanceRepository.manager.transaction(async (manager) => {
174
181
  const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
175
182
  const taskCandidateRepository = manager.getRepository(task_candidate_entity_1.TaskCandidateEntity);
@@ -223,6 +230,14 @@ let WorkflowEngineService = class WorkflowEngineService {
223
230
  const returnToNodeId = input.action === workflow_engine_enums_1.TaskDecisionActionEnum.RETURNED
224
231
  ? readReturnTargetNodeId(instance.workflowSnapshot, task.nodeId, input.returnToNodeId)
225
232
  : null;
233
+ // Read from the instance snapshot, not the live template: templates
234
+ // that turn `requireComment` on later must not retroactively block
235
+ // in-flight instances started against the previous version.
236
+ if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.RETURNED &&
237
+ !decisionComment &&
238
+ isReturnCommentRequired(instance.workflowSnapshot, task.nodeId)) {
239
+ throw new common_1.BadRequestException(`Return decision comment is required by workflow node ${task.nodeId}`);
240
+ }
226
241
  const decidedAt = new Date();
227
242
  const claimedTask = await taskRepository.save({
228
243
  ...task,
@@ -261,7 +276,7 @@ let WorkflowEngineService = class WorkflowEngineService {
261
276
  transferToMemberId,
262
277
  }));
263
278
  if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.TRANSFERRED) {
264
- await this.transferTask(manager, instance, claimedTask, decision, signature, transferToMemberId, decisionComment, decidedAt, claimedCandidate);
279
+ await this.transferTask(manager, instance, claimedTask, decision, signature, transferToMemberId, decisionComment, decidedAt, claimedCandidate, options.transferReason ?? exports.MANUAL_TRANSFER_DELEGATION_REASON);
265
280
  await this.notificationService.resolveTaskNotifications({
266
281
  actingMemberId: input.decidedByMemberId,
267
282
  manager,
@@ -1895,6 +1910,10 @@ let WorkflowEngineService = class WorkflowEngineService {
1895
1910
  const candidates = await this.resolveRuntimeTaskCandidates(manager, instance, node);
1896
1911
  const primaryCandidate = candidates[0];
1897
1912
  const now = new Date();
1913
+ const slaDueAt = await this.slaScheduleService.resolveTaskSlaDueAt({
1914
+ node,
1915
+ now,
1916
+ });
1898
1917
  const task = await taskRepository.save(taskRepository.create({
1899
1918
  assigneeMemberId: candidates.length === 1 ? primaryCandidate.memberId : null,
1900
1919
  assignmentType: candidates.length === 1
@@ -1908,7 +1927,7 @@ let WorkflowEngineService = class WorkflowEngineService {
1908
1927
  nodeId: node.id,
1909
1928
  openedAt: null,
1910
1929
  originalAssigneeMemberId: candidates.length === 1 ? primaryCandidate.originalMemberId : null,
1911
- slaDueAt: (0, notification_service_1.calculateTaskSlaDueAt)({ node, now }),
1930
+ slaDueAt,
1912
1931
  status: workflow_engine_enums_1.TaskStatusEnum.PENDING,
1913
1932
  tokenId: token.id,
1914
1933
  }));
@@ -2006,7 +2025,7 @@ let WorkflowEngineService = class WorkflowEngineService {
2006
2025
  }
2007
2026
  return nextCandidates;
2008
2027
  }
2009
- async transferTask(manager, instance, task, decision, signature, transferToMemberId, decisionComment, decidedAt, actorCandidate) {
2028
+ async transferTask(manager, instance, task, decision, signature, transferToMemberId, decisionComment, decidedAt, actorCandidate, transferReason) {
2010
2029
  if (!transferToMemberId) {
2011
2030
  throw new common_1.BadRequestException('Transfer target member is required');
2012
2031
  }
@@ -2021,7 +2040,7 @@ let WorkflowEngineService = class WorkflowEngineService {
2021
2040
  ...readDelegationSteps(task.delegationChain),
2022
2041
  {
2023
2042
  from: transferringMemberId,
2024
- reason: 'MANUAL_TRANSFER',
2043
+ reason: transferReason,
2025
2044
  ruleId: null,
2026
2045
  to: transferToMemberId,
2027
2046
  },
@@ -2161,9 +2180,9 @@ let WorkflowEngineService = class WorkflowEngineService {
2161
2180
  return this.resolveOrgUnitPositionCandidates(manager, resolver.orgUnitId, resolver.positionId, Boolean(resolver.includeDescendants), label).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
2162
2181
  }
2163
2182
  if (resolver.type === 'ORG_UNIT_MANAGER') {
2164
- return this.resolveOrgUnitManagerCandidates(manager, instance, resolver.orgUnitId, resolver.fallback, label).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
2183
+ return this.resolveOrgUnitManagerCandidates(manager, instance, resolver.orgUnitId, resolver.fallback, label, resolver.preferClosestOrgUnit === true).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
2165
2184
  }
2166
- return this.resolveInitiatorManagerCandidates(manager, instance, resolver.levelsUp, resolver.fallback, label).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
2185
+ return this.resolveInitiatorManagerCandidates(manager, instance, resolver.levelsUp, resolver.fallback, label, resolver.preferClosestOrgUnit === true).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
2167
2186
  }
2168
2187
  async resolvePositionCandidates(manager, positionId, label) {
2169
2188
  const today = toDateOnlyString(new Date());
@@ -2184,9 +2203,9 @@ let WorkflowEngineService = class WorkflowEngineService {
2184
2203
  }
2185
2204
  return uniqueTexts(activeMemberships.map((membership) => membership.memberId));
2186
2205
  }
2187
- async resolveInitiatorManagerCandidates(manager, instance, levelsUp, fallback, label) {
2206
+ async resolveInitiatorManagerCandidates(manager, instance, levelsUp, fallback, label, preferClosestOrgUnit) {
2188
2207
  const normalizedLevelsUp = Math.max(Math.trunc(levelsUp), 1);
2189
- const resolvedManagerMemberIds = await this.resolveManagerMemberIdsFromOrganization(manager, instance.initiatorMemberId, normalizedLevelsUp);
2208
+ const resolvedManagerMemberIds = await this.resolveManagerMemberIdsFromOrganization(manager, instance.initiatorMemberId, normalizedLevelsUp, preferClosestOrgUnit);
2190
2209
  if (resolvedManagerMemberIds.length) {
2191
2210
  return resolvedManagerMemberIds;
2192
2211
  }
@@ -2207,18 +2226,18 @@ let WorkflowEngineService = class WorkflowEngineService {
2207
2226
  }
2208
2227
  return fallback.memberId;
2209
2228
  }
2210
- async resolveManagerMemberIdsFromOrganization(manager, memberId, levelsUp) {
2211
- return this.resolveManagerMemberIdsAtLevel(manager, memberId, toDateOnlyString(new Date()), levelsUp);
2229
+ async resolveManagerMemberIdsFromOrganization(manager, memberId, levelsUp, preferClosestOrgUnit) {
2230
+ return this.resolveManagerMemberIdsAtLevel(manager, memberId, toDateOnlyString(new Date()), levelsUp, preferClosestOrgUnit);
2212
2231
  }
2213
- async resolveManagerMemberIdsAtLevel(manager, memberId, date, remainingLevels) {
2214
- const directManagerMemberIds = await this.resolveDirectManagerMemberIds(manager, memberId, date);
2232
+ async resolveManagerMemberIdsAtLevel(manager, memberId, date, remainingLevels, preferClosestOrgUnit) {
2233
+ const directManagerMemberIds = await this.resolveDirectManagerMemberIds(manager, memberId, date, preferClosestOrgUnit);
2215
2234
  if (remainingLevels <= 1 || directManagerMemberIds.length === 0) {
2216
2235
  return directManagerMemberIds;
2217
2236
  }
2218
- const nextLevelGroups = await Promise.all(directManagerMemberIds.map((managerMemberId) => this.resolveManagerMemberIdsAtLevel(manager, managerMemberId, date, remainingLevels - 1)));
2237
+ const nextLevelGroups = await Promise.all(directManagerMemberIds.map((managerMemberId) => this.resolveManagerMemberIdsAtLevel(manager, managerMemberId, date, remainingLevels - 1, preferClosestOrgUnit)));
2219
2238
  return uniqueTexts(nextLevelGroups.flat());
2220
2239
  }
2221
- async resolveDirectManagerMemberIds(manager, memberId, date) {
2240
+ async resolveDirectManagerMemberIds(manager, memberId, date, preferClosestOrgUnit) {
2222
2241
  const memberships = await manager.getRepository(membership_entity_1.MembershipEntity).find({
2223
2242
  where: { memberId },
2224
2243
  });
@@ -2227,11 +2246,11 @@ let WorkflowEngineService = class WorkflowEngineService {
2227
2246
  const positionIds = activeMemberships
2228
2247
  .map((membership) => membership.positionId)
2229
2248
  .filter((positionId) => Boolean(positionId));
2230
- const orgUnitIds = await this.readOrgUnitAndAncestorIds(manager, directOrgUnitIds);
2249
+ const orgUnits = await this.readOrgUnitAndAncestors(manager, directOrgUnitIds);
2231
2250
  const candidatePairs = [
2232
2251
  { scopeId: memberId, scopeType: organization_enums_1.ManagerResolutionScopeTypeEnum.MEMBER },
2233
- ...orgUnitIds.map((scopeId) => ({
2234
- scopeId,
2252
+ ...orgUnits.map((orgUnit) => ({
2253
+ scopeId: orgUnit.id,
2235
2254
  scopeType: organization_enums_1.ManagerResolutionScopeTypeEnum.ORG_UNIT,
2236
2255
  })),
2237
2256
  ...positionIds.map((scopeId) => ({
@@ -2239,17 +2258,15 @@ let WorkflowEngineService = class WorkflowEngineService {
2239
2258
  scopeType: organization_enums_1.ManagerResolutionScopeTypeEnum.POSITION,
2240
2259
  })),
2241
2260
  ];
2242
- return this.resolveManagerResolutionCandidates(manager, candidatePairs, date);
2261
+ return this.resolveManagerResolutionCandidates(manager, candidatePairs, date, readOrgUnitDepthMap(orgUnits), preferClosestOrgUnit);
2243
2262
  }
2244
- async resolveOrgUnitManagerCandidates(manager, instance, orgUnitId, fallback, label) {
2263
+ async resolveOrgUnitManagerCandidates(manager, instance, orgUnitId, fallback, label, preferClosestOrgUnit) {
2245
2264
  const date = toDateOnlyString(new Date());
2246
- const orgUnitIds = await this.readOrgUnitAndAncestorIds(manager, [
2247
- orgUnitId,
2248
- ]);
2249
- const managerMemberIds = await this.resolveManagerResolutionCandidates(manager, orgUnitIds.map((scopeId) => ({
2250
- scopeId,
2265
+ const orgUnits = await this.readOrgUnitAndAncestors(manager, [orgUnitId]);
2266
+ const managerMemberIds = await this.resolveManagerResolutionCandidates(manager, orgUnits.map((orgUnit) => ({
2267
+ scopeId: orgUnit.id,
2251
2268
  scopeType: organization_enums_1.ManagerResolutionScopeTypeEnum.ORG_UNIT,
2252
- })), date);
2269
+ })), date, readOrgUnitDepthMap(orgUnits), preferClosestOrgUnit);
2253
2270
  if (!managerMemberIds.length) {
2254
2271
  return [
2255
2272
  this.resolveFallbackAssignee(instance, fallback, label, `找不到指定組織的主管規則`),
@@ -2257,7 +2274,7 @@ let WorkflowEngineService = class WorkflowEngineService {
2257
2274
  }
2258
2275
  return managerMemberIds;
2259
2276
  }
2260
- async readOrgUnitAndAncestorIds(manager, orgUnitIds) {
2277
+ async readOrgUnitAndAncestors(manager, orgUnitIds) {
2261
2278
  if (!orgUnitIds.length) {
2262
2279
  return [];
2263
2280
  }
@@ -2271,9 +2288,9 @@ let WorkflowEngineService = class WorkflowEngineService {
2271
2288
  .andWhere('orgUnit.path @> :path', { path: orgUnit.path })
2272
2289
  .getMany()));
2273
2290
  const byId = new Map([...directOrgUnits, ...ancestorGroups.flat()].map((orgUnit) => [orgUnit.id, orgUnit]));
2274
- return [...byId.keys()];
2291
+ return [...byId.values()];
2275
2292
  }
2276
- async resolveManagerResolutionCandidates(manager, candidatePairs, date) {
2293
+ async resolveManagerResolutionCandidates(manager, candidatePairs, date, orgUnitDepths, preferClosestOrgUnit) {
2277
2294
  const scopeIds = candidatePairs.map((pair) => pair.scopeId);
2278
2295
  if (!scopeIds.length) {
2279
2296
  return [];
@@ -2290,12 +2307,13 @@ let WorkflowEngineService = class WorkflowEngineService {
2290
2307
  ]),
2291
2308
  },
2292
2309
  });
2310
+ const depthLookup = preferClosestOrgUnit ? orgUnitDepths : undefined;
2293
2311
  const active = resolutions
2294
2312
  .filter((resolution) => candidatePairs.some((pair) => pair.scopeId === resolution.scopeId &&
2295
2313
  pair.scopeType === resolution.scopeType))
2296
2314
  .filter((resolution) => isDateRangeActive(resolution, date))
2297
- .sort(compareManagerResolution);
2298
- return uniqueTexts(active.map((resolution) => resolution.managerMemberId));
2315
+ .sort((left, right) => compareManagerResolution(left, right, depthLookup));
2316
+ return uniqueTexts(readTopPriorityResolutions(active, depthLookup).map((resolution) => resolution.managerMemberId));
2299
2317
  }
2300
2318
  async resolveOrgUnitMemberCandidates(manager, orgUnitId, includeDescendants, label) {
2301
2319
  const orgUnitIds = await this.readOrgUnitScopeIds(manager, orgUnitId, includeDescendants);
@@ -2716,8 +2734,8 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2716
2734
  tslib_1.__param(8, (0, typeorm_1.InjectRepository)(approval_template_entity_1.ApprovalTemplateEntity)),
2717
2735
  tslib_1.__param(9, (0, typeorm_1.InjectRepository)(approval_template_version_entity_1.ApprovalTemplateVersionEntity)),
2718
2736
  tslib_1.__param(10, (0, typeorm_1.InjectRepository)(form_definition_version_entity_1.FormDefinitionVersionEntity)),
2719
- tslib_1.__param(16, (0, common_1.Optional)()),
2720
- tslib_1.__param(16, (0, common_1.Inject)(workflow_service_task_dispatcher_token_1.BPM_WORKFLOW_SERVICE_TASK_DISPATCHER)),
2737
+ tslib_1.__param(17, (0, common_1.Optional)()),
2738
+ tslib_1.__param(17, (0, common_1.Inject)(workflow_service_task_dispatcher_token_1.BPM_WORKFLOW_SERVICE_TASK_DISPATCHER)),
2721
2739
  tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
2722
2740
  typeorm_2.Repository,
2723
2741
  typeorm_2.Repository,
@@ -2733,7 +2751,8 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2733
2751
  condition_service_1.ConditionService,
2734
2752
  delegation_service_1.DelegationService,
2735
2753
  notification_service_1.NotificationService,
2736
- signature_service_1.SignatureService, Object])
2754
+ signature_service_1.SignatureService,
2755
+ sla_schedule_service_1.BPMSlaScheduleService, Object])
2737
2756
  ], WorkflowEngineService);
2738
2757
  function parseJsonObject(value, label) {
2739
2758
  try {
@@ -2917,6 +2936,11 @@ function readDefaultReturnTargetNodeId(workflow, currentNode) {
2917
2936
  }
2918
2937
  return previousNodeId;
2919
2938
  }
2939
+ function isReturnCommentRequired(workflow, nodeId) {
2940
+ const node = readWorkflowNodeOrThrow(workflow, nodeId);
2941
+ return (node.type === 'userTask' &&
2942
+ node.data.returnBehavior.requireComment === true);
2943
+ }
2920
2944
  function readReturnResubmitStrategy(task, instance) {
2921
2945
  const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, task.nodeId);
2922
2946
  if (node.type !== 'userTask') {
@@ -3289,7 +3313,10 @@ function readManagerMemberIdFromInitiatorSnapshot(initiatorMetadataSnapshot, lev
3289
3313
  return managerMemberIds[Math.max(levelsUp - 1, 0)];
3290
3314
  }
3291
3315
  const customFields = initiatorMetadataSnapshot.customFields;
3292
- if (isRecord(customFields)) {
3316
+ // An empty `customFields` object must not swallow the top-level fallback:
3317
+ // callers commonly send `{ customFields: {}, managerMemberId: '...' }`, and
3318
+ // returning early there resolves no approver at all.
3319
+ if (isRecord(customFields) && customFields.managerMemberId !== undefined) {
3293
3320
  return customFields.managerMemberId;
3294
3321
  }
3295
3322
  return initiatorMetadataSnapshot.managerMemberId;
@@ -3298,14 +3325,61 @@ function isDateRangeActive(value, date) {
3298
3325
  return (value.effectiveFrom <= date &&
3299
3326
  (!value.effectiveTo || value.effectiveTo >= date));
3300
3327
  }
3301
- function compareManagerResolution(left, right) {
3328
+ /**
3329
+ * Manager resolutions are sorted by priority and then by scope specificity, so
3330
+ * every entry below the winning tier is a lower-precedence fallback that must
3331
+ * not join the approver list — a company-wide catch-all rule would otherwise be
3332
+ * appended to every member's direct manager. The organization module already
3333
+ * resolves managers this way (`OrganizationService.resolveManagerMemberId`
3334
+ * returns `active[0]`); this keeps the workflow engine consistent with it.
3335
+ *
3336
+ * Ties on the winning tier are preserved so a step can still be shared by
3337
+ * several managers of equal precedence.
3338
+ *
3339
+ * When `orgUnitDepths` is provided and the winning scope is ORG_UNIT, only the
3340
+ * deepest org units on the winning tier are kept so that ancestor-level
3341
+ * catch-all rules do not dilute the approver list. This is opt-in via the
3342
+ * resolver's `preferClosestOrgUnit` flag because org-tree depth does not always
3343
+ * reflect authority (e.g. project offices attached to executive roles).
3344
+ */
3345
+ function readTopPriorityResolutions(resolutions, orgUnitDepths) {
3346
+ const [top] = resolutions;
3347
+ if (!top) {
3348
+ return [];
3349
+ }
3350
+ const sameTier = resolutions.filter((resolution) => resolution.priority === top.priority &&
3351
+ resolution.scopeType === top.scopeType);
3352
+ if (orgUnitDepths &&
3353
+ top.scopeType === organization_enums_1.ManagerResolutionScopeTypeEnum.ORG_UNIT) {
3354
+ const maxDepth = Math.max(...sameTier.map((resolution) => orgUnitDepths.get(resolution.scopeId) ?? 0));
3355
+ return sameTier.filter((resolution) => (orgUnitDepths.get(resolution.scopeId) ?? 0) === maxDepth);
3356
+ }
3357
+ return sameTier;
3358
+ }
3359
+ function compareManagerResolution(left, right, orgUnitDepths) {
3302
3360
  const priorityDiff = right.priority - left.priority;
3303
3361
  if (priorityDiff !== 0) {
3304
3362
  return priorityDiff;
3305
3363
  }
3306
- return (readManagerResolutionScopeRank(right.scopeType) -
3307
- readManagerResolutionScopeRank(left.scopeType) ||
3308
- right.effectiveFrom.localeCompare(left.effectiveFrom));
3364
+ const scopeRankDiff = readManagerResolutionScopeRank(right.scopeType) -
3365
+ readManagerResolutionScopeRank(left.scopeType);
3366
+ if (scopeRankDiff !== 0) {
3367
+ return scopeRankDiff;
3368
+ }
3369
+ if (orgUnitDepths) {
3370
+ const depthDiff = (orgUnitDepths.get(right.scopeId) ?? 0) -
3371
+ (orgUnitDepths.get(left.scopeId) ?? 0);
3372
+ if (depthDiff !== 0) {
3373
+ return depthDiff;
3374
+ }
3375
+ }
3376
+ return right.effectiveFrom.localeCompare(left.effectiveFrom);
3377
+ }
3378
+ function readOrgUnitDepthMap(orgUnits) {
3379
+ return new Map(orgUnits.map((orgUnit) => [
3380
+ orgUnit.id,
3381
+ orgUnit.path.split('.').length,
3382
+ ]));
3309
3383
  }
3310
3384
  function readManagerResolutionScopeRank(scopeType) {
3311
3385
  const ranks = {