@rytass/bpm-core-nestjs-module 0.3.0 → 0.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 (658) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/eslint.config.mjs +20 -0
  3. package/jest.config.cts +15 -0
  4. package/package.json +2 -2
  5. package/project.json +58 -0
  6. package/src/lib/attachment/attachment-options.spec.ts +45 -0
  7. package/src/lib/attachment/attachment-options.ts +130 -0
  8. package/src/lib/attachment/attachment-storage.provider.spec.ts +79 -0
  9. package/src/lib/attachment/attachment-storage.provider.ts +60 -0
  10. package/src/lib/attachment/{attachment-storage.token.d.ts → attachment-storage.token.ts} +3 -1
  11. package/src/lib/attachment/attachment.controller.ts +39 -0
  12. package/src/lib/attachment/attachment.entity.ts +59 -0
  13. package/src/lib/attachment/attachment.module.ts +149 -0
  14. package/src/lib/attachment/attachment.mutations.ts +22 -0
  15. package/src/lib/attachment/attachment.queries.ts +65 -0
  16. package/src/lib/attachment/attachment.service.spec.ts +319 -0
  17. package/src/lib/attachment/attachment.service.ts +477 -0
  18. package/src/lib/attachment/dto/upload-attachment.input.ts +60 -0
  19. package/src/lib/bpm/bpm-root.module.spec.ts +138 -0
  20. package/src/lib/bpm/bpm-root.module.ts +320 -0
  21. package/src/lib/bpm-auth/bpm-auth-context.extractor.ts +106 -0
  22. package/src/lib/bpm-auth/bpm-auth-context.ts +40 -0
  23. package/src/lib/bpm-auth/bpm-auth.authorization.ts +80 -0
  24. package/src/lib/bpm-auth/bpm-auth.decorators.ts +19 -0
  25. package/src/lib/bpm-auth/bpm-auth.guard.ts +31 -0
  26. package/src/lib/bpm-auth/bpm-auth.module.spec.ts +106 -0
  27. package/src/lib/bpm-auth/bpm-auth.module.ts +69 -0
  28. package/src/lib/bpm-auth/bpm-auth.options.ts +21 -0
  29. package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.ts +55 -0
  30. package/src/lib/common/filters/all-exceptions.filter.ts +138 -0
  31. package/src/lib/common/iso-duration.ts +31 -0
  32. package/src/lib/condition/condition.module.ts +8 -0
  33. package/src/lib/condition/condition.service.spec.ts +50 -0
  34. package/src/lib/condition/condition.service.ts +141 -0
  35. package/src/lib/database/data-source.ts +7 -0
  36. package/src/lib/database/migration-runner.ts +26 -0
  37. package/src/lib/database/reconcile-legacy-migrations.ts +87 -0
  38. package/src/lib/database/typeorm.config.ts +217 -0
  39. package/src/lib/delegation/delegation-rule.entity.ts +84 -0
  40. package/src/lib/delegation/delegation.enums.ts +21 -0
  41. package/src/lib/delegation/delegation.module.ts +14 -0
  42. package/src/lib/delegation/delegation.mutations.ts +92 -0
  43. package/src/lib/delegation/delegation.queries.ts +111 -0
  44. package/src/lib/delegation/delegation.service.spec.ts +295 -0
  45. package/src/lib/delegation/delegation.service.ts +482 -0
  46. package/src/lib/delegation/dto/delegation-rule.input.ts +125 -0
  47. package/src/lib/form/dto/form-definition.input.ts +90 -0
  48. package/src/lib/form/form-definition-version.entity.ts +66 -0
  49. package/src/lib/form/form-definition.entity.ts +54 -0
  50. package/src/lib/form/form-schema-lint.object.ts +10 -0
  51. package/src/lib/form/form-schema.validator.spec.ts +180 -0
  52. package/src/lib/form/form-schema.validator.ts +538 -0
  53. package/src/lib/form/form.enums.ts +20 -0
  54. package/src/lib/form/form.module.ts +19 -0
  55. package/src/lib/form/form.mutations.ts +73 -0
  56. package/src/lib/form/form.queries.ts +72 -0
  57. package/src/lib/form/form.service.spec.ts +371 -0
  58. package/src/lib/form/form.service.ts +607 -0
  59. package/src/lib/identity/identity-options.ts +41 -0
  60. package/src/lib/identity/identity.module.ts +96 -0
  61. package/src/lib/identity/identity.queries.ts +61 -0
  62. package/src/lib/identity/identity.service.spec.ts +328 -0
  63. package/src/lib/identity/identity.service.ts +277 -0
  64. package/src/lib/identity/member-base.adapter.spec.ts +122 -0
  65. package/src/lib/identity/member-base.adapter.ts +302 -0
  66. package/src/lib/identity/member-metadata-cache.entity.ts +27 -0
  67. package/src/lib/identity/{member-not-found.exception.d.ts → member-not-found.exception.ts} +8 -3
  68. package/src/lib/identity/member-profile.object.ts +28 -0
  69. package/src/lib/identity/member-resolver.interface.ts +140 -0
  70. package/src/lib/migrations/0000000000001-enable-postgres-extensions.ts +14 -0
  71. package/src/lib/migrations/{0000000001000-identity-organization-foundation.js → 0000000001000-identity-organization-foundation.ts} +38 -29
  72. package/src/lib/migrations/{0000000002000-form-builder-foundation.js → 0000000002000-form-builder-foundation.ts} +22 -21
  73. package/src/lib/migrations/{0000000003000-approval-template-foundation.js → 0000000003000-approval-template-foundation.ts} +24 -21
  74. package/src/lib/migrations/{0000000004000-workflow-engine-foundation.js → 0000000004000-workflow-engine-foundation.ts} +46 -34
  75. package/src/lib/migrations/{0000000005000-delegation-rules.js → 0000000005000-delegation-rules.ts} +20 -19
  76. package/src/lib/migrations/{0000000006000-notifications-sla.js → 0000000006000-notifications-sla.ts} +26 -23
  77. package/src/lib/migrations/{0000000007000-signatures-attachments.js → 0000000007000-signatures-attachments.ts} +28 -27
  78. package/src/lib/migrations/{0000000008000-approval-template-categories.js → 0000000008000-approval-template-categories.ts} +28 -25
  79. package/src/lib/migrations/{0000000009000-task-candidates.js → 0000000009000-task-candidates.ts} +26 -23
  80. package/src/lib/migrations/{0000000010000-notification-delivery-state.js → 0000000010000-notification-delivery-state.ts} +16 -17
  81. package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.ts +19 -0
  82. package/src/lib/migrations/0000000012000-notification-sla-idempotency.ts +22 -0
  83. package/src/lib/migrations/0000000013000-workflow-query-indexes.ts +113 -0
  84. package/src/lib/migrations/0000000014000-notification-resolution.ts +29 -0
  85. package/src/lib/migrations/{0000000015000-backfill-stale-notification-resolution.js → 0000000015000-backfill-stale-notification-resolution.ts} +15 -16
  86. package/src/lib/migrations/{0000000016000-archive-parallel-form-drafts.js → 0000000016000-archive-parallel-form-drafts.ts} +15 -16
  87. package/src/lib/migrations/{0000000017000-adhoc-directives.js → 0000000017000-adhoc-directives.ts} +18 -19
  88. package/src/lib/migrations/index.ts +59 -0
  89. package/src/lib/notification/dto/notification-preference.input.ts +46 -0
  90. package/src/lib/notification/notification-delivery-scheduler.service.ts +69 -0
  91. package/src/lib/notification/notification-delivery.service.spec.ts +315 -0
  92. package/src/lib/notification/notification-delivery.service.ts +339 -0
  93. package/src/lib/notification/notification-dispatcher.token.ts +13 -0
  94. package/src/lib/notification/notification-options.module.ts +67 -0
  95. package/src/lib/notification/notification-options.spec.ts +93 -0
  96. package/src/lib/notification/notification-options.ts +452 -0
  97. package/src/lib/notification/notification-preference.entity.ts +35 -0
  98. package/src/lib/notification/notification-sla-scheduler.service.ts +66 -0
  99. package/src/lib/notification/notification-template.spec.ts +33 -0
  100. package/src/lib/notification/notification-template.ts +97 -0
  101. package/src/lib/notification/notification.entity.ts +129 -0
  102. package/src/lib/notification/notification.enums.ts +64 -0
  103. package/src/lib/notification/notification.module.ts +37 -0
  104. package/src/lib/notification/notification.mutations.ts +47 -0
  105. package/src/lib/notification/notification.queries.ts +64 -0
  106. package/src/lib/notification/notification.service.spec.ts +412 -0
  107. package/src/lib/notification/notification.service.ts +1129 -0
  108. package/src/lib/organization/dto/manager-resolution.input.ts +91 -0
  109. package/src/lib/organization/dto/membership.input.ts +77 -0
  110. package/src/lib/organization/dto/org-unit.input.ts +96 -0
  111. package/src/lib/organization/dto/position.input.ts +50 -0
  112. package/src/lib/organization/json-metadata.ts +15 -0
  113. package/src/lib/organization/manager-resolution.entity.ts +44 -0
  114. package/src/lib/organization/membership.entity.ts +50 -0
  115. package/src/lib/organization/org-unit-tree-commit-result.object.ts +8 -0
  116. package/src/lib/organization/org-unit.entity.ts +53 -0
  117. package/src/lib/organization/organization-summary.object.ts +16 -0
  118. package/src/lib/organization/organization.enums.ts +19 -0
  119. package/src/lib/organization/organization.module.ts +27 -0
  120. package/src/lib/organization/organization.mutations.ts +115 -0
  121. package/src/lib/organization/organization.queries.ts +191 -0
  122. package/src/lib/organization/organization.service.spec.ts +835 -0
  123. package/src/lib/organization/organization.service.ts +1245 -0
  124. package/src/lib/organization/position.entity.ts +39 -0
  125. package/src/lib/organization/resolved-manager.object.ts +10 -0
  126. package/src/lib/signature/signature-options.spec.ts +60 -0
  127. package/src/lib/signature/signature-options.ts +120 -0
  128. package/src/lib/signature/signature-verification.object.ts +16 -0
  129. package/src/lib/signature/signature.entity.ts +62 -0
  130. package/src/lib/signature/signature.module.ts +80 -0
  131. package/src/lib/signature/signature.queries.ts +37 -0
  132. package/src/lib/signature/signature.service.spec.ts +134 -0
  133. package/src/lib/signature/signature.service.ts +205 -0
  134. package/src/lib/template/approval-template-category.entity.ts +40 -0
  135. package/src/lib/template/approval-template-version.entity.ts +82 -0
  136. package/src/lib/template/approval-template.entity.ts +64 -0
  137. package/src/lib/template/compose-approval-template.object.ts +23 -0
  138. package/src/lib/template/compose-template.mutations.ts +22 -0
  139. package/src/lib/template/dto/approval-template.input.ts +147 -0
  140. package/src/lib/template/dto/compose-approval-template.input.ts +80 -0
  141. package/src/lib/template/template.enums.ts +30 -0
  142. package/src/lib/template/template.module.ts +33 -0
  143. package/src/lib/template/template.mutations.ts +106 -0
  144. package/src/lib/template/template.queries.ts +119 -0
  145. package/src/lib/template/template.service.spec.ts +822 -0
  146. package/src/lib/template/template.service.ts +1082 -0
  147. package/src/lib/template/workflow-definition.validator.spec.ts +595 -0
  148. package/src/lib/template/workflow-definition.validator.ts +539 -0
  149. package/src/lib/testing/cel-js.jest.ts +46 -0
  150. package/src/lib/workflow-engine/activity-log.entity.ts +48 -0
  151. package/src/lib/workflow-engine/adhoc-directive.entity.ts +90 -0
  152. package/src/lib/workflow-engine/adhoc.enums.ts +42 -0
  153. package/src/lib/workflow-engine/approval-instance-page-info.object.ts +22 -0
  154. package/src/lib/workflow-engine/approval-instance.entity.ts +86 -0
  155. package/src/lib/workflow-engine/dto/adhoc-notification.input.ts +18 -0
  156. package/src/lib/workflow-engine/dto/adhoc-target.input.ts +52 -0
  157. package/src/lib/workflow-engine/dto/cancel-approval-instance.input.ts +18 -0
  158. package/src/lib/workflow-engine/dto/decide-task.input.ts +33 -0
  159. package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.ts +22 -0
  160. package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.ts +22 -0
  161. package/src/lib/workflow-engine/dto/submit-approval-instance.input.ts +27 -0
  162. package/src/lib/workflow-engine/task-candidate.entity.ts +56 -0
  163. package/src/lib/workflow-engine/task-decision.entity.ts +43 -0
  164. package/src/lib/workflow-engine/task.entity.ts +102 -0
  165. package/src/lib/workflow-engine/workflow-condition-evaluator.ts +168 -0
  166. package/src/lib/workflow-engine/workflow-dashboard-summary.object.ts +22 -0
  167. package/src/lib/workflow-engine/workflow-dry-run.object.ts +58 -0
  168. package/src/lib/workflow-engine/workflow-engine.enums.ts +102 -0
  169. package/src/lib/workflow-engine/workflow-engine.module.ts +77 -0
  170. package/src/lib/workflow-engine/workflow-engine.mutations.ts +193 -0
  171. package/src/lib/workflow-engine/workflow-engine.queries.ts +241 -0
  172. package/src/lib/workflow-engine/workflow-engine.service.spec.ts +3345 -0
  173. package/src/lib/workflow-engine/workflow-engine.service.ts +6022 -0
  174. package/src/lib/workflow-engine/{workflow-engine.tokens.d.ts → workflow-engine.tokens.ts} +9 -3
  175. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.ts +69 -0
  176. package/src/lib/workflow-engine/workflow-token.entity.ts +40 -0
  177. package/tsconfig.json +24 -0
  178. package/tsconfig.lib.json +16 -0
  179. package/tsconfig.spec.json +16 -0
  180. package/src/index.js +0 -18
  181. package/src/index.js.map +0 -1
  182. package/src/lib/attachment/attachment-options.d.ts +0 -44
  183. package/src/lib/attachment/attachment-options.js +0 -62
  184. package/src/lib/attachment/attachment-options.js.map +0 -1
  185. package/src/lib/attachment/attachment-storage.provider.d.ts +0 -4
  186. package/src/lib/attachment/attachment-storage.provider.js +0 -35
  187. package/src/lib/attachment/attachment-storage.provider.js.map +0 -1
  188. package/src/lib/attachment/attachment-storage.token.js +0 -5
  189. package/src/lib/attachment/attachment-storage.token.js.map +0 -1
  190. package/src/lib/attachment/attachment.controller.d.ts +0 -8
  191. package/src/lib/attachment/attachment.controller.js +0 -38
  192. package/src/lib/attachment/attachment.controller.js.map +0 -1
  193. package/src/lib/attachment/attachment.entity.d.ts +0 -14
  194. package/src/lib/attachment/attachment.entity.js +0 -74
  195. package/src/lib/attachment/attachment.entity.js.map +0 -1
  196. package/src/lib/attachment/attachment.module.d.ts +0 -27
  197. package/src/lib/attachment/attachment.module.js +0 -103
  198. package/src/lib/attachment/attachment.module.js.map +0 -1
  199. package/src/lib/attachment/attachment.mutations.d.ts +0 -8
  200. package/src/lib/attachment/attachment.mutations.js +0 -35
  201. package/src/lib/attachment/attachment.mutations.js.map +0 -1
  202. package/src/lib/attachment/attachment.queries.d.ts +0 -9
  203. package/src/lib/attachment/attachment.queries.js +0 -78
  204. package/src/lib/attachment/attachment.queries.js.map +0 -1
  205. package/src/lib/attachment/attachment.service.d.ts +0 -49
  206. package/src/lib/attachment/attachment.service.js +0 -276
  207. package/src/lib/attachment/attachment.service.js.map +0 -1
  208. package/src/lib/attachment/dto/upload-attachment.input.d.ts +0 -11
  209. package/src/lib/attachment/dto/upload-attachment.input.js +0 -68
  210. package/src/lib/attachment/dto/upload-attachment.input.js.map +0 -1
  211. package/src/lib/attachment/index.js +0 -9
  212. package/src/lib/attachment/index.js.map +0 -1
  213. package/src/lib/bpm/bpm-root.module.d.ts +0 -155
  214. package/src/lib/bpm/bpm-root.module.js +0 -170
  215. package/src/lib/bpm/bpm-root.module.js.map +0 -1
  216. package/src/lib/bpm/index.js +0 -5
  217. package/src/lib/bpm/index.js.map +0 -1
  218. package/src/lib/bpm-auth/bpm-auth-context.d.ts +0 -24
  219. package/src/lib/bpm-auth/bpm-auth-context.extractor.d.ts +0 -4
  220. package/src/lib/bpm-auth/bpm-auth-context.extractor.js +0 -69
  221. package/src/lib/bpm-auth/bpm-auth-context.extractor.js.map +0 -1
  222. package/src/lib/bpm-auth/bpm-auth-context.js +0 -15
  223. package/src/lib/bpm-auth/bpm-auth-context.js.map +0 -1
  224. package/src/lib/bpm-auth/bpm-auth.authorization.d.ts +0 -12
  225. package/src/lib/bpm-auth/bpm-auth.authorization.js +0 -67
  226. package/src/lib/bpm-auth/bpm-auth.authorization.js.map +0 -1
  227. package/src/lib/bpm-auth/bpm-auth.decorators.d.ts +0 -3
  228. package/src/lib/bpm-auth/bpm-auth.decorators.js +0 -11
  229. package/src/lib/bpm-auth/bpm-auth.decorators.js.map +0 -1
  230. package/src/lib/bpm-auth/bpm-auth.guard.d.ts +0 -7
  231. package/src/lib/bpm-auth/bpm-auth.guard.js +0 -24
  232. package/src/lib/bpm-auth/bpm-auth.guard.js.map +0 -1
  233. package/src/lib/bpm-auth/bpm-auth.module.d.ts +0 -6
  234. package/src/lib/bpm-auth/bpm-auth.module.js +0 -69
  235. package/src/lib/bpm-auth/bpm-auth.module.js.map +0 -1
  236. package/src/lib/bpm-auth/bpm-auth.options.d.ts +0 -11
  237. package/src/lib/bpm-auth/bpm-auth.options.js +0 -3
  238. package/src/lib/bpm-auth/bpm-auth.options.js.map +0 -1
  239. package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.d.ts +0 -10
  240. package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.js +0 -40
  241. package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.js.map +0 -1
  242. package/src/lib/bpm-auth/index.js +0 -10
  243. package/src/lib/bpm-auth/index.js.map +0 -1
  244. package/src/lib/common/filters/all-exceptions.filter.d.ts +0 -25
  245. package/src/lib/common/filters/all-exceptions.filter.js +0 -93
  246. package/src/lib/common/filters/all-exceptions.filter.js.map +0 -1
  247. package/src/lib/common/index.js +0 -5
  248. package/src/lib/common/index.js.map +0 -1
  249. package/src/lib/common/iso-duration.d.ts +0 -1
  250. package/src/lib/common/iso-duration.js +0 -27
  251. package/src/lib/common/iso-duration.js.map +0 -1
  252. package/src/lib/condition/condition.module.d.ts +0 -2
  253. package/src/lib/condition/condition.module.js +0 -16
  254. package/src/lib/condition/condition.module.js.map +0 -1
  255. package/src/lib/condition/condition.service.d.ts +0 -16
  256. package/src/lib/condition/condition.service.js +0 -82
  257. package/src/lib/condition/condition.service.js.map +0 -1
  258. package/src/lib/condition/index.js +0 -6
  259. package/src/lib/condition/index.js.map +0 -1
  260. package/src/lib/database/data-source.d.ts +0 -4
  261. package/src/lib/database/data-source.js +0 -7
  262. package/src/lib/database/data-source.js.map +0 -1
  263. package/src/lib/database/index.js +0 -6
  264. package/src/lib/database/index.js.map +0 -1
  265. package/src/lib/database/migration-runner.d.ts +0 -1
  266. package/src/lib/database/migration-runner.js +0 -25
  267. package/src/lib/database/migration-runner.js.map +0 -1
  268. package/src/lib/database/reconcile-legacy-migrations.d.ts +0 -13
  269. package/src/lib/database/reconcile-legacy-migrations.js +0 -70
  270. package/src/lib/database/reconcile-legacy-migrations.js.map +0 -1
  271. package/src/lib/database/typeorm.config.d.ts +0 -14
  272. package/src/lib/database/typeorm.config.js +0 -135
  273. package/src/lib/database/typeorm.config.js.map +0 -1
  274. package/src/lib/delegation/delegation-rule.entity.d.ts +0 -19
  275. package/src/lib/delegation/delegation-rule.entity.js +0 -99
  276. package/src/lib/delegation/delegation-rule.entity.js.map +0 -1
  277. package/src/lib/delegation/delegation.enums.d.ts +0 -10
  278. package/src/lib/delegation/delegation.enums.js +0 -23
  279. package/src/lib/delegation/delegation.enums.js.map +0 -1
  280. package/src/lib/delegation/delegation.module.d.ts +0 -2
  281. package/src/lib/delegation/delegation.module.js +0 -22
  282. package/src/lib/delegation/delegation.module.js.map +0 -1
  283. package/src/lib/delegation/delegation.mutations.d.ts +0 -11
  284. package/src/lib/delegation/delegation.mutations.js +0 -87
  285. package/src/lib/delegation/delegation.mutations.js.map +0 -1
  286. package/src/lib/delegation/delegation.queries.d.ts +0 -10
  287. package/src/lib/delegation/delegation.queries.js +0 -91
  288. package/src/lib/delegation/delegation.queries.js.map +0 -1
  289. package/src/lib/delegation/delegation.service.d.ts +0 -56
  290. package/src/lib/delegation/delegation.service.js +0 -268
  291. package/src/lib/delegation/delegation.service.js.map +0 -1
  292. package/src/lib/delegation/dto/delegation-rule.input.d.ts +0 -24
  293. package/src/lib/delegation/dto/delegation-rule.input.js +0 -143
  294. package/src/lib/delegation/dto/delegation-rule.input.js.map +0 -1
  295. package/src/lib/delegation/index.js +0 -9
  296. package/src/lib/delegation/index.js.map +0 -1
  297. package/src/lib/form/dto/form-definition.input.d.ts +0 -26
  298. package/src/lib/form/dto/form-definition.input.js +0 -126
  299. package/src/lib/form/dto/form-definition.input.js.map +0 -1
  300. package/src/lib/form/form-definition-version.entity.d.ts +0 -17
  301. package/src/lib/form/form-definition-version.entity.js +0 -84
  302. package/src/lib/form/form-definition-version.entity.js.map +0 -1
  303. package/src/lib/form/form-definition.entity.d.ts +0 -13
  304. package/src/lib/form/form-definition.entity.js +0 -66
  305. package/src/lib/form/form-definition.entity.js.map +0 -1
  306. package/src/lib/form/form-schema-lint.object.d.ts +0 -4
  307. package/src/lib/form/form-schema-lint.object.js +0 -20
  308. package/src/lib/form/form-schema-lint.object.js.map +0 -1
  309. package/src/lib/form/form-schema.validator.d.ts +0 -13
  310. package/src/lib/form/form-schema.validator.js +0 -354
  311. package/src/lib/form/form-schema.validator.js.map +0 -1
  312. package/src/lib/form/form.enums.d.ts +0 -9
  313. package/src/lib/form/form.enums.js +0 -22
  314. package/src/lib/form/form.enums.js.map +0 -1
  315. package/src/lib/form/form.module.d.ts +0 -2
  316. package/src/lib/form/form.module.js +0 -27
  317. package/src/lib/form/form.module.js.map +0 -1
  318. package/src/lib/form/form.mutations.d.ts +0 -14
  319. package/src/lib/form/form.mutations.js +0 -89
  320. package/src/lib/form/form.mutations.js.map +0 -1
  321. package/src/lib/form/form.queries.d.ts +0 -16
  322. package/src/lib/form/form.queries.js +0 -96
  323. package/src/lib/form/form.queries.js.map +0 -1
  324. package/src/lib/form/form.service.d.ts +0 -49
  325. package/src/lib/form/form.service.js +0 -346
  326. package/src/lib/form/form.service.js.map +0 -1
  327. package/src/lib/form/index.js +0 -12
  328. package/src/lib/form/index.js.map +0 -1
  329. package/src/lib/identity/identity-options.d.ts +0 -13
  330. package/src/lib/identity/identity-options.js +0 -20
  331. package/src/lib/identity/identity-options.js.map +0 -1
  332. package/src/lib/identity/identity.module.d.ts +0 -17
  333. package/src/lib/identity/identity.module.js +0 -65
  334. package/src/lib/identity/identity.module.js.map +0 -1
  335. package/src/lib/identity/identity.queries.d.ts +0 -12
  336. package/src/lib/identity/identity.queries.js +0 -79
  337. package/src/lib/identity/identity.queries.js.map +0 -1
  338. package/src/lib/identity/identity.service.d.ts +0 -22
  339. package/src/lib/identity/identity.service.js +0 -141
  340. package/src/lib/identity/identity.service.js.map +0 -1
  341. package/src/lib/identity/index.js +0 -8
  342. package/src/lib/identity/index.js.map +0 -1
  343. package/src/lib/identity/member-base.adapter.d.ts +0 -95
  344. package/src/lib/identity/member-base.adapter.js +0 -137
  345. package/src/lib/identity/member-base.adapter.js.map +0 -1
  346. package/src/lib/identity/member-metadata-cache.entity.d.ts +0 -8
  347. package/src/lib/identity/member-metadata-cache.entity.js +0 -39
  348. package/src/lib/identity/member-metadata-cache.entity.js.map +0 -1
  349. package/src/lib/identity/member-not-found.exception.js +0 -26
  350. package/src/lib/identity/member-not-found.exception.js.map +0 -1
  351. package/src/lib/identity/member-profile.object.d.ts +0 -8
  352. package/src/lib/identity/member-profile.object.js +0 -37
  353. package/src/lib/identity/member-profile.object.js.map +0 -1
  354. package/src/lib/identity/member-resolver.interface.d.ts +0 -79
  355. package/src/lib/identity/member-resolver.interface.js +0 -19
  356. package/src/lib/identity/member-resolver.interface.js.map +0 -1
  357. package/src/lib/migrations/0000000000001-enable-postgres-extensions.d.ts +0 -6
  358. package/src/lib/migrations/0000000000001-enable-postgres-extensions.js +0 -17
  359. package/src/lib/migrations/0000000000001-enable-postgres-extensions.js.map +0 -1
  360. package/src/lib/migrations/0000000001000-identity-organization-foundation.d.ts +0 -6
  361. package/src/lib/migrations/0000000001000-identity-organization-foundation.js.map +0 -1
  362. package/src/lib/migrations/0000000002000-form-builder-foundation.d.ts +0 -6
  363. package/src/lib/migrations/0000000002000-form-builder-foundation.js.map +0 -1
  364. package/src/lib/migrations/0000000003000-approval-template-foundation.d.ts +0 -6
  365. package/src/lib/migrations/0000000003000-approval-template-foundation.js.map +0 -1
  366. package/src/lib/migrations/0000000004000-workflow-engine-foundation.d.ts +0 -6
  367. package/src/lib/migrations/0000000004000-workflow-engine-foundation.js.map +0 -1
  368. package/src/lib/migrations/0000000005000-delegation-rules.d.ts +0 -6
  369. package/src/lib/migrations/0000000005000-delegation-rules.js.map +0 -1
  370. package/src/lib/migrations/0000000006000-notifications-sla.d.ts +0 -6
  371. package/src/lib/migrations/0000000006000-notifications-sla.js.map +0 -1
  372. package/src/lib/migrations/0000000007000-signatures-attachments.d.ts +0 -6
  373. package/src/lib/migrations/0000000007000-signatures-attachments.js.map +0 -1
  374. package/src/lib/migrations/0000000008000-approval-template-categories.d.ts +0 -6
  375. package/src/lib/migrations/0000000008000-approval-template-categories.js.map +0 -1
  376. package/src/lib/migrations/0000000009000-task-candidates.d.ts +0 -6
  377. package/src/lib/migrations/0000000009000-task-candidates.js.map +0 -1
  378. package/src/lib/migrations/0000000010000-notification-delivery-state.d.ts +0 -6
  379. package/src/lib/migrations/0000000010000-notification-delivery-state.js.map +0 -1
  380. package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.d.ts +0 -6
  381. package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.js +0 -22
  382. package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.js.map +0 -1
  383. package/src/lib/migrations/0000000012000-notification-sla-idempotency.d.ts +0 -6
  384. package/src/lib/migrations/0000000012000-notification-sla-idempotency.js +0 -21
  385. package/src/lib/migrations/0000000012000-notification-sla-idempotency.js.map +0 -1
  386. package/src/lib/migrations/0000000013000-workflow-query-indexes.d.ts +0 -6
  387. package/src/lib/migrations/0000000013000-workflow-query-indexes.js +0 -80
  388. package/src/lib/migrations/0000000013000-workflow-query-indexes.js.map +0 -1
  389. package/src/lib/migrations/0000000014000-notification-resolution.d.ts +0 -6
  390. package/src/lib/migrations/0000000014000-notification-resolution.js +0 -30
  391. package/src/lib/migrations/0000000014000-notification-resolution.js.map +0 -1
  392. package/src/lib/migrations/0000000015000-backfill-stale-notification-resolution.d.ts +0 -15
  393. package/src/lib/migrations/0000000015000-backfill-stale-notification-resolution.js.map +0 -1
  394. package/src/lib/migrations/0000000016000-archive-parallel-form-drafts.d.ts +0 -15
  395. package/src/lib/migrations/0000000016000-archive-parallel-form-drafts.js.map +0 -1
  396. package/src/lib/migrations/0000000017000-adhoc-directives.d.ts +0 -6
  397. package/src/lib/migrations/0000000017000-adhoc-directives.js.map +0 -1
  398. package/src/lib/migrations/index.d.ts +0 -20
  399. package/src/lib/migrations/index.js +0 -61
  400. package/src/lib/migrations/index.js.map +0 -1
  401. package/src/lib/notification/dto/notification-preference.input.d.ts +0 -9
  402. package/src/lib/notification/dto/notification-preference.input.js +0 -53
  403. package/src/lib/notification/dto/notification-preference.input.js.map +0 -1
  404. package/src/lib/notification/index.js +0 -11
  405. package/src/lib/notification/index.js.map +0 -1
  406. package/src/lib/notification/notification-delivery-scheduler.service.d.ts +0 -13
  407. package/src/lib/notification/notification-delivery-scheduler.service.js +0 -53
  408. package/src/lib/notification/notification-delivery-scheduler.service.js.map +0 -1
  409. package/src/lib/notification/notification-delivery.service.d.ts +0 -23
  410. package/src/lib/notification/notification-delivery.service.js +0 -245
  411. package/src/lib/notification/notification-delivery.service.js.map +0 -1
  412. package/src/lib/notification/notification-dispatcher.token.d.ts +0 -7
  413. package/src/lib/notification/notification-dispatcher.token.js +0 -5
  414. package/src/lib/notification/notification-dispatcher.token.js.map +0 -1
  415. package/src/lib/notification/notification-options.d.ts +0 -215
  416. package/src/lib/notification/notification-options.js +0 -128
  417. package/src/lib/notification/notification-options.js.map +0 -1
  418. package/src/lib/notification/notification-options.module.d.ts +0 -23
  419. package/src/lib/notification/notification-options.module.js +0 -41
  420. package/src/lib/notification/notification-options.module.js.map +0 -1
  421. package/src/lib/notification/notification-preference.entity.d.ts +0 -10
  422. package/src/lib/notification/notification-preference.entity.js +0 -50
  423. package/src/lib/notification/notification-preference.entity.js.map +0 -1
  424. package/src/lib/notification/notification-sla-scheduler.service.d.ts +0 -13
  425. package/src/lib/notification/notification-sla-scheduler.service.js +0 -51
  426. package/src/lib/notification/notification-sla-scheduler.service.js.map +0 -1
  427. package/src/lib/notification/notification-template.d.ts +0 -13
  428. package/src/lib/notification/notification-template.js +0 -61
  429. package/src/lib/notification/notification-template.js.map +0 -1
  430. package/src/lib/notification/notification.entity.d.ts +0 -31
  431. package/src/lib/notification/notification.entity.js +0 -149
  432. package/src/lib/notification/notification.entity.js.map +0 -1
  433. package/src/lib/notification/notification.enums.d.ts +0 -38
  434. package/src/lib/notification/notification.enums.js +0 -63
  435. package/src/lib/notification/notification.enums.js.map +0 -1
  436. package/src/lib/notification/notification.module.d.ts +0 -2
  437. package/src/lib/notification/notification.module.js +0 -45
  438. package/src/lib/notification/notification.module.js.map +0 -1
  439. package/src/lib/notification/notification.mutations.d.ts +0 -11
  440. package/src/lib/notification/notification.mutations.js +0 -64
  441. package/src/lib/notification/notification.mutations.js.map +0 -1
  442. package/src/lib/notification/notification.queries.d.ts +0 -11
  443. package/src/lib/notification/notification.queries.js +0 -77
  444. package/src/lib/notification/notification.queries.js.map +0 -1
  445. package/src/lib/notification/notification.service.d.ts +0 -121
  446. package/src/lib/notification/notification.service.js +0 -713
  447. package/src/lib/notification/notification.service.js.map +0 -1
  448. package/src/lib/organization/dto/manager-resolution.input.d.ts +0 -18
  449. package/src/lib/organization/dto/manager-resolution.input.js +0 -107
  450. package/src/lib/organization/dto/manager-resolution.input.js.map +0 -1
  451. package/src/lib/organization/dto/membership.input.d.ts +0 -16
  452. package/src/lib/organization/dto/membership.input.js +0 -99
  453. package/src/lib/organization/dto/membership.input.js.map +0 -1
  454. package/src/lib/organization/dto/org-unit.input.d.ts +0 -24
  455. package/src/lib/organization/dto/org-unit.input.js +0 -119
  456. package/src/lib/organization/dto/org-unit.input.js.map +0 -1
  457. package/src/lib/organization/dto/position.input.d.ts +0 -13
  458. package/src/lib/organization/dto/position.input.js +0 -70
  459. package/src/lib/organization/dto/position.input.js.map +0 -1
  460. package/src/lib/organization/index.js +0 -16
  461. package/src/lib/organization/index.js.map +0 -1
  462. package/src/lib/organization/json-metadata.d.ts +0 -1
  463. package/src/lib/organization/json-metadata.js +0 -14
  464. package/src/lib/organization/json-metadata.js.map +0 -1
  465. package/src/lib/organization/manager-resolution.entity.d.ts +0 -11
  466. package/src/lib/organization/manager-resolution.entity.js +0 -55
  467. package/src/lib/organization/manager-resolution.entity.js.map +0 -1
  468. package/src/lib/organization/membership.entity.d.ts +0 -11
  469. package/src/lib/organization/membership.entity.js +0 -60
  470. package/src/lib/organization/membership.entity.js.map +0 -1
  471. package/src/lib/organization/org-unit-tree-commit-result.object.d.ts +0 -4
  472. package/src/lib/organization/org-unit-tree-commit-result.object.js +0 -17
  473. package/src/lib/organization/org-unit-tree-commit-result.object.js.map +0 -1
  474. package/src/lib/organization/org-unit.entity.d.ts +0 -13
  475. package/src/lib/organization/org-unit.entity.js +0 -64
  476. package/src/lib/organization/org-unit.entity.js.map +0 -1
  477. package/src/lib/organization/organization-summary.object.d.ts +0 -6
  478. package/src/lib/organization/organization-summary.object.js +0 -28
  479. package/src/lib/organization/organization-summary.object.js.map +0 -1
  480. package/src/lib/organization/organization.enums.d.ts +0 -11
  481. package/src/lib/organization/organization.enums.js +0 -22
  482. package/src/lib/organization/organization.enums.js.map +0 -1
  483. package/src/lib/organization/organization.module.d.ts +0 -2
  484. package/src/lib/organization/organization.module.js +0 -35
  485. package/src/lib/organization/organization.module.js.map +0 -1
  486. package/src/lib/organization/organization.mutations.d.ts +0 -26
  487. package/src/lib/organization/organization.mutations.js +0 -148
  488. package/src/lib/organization/organization.mutations.js.map +0 -1
  489. package/src/lib/organization/organization.queries.d.ts +0 -23
  490. package/src/lib/organization/organization.queries.js +0 -202
  491. package/src/lib/organization/organization.queries.js.map +0 -1
  492. package/src/lib/organization/organization.service.d.ts +0 -102
  493. package/src/lib/organization/organization.service.js +0 -751
  494. package/src/lib/organization/organization.service.js.map +0 -1
  495. package/src/lib/organization/position.entity.d.ts +0 -9
  496. package/src/lib/organization/position.entity.js +0 -48
  497. package/src/lib/organization/position.entity.js.map +0 -1
  498. package/src/lib/organization/resolved-manager.object.d.ts +0 -4
  499. package/src/lib/organization/resolved-manager.object.js +0 -20
  500. package/src/lib/organization/resolved-manager.object.js.map +0 -1
  501. package/src/lib/signature/index.js +0 -8
  502. package/src/lib/signature/index.js.map +0 -1
  503. package/src/lib/signature/signature-options.d.ts +0 -33
  504. package/src/lib/signature/signature-options.js +0 -51
  505. package/src/lib/signature/signature-options.js.map +0 -1
  506. package/src/lib/signature/signature-verification.object.d.ts +0 -6
  507. package/src/lib/signature/signature-verification.object.js +0 -28
  508. package/src/lib/signature/signature-verification.object.js.map +0 -1
  509. package/src/lib/signature/signature.entity.d.ts +0 -16
  510. package/src/lib/signature/signature.entity.js +0 -88
  511. package/src/lib/signature/signature.entity.js.map +0 -1
  512. package/src/lib/signature/signature.module.d.ts +0 -11
  513. package/src/lib/signature/signature.module.js +0 -60
  514. package/src/lib/signature/signature.module.js.map +0 -1
  515. package/src/lib/signature/signature.queries.d.ts +0 -11
  516. package/src/lib/signature/signature.queries.js +0 -46
  517. package/src/lib/signature/signature.queries.js.map +0 -1
  518. package/src/lib/signature/signature.service.d.ts +0 -25
  519. package/src/lib/signature/signature.service.js +0 -124
  520. package/src/lib/signature/signature.service.js.map +0 -1
  521. package/src/lib/template/approval-template-category.entity.d.ts +0 -9
  522. package/src/lib/template/approval-template-category.entity.js +0 -49
  523. package/src/lib/template/approval-template-category.entity.js.map +0 -1
  524. package/src/lib/template/approval-template-version.entity.d.ts +0 -21
  525. package/src/lib/template/approval-template-version.entity.js +0 -106
  526. package/src/lib/template/approval-template-version.entity.js.map +0 -1
  527. package/src/lib/template/approval-template.entity.d.ts +0 -14
  528. package/src/lib/template/approval-template.entity.js +0 -74
  529. package/src/lib/template/approval-template.entity.js.map +0 -1
  530. package/src/lib/template/compose-approval-template.object.d.ts +0 -11
  531. package/src/lib/template/compose-approval-template.object.js +0 -36
  532. package/src/lib/template/compose-approval-template.object.js.map +0 -1
  533. package/src/lib/template/compose-template.mutations.d.ts +0 -8
  534. package/src/lib/template/compose-template.mutations.js +0 -32
  535. package/src/lib/template/compose-template.mutations.js.map +0 -1
  536. package/src/lib/template/dto/approval-template.input.d.ts +0 -36
  537. package/src/lib/template/dto/approval-template.input.js +0 -187
  538. package/src/lib/template/dto/approval-template.input.js.map +0 -1
  539. package/src/lib/template/dto/compose-approval-template.input.d.ts +0 -17
  540. package/src/lib/template/dto/compose-approval-template.input.js +0 -99
  541. package/src/lib/template/dto/compose-approval-template.input.js.map +0 -1
  542. package/src/lib/template/index.js +0 -14
  543. package/src/lib/template/index.js.map +0 -1
  544. package/src/lib/template/template.enums.d.ts +0 -14
  545. package/src/lib/template/template.enums.js +0 -31
  546. package/src/lib/template/template.enums.js.map +0 -1
  547. package/src/lib/template/template.module.d.ts +0 -2
  548. package/src/lib/template/template.module.js +0 -41
  549. package/src/lib/template/template.module.js.map +0 -1
  550. package/src/lib/template/template.mutations.d.ts +0 -20
  551. package/src/lib/template/template.mutations.js +0 -139
  552. package/src/lib/template/template.mutations.js.map +0 -1
  553. package/src/lib/template/template.queries.d.ts +0 -16
  554. package/src/lib/template/template.queries.js +0 -133
  555. package/src/lib/template/template.queries.js.map +0 -1
  556. package/src/lib/template/template.service.d.ts +0 -83
  557. package/src/lib/template/template.service.js +0 -654
  558. package/src/lib/template/template.service.js.map +0 -1
  559. package/src/lib/template/workflow-definition.validator.d.ts +0 -9
  560. package/src/lib/template/workflow-definition.validator.js +0 -362
  561. package/src/lib/template/workflow-definition.validator.js.map +0 -1
  562. package/src/lib/testing/cel-js.jest.d.ts +0 -15
  563. package/src/lib/testing/cel-js.jest.js +0 -27
  564. package/src/lib/testing/cel-js.jest.js.map +0 -1
  565. package/src/lib/workflow-engine/activity-log.entity.d.ts +0 -12
  566. package/src/lib/workflow-engine/activity-log.entity.js +0 -62
  567. package/src/lib/workflow-engine/activity-log.entity.js.map +0 -1
  568. package/src/lib/workflow-engine/adhoc-directive.entity.d.ts +0 -31
  569. package/src/lib/workflow-engine/adhoc-directive.entity.js +0 -105
  570. package/src/lib/workflow-engine/adhoc-directive.entity.js.map +0 -1
  571. package/src/lib/workflow-engine/adhoc.enums.d.ts +0 -21
  572. package/src/lib/workflow-engine/adhoc.enums.js +0 -42
  573. package/src/lib/workflow-engine/adhoc.enums.js.map +0 -1
  574. package/src/lib/workflow-engine/approval-instance-page-info.object.d.ts +0 -8
  575. package/src/lib/workflow-engine/approval-instance-page-info.object.js +0 -36
  576. package/src/lib/workflow-engine/approval-instance-page-info.object.js.map +0 -1
  577. package/src/lib/workflow-engine/approval-instance.entity.d.ts +0 -22
  578. package/src/lib/workflow-engine/approval-instance.entity.js +0 -113
  579. package/src/lib/workflow-engine/approval-instance.entity.js.map +0 -1
  580. package/src/lib/workflow-engine/dto/adhoc-notification.input.d.ts +0 -6
  581. package/src/lib/workflow-engine/dto/adhoc-notification.input.js +0 -28
  582. package/src/lib/workflow-engine/dto/adhoc-notification.input.js.map +0 -1
  583. package/src/lib/workflow-engine/dto/adhoc-target.input.d.ts +0 -16
  584. package/src/lib/workflow-engine/dto/adhoc-target.input.js +0 -61
  585. package/src/lib/workflow-engine/dto/adhoc-target.input.js.map +0 -1
  586. package/src/lib/workflow-engine/dto/cancel-approval-instance.input.d.ts +0 -5
  587. package/src/lib/workflow-engine/dto/cancel-approval-instance.input.js +0 -29
  588. package/src/lib/workflow-engine/dto/cancel-approval-instance.input.js.map +0 -1
  589. package/src/lib/workflow-engine/dto/decide-task.input.d.ts +0 -9
  590. package/src/lib/workflow-engine/dto/decide-task.input.js +0 -47
  591. package/src/lib/workflow-engine/dto/decide-task.input.js.map +0 -1
  592. package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.d.ts +0 -6
  593. package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.js +0 -34
  594. package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.js.map +0 -1
  595. package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.d.ts +0 -6
  596. package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.js +0 -34
  597. package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.js.map +0 -1
  598. package/src/lib/workflow-engine/dto/submit-approval-instance.input.d.ts +0 -7
  599. package/src/lib/workflow-engine/dto/submit-approval-instance.input.js +0 -40
  600. package/src/lib/workflow-engine/dto/submit-approval-instance.input.js.map +0 -1
  601. package/src/lib/workflow-engine/index.js +0 -28
  602. package/src/lib/workflow-engine/index.js.map +0 -1
  603. package/src/lib/workflow-engine/task-candidate.entity.d.ts +0 -14
  604. package/src/lib/workflow-engine/task-candidate.entity.js +0 -72
  605. package/src/lib/workflow-engine/task-candidate.entity.js.map +0 -1
  606. package/src/lib/workflow-engine/task-decision.entity.d.ts +0 -12
  607. package/src/lib/workflow-engine/task-decision.entity.js +0 -60
  608. package/src/lib/workflow-engine/task-decision.entity.js.map +0 -1
  609. package/src/lib/workflow-engine/task.entity.d.ts +0 -25
  610. package/src/lib/workflow-engine/task.entity.js +0 -130
  611. package/src/lib/workflow-engine/task.entity.js.map +0 -1
  612. package/src/lib/workflow-engine/workflow-condition-evaluator.d.ts +0 -5
  613. package/src/lib/workflow-engine/workflow-condition-evaluator.js +0 -107
  614. package/src/lib/workflow-engine/workflow-condition-evaluator.js.map +0 -1
  615. package/src/lib/workflow-engine/workflow-dashboard-summary.object.d.ts +0 -8
  616. package/src/lib/workflow-engine/workflow-dashboard-summary.object.js +0 -36
  617. package/src/lib/workflow-engine/workflow-dashboard-summary.object.js.map +0 -1
  618. package/src/lib/workflow-engine/workflow-dry-run.object.d.ts +0 -21
  619. package/src/lib/workflow-engine/workflow-dry-run.object.js +0 -86
  620. package/src/lib/workflow-engine/workflow-dry-run.object.js.map +0 -1
  621. package/src/lib/workflow-engine/workflow-engine.enums.d.ts +0 -61
  622. package/src/lib/workflow-engine/workflow-engine.enums.js +0 -98
  623. package/src/lib/workflow-engine/workflow-engine.enums.js.map +0 -1
  624. package/src/lib/workflow-engine/workflow-engine.module.d.ts +0 -9
  625. package/src/lib/workflow-engine/workflow-engine.module.js +0 -78
  626. package/src/lib/workflow-engine/workflow-engine.module.js.map +0 -1
  627. package/src/lib/workflow-engine/workflow-engine.mutations.d.ts +0 -29
  628. package/src/lib/workflow-engine/workflow-engine.mutations.js +0 -218
  629. package/src/lib/workflow-engine/workflow-engine.mutations.js.map +0 -1
  630. package/src/lib/workflow-engine/workflow-engine.queries.d.ts +0 -31
  631. package/src/lib/workflow-engine/workflow-engine.queries.js +0 -237
  632. package/src/lib/workflow-engine/workflow-engine.queries.js.map +0 -1
  633. package/src/lib/workflow-engine/workflow-engine.service.d.ts +0 -188
  634. package/src/lib/workflow-engine/workflow-engine.service.js +0 -3561
  635. package/src/lib/workflow-engine/workflow-engine.service.js.map +0 -1
  636. package/src/lib/workflow-engine/workflow-engine.tokens.js +0 -5
  637. package/src/lib/workflow-engine/workflow-engine.tokens.js.map +0 -1
  638. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.d.ts +0 -18
  639. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js +0 -39
  640. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js.map +0 -1
  641. package/src/lib/workflow-engine/workflow-token.entity.d.ts +0 -10
  642. package/src/lib/workflow-engine/workflow-token.entity.js +0 -50
  643. package/src/lib/workflow-engine/workflow-token.entity.js.map +0 -1
  644. /package/src/{index.d.ts → index.ts} +0 -0
  645. /package/src/lib/attachment/{index.d.ts → index.ts} +0 -0
  646. /package/src/lib/bpm/{index.d.ts → index.ts} +0 -0
  647. /package/src/lib/bpm-auth/{index.d.ts → index.ts} +0 -0
  648. /package/src/lib/common/{index.d.ts → index.ts} +0 -0
  649. /package/src/lib/condition/{index.d.ts → index.ts} +0 -0
  650. /package/src/lib/database/{index.d.ts → index.ts} +0 -0
  651. /package/src/lib/delegation/{index.d.ts → index.ts} +0 -0
  652. /package/src/lib/form/{index.d.ts → index.ts} +0 -0
  653. /package/src/lib/identity/{index.d.ts → index.ts} +0 -0
  654. /package/src/lib/notification/{index.d.ts → index.ts} +0 -0
  655. /package/src/lib/organization/{index.d.ts → index.ts} +0 -0
  656. /package/src/lib/signature/{index.d.ts → index.ts} +0 -0
  657. /package/src/lib/template/{index.d.ts → index.ts} +0 -0
  658. /package/src/lib/workflow-engine/{index.d.ts → index.ts} +0 -0
@@ -0,0 +1,3345 @@
1
+ import { FormDefinitionSchema } from '@rytass/bpm-core-shared/form';
2
+ import {
3
+ ApproverResolver,
4
+ WorkflowDefinition,
5
+ } from '@rytass/bpm-core-shared/workflow';
6
+ import { ObjectLiteral } from 'typeorm';
7
+ import { BPMAuthContext } from '../bpm-auth';
8
+ import { AttachmentService } from '../attachment/attachment.service';
9
+ import { ConditionService } from '../condition/condition.service';
10
+ import {
11
+ DelegationResolution,
12
+ DelegationService,
13
+ } from '../delegation/delegation.service';
14
+ import { FormDefinitionVersionEntity } from '../form/form-definition-version.entity';
15
+ import { FormDefinitionVersionStatusEnum } from '../form/form.enums';
16
+ import { NotificationEntity } from '../notification/notification.entity';
17
+ import { NotificationResolutionEnum } from '../notification/notification.enums';
18
+ import { NotificationService } from '../notification/notification.service';
19
+ import { ManagerResolutionEntity } from '../organization/manager-resolution.entity';
20
+ import { MembershipEntity } from '../organization/membership.entity';
21
+ import { OrgUnitEntity } from '../organization/org-unit.entity';
22
+ import {
23
+ ManagerResolutionScopeTypeEnum,
24
+ OrgUnitTypeEnum,
25
+ } from '../organization/organization.enums';
26
+ import { SignatureEntity } from '../signature/signature.entity';
27
+ import { SignatureService } from '../signature/signature.service';
28
+ import { ApprovalTemplateVersionEntity } from '../template/approval-template-version.entity';
29
+ import { ApprovalTemplateEntity } from '../template/approval-template.entity';
30
+ import { ApprovalTemplateVersionStatusEnum } from '../template/template.enums';
31
+ import { ActivityLogEntity } from './activity-log.entity';
32
+ import { AdhocDirectiveEntity } from './adhoc-directive.entity';
33
+ import {
34
+ AdhocDirectiveStatusEnum,
35
+ AdhocDirectiveTypeEnum,
36
+ AdhocPreApprovalRejectBehaviorEnum,
37
+ AdhocTargetKindEnum,
38
+ } from './adhoc.enums';
39
+ import { ApprovalInstanceEntity } from './approval-instance.entity';
40
+ import { TaskCandidateEntity } from './task-candidate.entity';
41
+ import { TaskDecisionEntity } from './task-decision.entity';
42
+ import { TaskEntity } from './task.entity';
43
+ import {
44
+ ActivityLogEventTypeEnum,
45
+ ApprovalInstanceListViewEnum,
46
+ ApprovalInstanceStateEnum,
47
+ TaskAssignmentTypeEnum,
48
+ TaskCandidateStatusEnum,
49
+ TaskDecisionActionEnum,
50
+ TaskStatusEnum,
51
+ WorkflowTokenStatusEnum,
52
+ } from './workflow-engine.enums';
53
+ import { WorkflowEngineService } from './workflow-engine.service';
54
+ import { BPMWorkflowServiceTaskDispatcher } from './workflow-service-task-dispatcher.token';
55
+ import { WorkflowTokenEntity } from './workflow-token.entity';
56
+
57
+ describe('WorkflowEngineService', () => {
58
+ it('submits an approval instance with snapshots, start token, and activity logs', async (): Promise<void> => {
59
+ const fixture = createServiceFixture({
60
+ currentVersionId: 'template-version-1',
61
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
62
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
63
+ });
64
+
65
+ const instance = await fixture.service.submitApprovalInstance({
66
+ formDataJson: '{"amount":1000}',
67
+ initiatorMemberId: 'member-001',
68
+ initiatorMetadataSnapshotJson: '{"memberId":"member-001","roles":["IT"]}',
69
+ templateId: 'template-1',
70
+ title: null,
71
+ });
72
+
73
+ expect(instance.state).toBe(ApprovalInstanceStateEnum.RUNNING);
74
+ expect(instance.workflowSnapshot.nodes[0]?.id).toBe('start');
75
+ expect(instance.formDefinitionSnapshot).toMatchObject({
76
+ formDefinitionVersionId: 'form-version-1',
77
+ version: 1,
78
+ });
79
+ expect(instance.formData).toEqual({ amount: 1000 });
80
+ expect(fixture.savedToken).toMatchObject({
81
+ currentNodeId: 'start',
82
+ instanceId: 'instance-1',
83
+ status: WorkflowTokenStatusEnum.ACTIVE,
84
+ });
85
+ expect(fixture.savedActivityLogs.map((log) => log.eventType)).toEqual([
86
+ ActivityLogEventTypeEnum.INSTANCE_STARTED,
87
+ ActivityLogEventTypeEnum.TOKEN_CREATED,
88
+ ]);
89
+ });
90
+
91
+ it('builds initiator metadata from active memberships when submit omits a snapshot', async (): Promise<void> => {
92
+ const fixture = createServiceFixture({
93
+ currentVersionId: 'template-version-1',
94
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
95
+ processMemberships: [
96
+ createMembership({
97
+ id: 'membership-sales',
98
+ isPrimary: true,
99
+ orgUnitId: 'org-sales',
100
+ positionId: 'position-manager',
101
+ }),
102
+ createMembership({
103
+ id: 'membership-ops',
104
+ isPrimary: false,
105
+ orgUnitId: 'org-ops',
106
+ positionId: 'position-staff',
107
+ }),
108
+ createMembership({
109
+ effectiveTo: '2026-01-31',
110
+ id: 'membership-ended',
111
+ orgUnitId: 'org-ended',
112
+ positionId: 'position-ended',
113
+ }),
114
+ ],
115
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
116
+ });
117
+
118
+ await fixture.service.submitApprovalInstance({
119
+ formDataJson: '{"amount":1000}',
120
+ initiatorMemberId: 'member-001',
121
+ initiatorMetadataSnapshotJson: null,
122
+ templateId: 'template-1',
123
+ title: null,
124
+ });
125
+
126
+ expect(fixture.savedInstance?.initiatorMetadataSnapshot).toMatchObject({
127
+ memberId: 'member-001',
128
+ orgUnitIds: ['org-sales', 'org-ops'],
129
+ positionId: 'position-manager',
130
+ positionIds: ['position-manager', 'position-staff'],
131
+ primaryOrgUnitId: 'org-sales',
132
+ });
133
+ });
134
+
135
+ it('rejects submit when the template has no published current version', async (): Promise<void> => {
136
+ const fixture = createServiceFixture({
137
+ currentVersionId: null,
138
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
139
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
140
+ });
141
+
142
+ await expect(
143
+ fixture.service.submitApprovalInstance({
144
+ formDataJson: '{}',
145
+ initiatorMemberId: 'member-001',
146
+ initiatorMetadataSnapshotJson: null,
147
+ templateId: 'template-1',
148
+ title: 'Request',
149
+ }),
150
+ ).rejects.toThrow('Approval template does not have a published version');
151
+ });
152
+
153
+ it('rejects submit when required form fields are missing', async (): Promise<void> => {
154
+ const fixture = createServiceFixture({
155
+ currentVersionId: 'template-version-1',
156
+ formSchema: createRequiredReasonFormSchema(),
157
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
158
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
159
+ });
160
+
161
+ await expect(
162
+ fixture.service.submitApprovalInstance({
163
+ formDataJson: '{}',
164
+ initiatorMemberId: 'member-001',
165
+ initiatorMetadataSnapshotJson: null,
166
+ templateId: 'template-1',
167
+ title: 'Request',
168
+ }),
169
+ ).rejects.toThrow('Form data is missing required fields: 事由');
170
+ });
171
+
172
+ it('requires conditional fields only when their condition is visible and required', async (): Promise<void> => {
173
+ const fixture = createServiceFixture({
174
+ currentVersionId: 'template-version-1',
175
+ formSchema: createConditionalAttachmentFormSchema(),
176
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
177
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
178
+ });
179
+
180
+ await expect(
181
+ fixture.service.submitApprovalInstance({
182
+ formDataJson: '{"needsAttachment":false}',
183
+ initiatorMemberId: 'member-001',
184
+ initiatorMetadataSnapshotJson: null,
185
+ templateId: 'template-1',
186
+ title: 'Request',
187
+ }),
188
+ ).resolves.toMatchObject({ id: 'instance-1' });
189
+
190
+ await expect(
191
+ fixture.service.submitApprovalInstance({
192
+ formDataJson: '{"needsAttachment":true}',
193
+ initiatorMemberId: 'member-001',
194
+ initiatorMetadataSnapshotJson: null,
195
+ templateId: 'template-1',
196
+ title: 'Request',
197
+ }),
198
+ ).rejects.toThrow('Form data is missing required fields: 附件');
199
+ });
200
+
201
+ it('rejects resubmit when returned instance form data is missing required fields', async (): Promise<void> => {
202
+ const formSchema = createRequiredReasonFormSchema();
203
+ const fixture = createServiceFixture({
204
+ currentVersionId: 'template-version-1',
205
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
206
+ instanceState: ApprovalInstanceStateEnum.RETURNED,
207
+ processFormDefinitionSnapshot: { schema: formSchema },
208
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
209
+ });
210
+
211
+ await expect(
212
+ fixture.service.resubmitApprovalInstance({
213
+ formDataJson: '{}',
214
+ initiatorMemberId: 'member-001',
215
+ instanceId: 'instance-1',
216
+ title: 'Request',
217
+ }),
218
+ ).rejects.toThrow('Form data is missing required fields: 事由');
219
+ });
220
+
221
+ it('uses an advisory lock when processing an instance', async (): Promise<void> => {
222
+ const fixture = createServiceFixture({
223
+ currentVersionId: 'template-version-1',
224
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
225
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
226
+ });
227
+
228
+ await fixture.service.processInstance('instance-1');
229
+
230
+ expect(fixture.managerQuery).toHaveBeenCalledWith(
231
+ 'SELECT pg_advisory_xact_lock(hashtext($1))',
232
+ ['instance-1'],
233
+ );
234
+ expect(fixture.savedProcessLog?.eventType).toBe(
235
+ ActivityLogEventTypeEnum.ENGINE_PROCESS_REQUESTED,
236
+ );
237
+ });
238
+
239
+ it('advances a start token and creates a direct user task', async (): Promise<void> => {
240
+ const fixture = createServiceFixture({
241
+ currentVersionId: 'template-version-1',
242
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
243
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
244
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
245
+ });
246
+
247
+ await fixture.service.processInstance('instance-1');
248
+
249
+ expect(fixture.savedProcessToken).toMatchObject({
250
+ currentNodeId: 'task_finance',
251
+ status: WorkflowTokenStatusEnum.WAITING,
252
+ });
253
+ expect(fixture.savedTasks).toEqual([
254
+ expect.objectContaining({
255
+ assigneeMemberId: 'member-finance',
256
+ instanceId: 'instance-1',
257
+ nodeId: 'task_finance',
258
+ originalAssigneeMemberId: 'member-finance',
259
+ status: TaskStatusEnum.PENDING,
260
+ tokenId: 'token-1',
261
+ }),
262
+ ]);
263
+ expect(fixture.savedSingleActivityLogs.map((log) => log.eventType)).toEqual(
264
+ [
265
+ ActivityLogEventTypeEnum.ENGINE_PROCESS_REQUESTED,
266
+ ActivityLogEventTypeEnum.TOKEN_ADVANCED,
267
+ ActivityLogEventTypeEnum.TASK_CREATED,
268
+ ],
269
+ );
270
+ expect(
271
+ fixture.notificationService.createTaskAssignedNotification,
272
+ ).toHaveBeenCalledWith(
273
+ expect.objectContaining({
274
+ instance: expect.objectContaining({ id: 'instance-1' }),
275
+ node: expect.objectContaining({ id: 'task_finance' }),
276
+ task: expect.objectContaining({ assigneeMemberId: 'member-finance' }),
277
+ }),
278
+ );
279
+ });
280
+
281
+ it('sets task SLA due time from user task SLA config', async (): Promise<void> => {
282
+ const fixture = createServiceFixture({
283
+ currentVersionId: 'template-version-1',
284
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
285
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
286
+ slaDuration: 'PT2H',
287
+ }),
288
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
289
+ });
290
+
291
+ await fixture.service.processInstance('instance-1');
292
+
293
+ expect(fixture.savedTasks[0]?.slaDueAt).toBeInstanceOf(Date);
294
+ });
295
+
296
+ it('creates notifications from notify service task recipients', async (): Promise<void> => {
297
+ const fixture = createServiceFixture({
298
+ currentVersionId: 'template-version-1',
299
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
300
+ processWorkflowSnapshot: createNotifyServiceTaskWorkflow(),
301
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
302
+ });
303
+
304
+ await fixture.service.processInstance('instance-1');
305
+
306
+ expect(
307
+ fixture.notificationService.createServiceTaskNotifications,
308
+ ).toHaveBeenCalledWith(
309
+ expect.objectContaining({
310
+ instance: expect.objectContaining({ id: 'instance-1' }),
311
+ node: expect.objectContaining({ id: 'notify_finance' }),
312
+ recipientMemberIds: ['member-finance', 'member-admin'],
313
+ }),
314
+ );
315
+ expect(fixture.savedSingleActivityLogs.at(-1)).toMatchObject({
316
+ eventType: ActivityLogEventTypeEnum.TOKEN_ADVANCED,
317
+ payload: expect.objectContaining({
318
+ action: 'NOTIFY',
319
+ recipientMemberIds: ['member-finance', 'member-admin'],
320
+ }),
321
+ });
322
+ });
323
+
324
+ it('records webhook failures and continues processing', async (): Promise<void> => {
325
+ const fetchSpy = jest
326
+ .spyOn(globalThis, 'fetch')
327
+ .mockResolvedValueOnce(new Response('remote failure', { status: 500 }));
328
+ const fixture = createServiceFixture({
329
+ currentVersionId: 'template-version-1',
330
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
331
+ processWorkflowSnapshot: createWebhookServiceTaskWorkflow(),
332
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
333
+ });
334
+
335
+ await fixture.service.processInstance('instance-1');
336
+
337
+ expect(fetchSpy).toHaveBeenCalledWith(
338
+ 'https://example.test/workflow-hook',
339
+ expect.objectContaining({
340
+ body: expect.stringContaining('"title":"費用申請"'),
341
+ method: 'POST',
342
+ }),
343
+ );
344
+ expect(fixture.savedSingleActivityLogs).toContainEqual(
345
+ expect.objectContaining({
346
+ eventType: ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
347
+ payload: expect.objectContaining({
348
+ action: 'WEBHOOK',
349
+ ok: false,
350
+ status: 500,
351
+ }),
352
+ }),
353
+ );
354
+ expect(fixture.savedInstance).toMatchObject({
355
+ completedAt: expect.any(Date),
356
+ state: ApprovalInstanceStateEnum.APPROVED,
357
+ });
358
+
359
+ fetchSpy.mockRestore();
360
+ });
361
+
362
+ it('uses the injected workflow service task dispatcher for webhook nodes', async (): Promise<void> => {
363
+ const serviceTaskDispatcher: BPMWorkflowServiceTaskDispatcher = {
364
+ dispatchWebhook: jest.fn(() =>
365
+ Promise.resolve({ ok: true, status: 202 }),
366
+ ),
367
+ };
368
+ const fixture = createServiceFixture({
369
+ currentVersionId: 'template-version-1',
370
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
371
+ processFormData: { amount: 1000 },
372
+ processWorkflowSnapshot: createWebhookServiceTaskWorkflow(),
373
+ serviceTaskDispatcher,
374
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
375
+ });
376
+
377
+ await fixture.service.processInstance('instance-1');
378
+
379
+ expect(serviceTaskDispatcher.dispatchWebhook).toHaveBeenCalledWith({
380
+ headers: undefined,
381
+ payload: { amount: 1000, title: '費用申請' },
382
+ url: 'https://example.test/workflow-hook',
383
+ });
384
+ expect(fixture.savedSingleActivityLogs).toContainEqual(
385
+ expect.objectContaining({
386
+ eventType: ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED,
387
+ payload: expect.objectContaining({
388
+ action: 'WEBHOOK',
389
+ ok: true,
390
+ status: 202,
391
+ }),
392
+ }),
393
+ );
394
+ });
395
+
396
+ it('updates form data from set-form-field service tasks', async (): Promise<void> => {
397
+ const fixture = createServiceFixture({
398
+ currentVersionId: 'template-version-1',
399
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
400
+ processFormData: { amount: 1200 },
401
+ processWorkflowSnapshot: createSetFormFieldServiceTaskWorkflow(),
402
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
403
+ });
404
+
405
+ await fixture.service.processInstance('instance-1');
406
+
407
+ expect(fixture.savedSingleActivityLogs).toContainEqual(
408
+ expect.objectContaining({
409
+ eventType: ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED,
410
+ payload: expect.objectContaining({
411
+ action: 'SET_FORM_FIELD',
412
+ fieldPath: 'form.approvalLevel',
413
+ }),
414
+ }),
415
+ );
416
+ expect(fixture.savedInstance).toMatchObject({
417
+ formData: { amount: 1200, approvalLevel: '主管簽核' },
418
+ state: ApprovalInstanceStateEnum.APPROVED,
419
+ });
420
+ });
421
+
422
+ it('resolves a user task assignee from a selected position', async (): Promise<void> => {
423
+ const fixture = createServiceFixture({
424
+ currentVersionId: 'template-version-1',
425
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
426
+ processMemberships: [
427
+ createMembership({
428
+ isPrimary: true,
429
+ memberId: 'member-position-owner',
430
+ positionId: 'position-finance',
431
+ }),
432
+ ],
433
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
434
+ approverResolver: {
435
+ positionId: 'position-finance',
436
+ type: 'POSITION',
437
+ },
438
+ }),
439
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
440
+ });
441
+
442
+ await fixture.service.processInstance('instance-1');
443
+
444
+ expect(fixture.savedTasks[0]).toMatchObject({
445
+ assigneeMemberId: 'member-position-owner',
446
+ originalAssigneeMemberId: 'member-position-owner',
447
+ });
448
+ });
449
+
450
+ it('resolves a user task assignee from the initiator organization manager', async (): Promise<void> => {
451
+ const fixture = createServiceFixture({
452
+ currentVersionId: 'template-version-1',
453
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
454
+ processManagerResolutions: [
455
+ createManagerResolution({
456
+ managerMemberId: 'member-manager',
457
+ scopeId: 'org-finance',
458
+ scopeType: ManagerResolutionScopeTypeEnum.ORG_UNIT,
459
+ }),
460
+ ],
461
+ processMemberships: [
462
+ createMembership({
463
+ memberId: 'member-001',
464
+ orgUnitId: 'org-finance',
465
+ }),
466
+ ],
467
+ processOrgUnits: [
468
+ createOrgUnit({
469
+ id: 'org-finance',
470
+ path: 'org.finance',
471
+ }),
472
+ ],
473
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
474
+ approverResolver: {
475
+ baseFromInitiator: true,
476
+ levelsUp: 1,
477
+ type: 'ORG_MANAGER',
478
+ },
479
+ }),
480
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
481
+ });
482
+
483
+ await fixture.service.processInstance('instance-1');
484
+
485
+ expect(fixture.savedTasks[0]).toMatchObject({
486
+ assigneeMemberId: 'member-manager',
487
+ originalAssigneeMemberId: 'member-manager',
488
+ });
489
+ });
490
+
491
+ it('stops with a clear error when the initiator manager cannot be resolved', async (): Promise<void> => {
492
+ const fixture = createServiceFixture({
493
+ currentVersionId: 'template-version-1',
494
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
495
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
496
+ approverResolver: {
497
+ baseFromInitiator: true,
498
+ levelsUp: 1,
499
+ type: 'ORG_MANAGER',
500
+ },
501
+ }),
502
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
503
+ });
504
+
505
+ await expect(fixture.service.processInstance('instance-1')).rejects.toThrow(
506
+ '簽核節點「財務簽核」 無法建立待簽任務:找不到發起人的第 1 層主管,且未設定改派固定人。',
507
+ );
508
+ });
509
+
510
+ it('uses a fixed fallback assignee when the initiator manager cannot be resolved', async (): Promise<void> => {
511
+ const fixture = createServiceFixture({
512
+ currentVersionId: 'template-version-1',
513
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
514
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
515
+ approverResolver: {
516
+ baseFromInitiator: true,
517
+ fallback: {
518
+ memberId: 'member-admin',
519
+ type: 'DIRECT',
520
+ },
521
+ levelsUp: 1,
522
+ type: 'ORG_MANAGER',
523
+ },
524
+ }),
525
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
526
+ });
527
+
528
+ await fixture.service.processInstance('instance-1');
529
+
530
+ expect(fixture.savedTasks[0]).toMatchObject({
531
+ assigneeMemberId: 'member-admin',
532
+ originalAssigneeMemberId: 'member-admin',
533
+ });
534
+ });
535
+
536
+ it('rejects self fallback unless the workflow explicitly allows initiator self approval', async (): Promise<void> => {
537
+ const fixture = createServiceFixture({
538
+ currentVersionId: 'template-version-1',
539
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
540
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
541
+ approverResolver: {
542
+ baseFromInitiator: true,
543
+ fallback: {
544
+ memberId: 'member-001',
545
+ type: 'DIRECT',
546
+ },
547
+ levelsUp: 1,
548
+ type: 'ORG_MANAGER',
549
+ },
550
+ }),
551
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
552
+ });
553
+
554
+ await expect(fixture.service.processInstance('instance-1')).rejects.toThrow(
555
+ '改派固定人是申請人本人',
556
+ );
557
+ });
558
+
559
+ it('allows self fallback only when the workflow explicitly allows initiator self approval', async (): Promise<void> => {
560
+ const fixture = createServiceFixture({
561
+ currentVersionId: 'template-version-1',
562
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
563
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
564
+ approverResolver: {
565
+ baseFromInitiator: true,
566
+ fallback: {
567
+ allowInitiatorSelfApproval: true,
568
+ memberId: 'member-001',
569
+ type: 'DIRECT',
570
+ },
571
+ levelsUp: 1,
572
+ type: 'ORG_MANAGER',
573
+ },
574
+ }),
575
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
576
+ });
577
+
578
+ await fixture.service.processInstance('instance-1');
579
+
580
+ expect(fixture.savedTasks[0]).toMatchObject({
581
+ assigneeMemberId: 'member-001',
582
+ originalAssigneeMemberId: 'member-001',
583
+ });
584
+ });
585
+
586
+ it('resolves a user task assignee from a selected organization manager', async (): Promise<void> => {
587
+ const fixture = createServiceFixture({
588
+ currentVersionId: 'template-version-1',
589
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
590
+ processManagerResolutions: [
591
+ createManagerResolution({
592
+ managerMemberId: 'member-org-manager',
593
+ scopeId: 'org-finance',
594
+ scopeType: ManagerResolutionScopeTypeEnum.ORG_UNIT,
595
+ }),
596
+ ],
597
+ processOrgUnits: [
598
+ createOrgUnit({
599
+ id: 'org-finance',
600
+ path: 'org.finance',
601
+ }),
602
+ ],
603
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
604
+ approverResolver: {
605
+ orgUnitId: 'org-finance',
606
+ type: 'ORG_UNIT_MANAGER',
607
+ },
608
+ }),
609
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
610
+ });
611
+
612
+ await fixture.service.processInstance('instance-1');
613
+
614
+ expect(fixture.savedTasks[0]).toMatchObject({
615
+ assigneeMemberId: 'member-org-manager',
616
+ originalAssigneeMemberId: 'member-org-manager',
617
+ });
618
+ });
619
+
620
+ it('applies delegation when creating a user task', async (): Promise<void> => {
621
+ const fixture = createServiceFixture({
622
+ currentVersionId: 'template-version-1',
623
+ delegationResolution: {
624
+ delegationChain: [
625
+ {
626
+ from: 'member-finance',
627
+ reason: 'ALL',
628
+ ruleId: 'delegation-rule-1',
629
+ to: 'member-101',
630
+ },
631
+ ],
632
+ finalAssigneeMemberId: 'member-101',
633
+ },
634
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
635
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
636
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
637
+ });
638
+
639
+ await fixture.service.processInstance('instance-1');
640
+
641
+ expect(fixture.savedTasks).toEqual([
642
+ expect.objectContaining({
643
+ assigneeMemberId: 'member-101',
644
+ delegationChain: [
645
+ {
646
+ from: 'member-finance',
647
+ reason: 'ALL',
648
+ ruleId: 'delegation-rule-1',
649
+ to: 'member-101',
650
+ },
651
+ ],
652
+ originalAssigneeMemberId: 'member-finance',
653
+ }),
654
+ ]);
655
+ expect(fixture.savedSingleActivityLogs).toContainEqual(
656
+ expect.objectContaining({
657
+ eventType: ActivityLogEventTypeEnum.TASK_CREATED,
658
+ payload: expect.objectContaining({
659
+ assigneeMemberId: 'member-101',
660
+ originalAssigneeMemberId: 'member-finance',
661
+ }),
662
+ }),
663
+ );
664
+ });
665
+
666
+ it('approves a task and completes the linear instance', async (): Promise<void> => {
667
+ const fixture = createServiceFixture({
668
+ currentVersionId: 'template-version-1',
669
+ decisionTask: createTask({
670
+ nodeId: 'task_finance',
671
+ status: TaskStatusEnum.PENDING,
672
+ tokenId: 'token-1',
673
+ }),
674
+ decisionToken: createWorkflowToken({
675
+ currentNodeId: 'task_finance',
676
+ status: WorkflowTokenStatusEnum.WAITING,
677
+ }),
678
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
679
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
680
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
681
+ });
682
+
683
+ const decision = await fixture.service.decideTask({
684
+ action: TaskDecisionActionEnum.APPROVED,
685
+ comment: '同意',
686
+ decidedByMemberId: 'member-finance',
687
+ taskId: 'task-1',
688
+ });
689
+
690
+ expect(decision).toMatchObject({
691
+ action: TaskDecisionActionEnum.APPROVED,
692
+ comment: '同意',
693
+ decidedByMemberId: 'member-finance',
694
+ taskId: 'task-1',
695
+ });
696
+ expect(fixture.savedProcessToken).toMatchObject({
697
+ currentNodeId: 'end',
698
+ status: WorkflowTokenStatusEnum.CONSUMED,
699
+ });
700
+ expect(fixture.savedInstance).toMatchObject({
701
+ completedAt: expect.any(Date),
702
+ state: ApprovalInstanceStateEnum.APPROVED,
703
+ });
704
+ expect(
705
+ fixture.notificationService.createInstanceCompletedNotification,
706
+ ).toHaveBeenCalledWith(
707
+ expect.objectContaining({
708
+ instance: expect.objectContaining({
709
+ id: 'instance-1',
710
+ state: ApprovalInstanceStateEnum.APPROVED,
711
+ }),
712
+ }),
713
+ );
714
+ expect(
715
+ fixture.notificationService.resolveTaskNotifications,
716
+ ).toHaveBeenCalledWith(
717
+ expect.objectContaining({
718
+ resolution: NotificationResolutionEnum.APPROVED,
719
+ supersedeOthers: true,
720
+ }),
721
+ );
722
+ });
723
+
724
+ it('requires a rejection comment before rejecting a task', async (): Promise<void> => {
725
+ const fixture = createServiceFixture({
726
+ currentVersionId: 'template-version-1',
727
+ decisionTask: createTask({
728
+ nodeId: 'task_finance',
729
+ status: TaskStatusEnum.PENDING,
730
+ tokenId: 'token-1',
731
+ }),
732
+ decisionToken: createWorkflowToken({
733
+ currentNodeId: 'task_finance',
734
+ status: WorkflowTokenStatusEnum.WAITING,
735
+ }),
736
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
737
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
738
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
739
+ });
740
+
741
+ await expect(
742
+ fixture.service.decideTask({
743
+ action: TaskDecisionActionEnum.REJECTED,
744
+ comment: ' ',
745
+ decidedByMemberId: 'member-finance',
746
+ taskId: 'task-1',
747
+ }),
748
+ ).rejects.toThrow('Reject decision comment is required');
749
+ });
750
+
751
+ it('rejects a task with a rejection comment', async (): Promise<void> => {
752
+ const fixture = createServiceFixture({
753
+ currentVersionId: 'template-version-1',
754
+ decisionTask: createTask({
755
+ nodeId: 'task_finance',
756
+ status: TaskStatusEnum.PENDING,
757
+ tokenId: 'token-1',
758
+ }),
759
+ decisionToken: createWorkflowToken({
760
+ currentNodeId: 'task_finance',
761
+ status: WorkflowTokenStatusEnum.WAITING,
762
+ }),
763
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
764
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
765
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
766
+ });
767
+
768
+ const decision = await fixture.service.decideTask({
769
+ action: TaskDecisionActionEnum.REJECTED,
770
+ comment: ' 資料不足,請補件 ',
771
+ decidedByMemberId: 'member-finance',
772
+ taskId: 'task-1',
773
+ });
774
+
775
+ expect(decision).toMatchObject({
776
+ action: TaskDecisionActionEnum.REJECTED,
777
+ comment: '資料不足,請補件',
778
+ decidedByMemberId: 'member-finance',
779
+ taskId: 'task-1',
780
+ });
781
+ expect(fixture.savedSingleActivityLogs).toContainEqual(
782
+ expect.objectContaining({
783
+ eventType: ActivityLogEventTypeEnum.TASK_DECIDED,
784
+ payload: expect.objectContaining({
785
+ action: TaskDecisionActionEnum.REJECTED,
786
+ comment: '資料不足,請補件',
787
+ }),
788
+ }),
789
+ );
790
+ expect(fixture.savedInstance).toMatchObject({
791
+ completedAt: expect.any(Date),
792
+ state: ApprovalInstanceStateEnum.REJECTED,
793
+ });
794
+ expect(
795
+ fixture.notificationService.resolveTaskNotifications,
796
+ ).toHaveBeenCalledWith(
797
+ expect.objectContaining({
798
+ resolution: NotificationResolutionEnum.REJECTED,
799
+ supersedeOthers: true,
800
+ }),
801
+ );
802
+ });
803
+
804
+ it('transfers a task without advancing the token', async (): Promise<void> => {
805
+ const fixture = createServiceFixture({
806
+ currentVersionId: 'template-version-1',
807
+ decisionTask: createTask({
808
+ assigneeMemberId: 'member-finance',
809
+ nodeId: 'task_finance',
810
+ originalAssigneeMemberId: 'member-finance',
811
+ status: TaskStatusEnum.PENDING,
812
+ tokenId: 'token-1',
813
+ }),
814
+ decisionToken: createWorkflowToken({
815
+ currentNodeId: 'task_finance',
816
+ status: WorkflowTokenStatusEnum.WAITING,
817
+ }),
818
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
819
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
820
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
821
+ });
822
+
823
+ const decision = await fixture.service.decideTask({
824
+ action: TaskDecisionActionEnum.TRANSFERRED,
825
+ comment: '請改由財務主管處理',
826
+ decidedByMemberId: 'member-finance',
827
+ taskId: 'task-1',
828
+ transferToMemberId: 'member-101',
829
+ });
830
+
831
+ expect(decision).toMatchObject({
832
+ action: TaskDecisionActionEnum.TRANSFERRED,
833
+ transferToMemberId: 'member-101',
834
+ });
835
+ expect(fixture.savedTasks).toEqual(
836
+ expect.arrayContaining([
837
+ expect.objectContaining({
838
+ id: 'task-1',
839
+ status: TaskStatusEnum.TRANSFERRED,
840
+ }),
841
+ expect.objectContaining({
842
+ assigneeMemberId: 'member-101',
843
+ delegationChain: [
844
+ {
845
+ from: 'member-finance',
846
+ reason: 'MANUAL_TRANSFER',
847
+ ruleId: null,
848
+ to: 'member-101',
849
+ },
850
+ ],
851
+ originalAssigneeMemberId: 'member-finance',
852
+ status: TaskStatusEnum.PENDING,
853
+ tokenId: 'token-1',
854
+ }),
855
+ ]),
856
+ );
857
+ expect(fixture.savedProcessToken).toBeNull();
858
+ expect(fixture.savedActivityLogs.map((log) => log.eventType)).toEqual([
859
+ ActivityLogEventTypeEnum.TASK_DECIDED,
860
+ ActivityLogEventTypeEnum.TASK_CREATED,
861
+ ]);
862
+ expect(
863
+ fixture.notificationService.resolveTaskNotifications,
864
+ ).toHaveBeenCalledWith(
865
+ expect.objectContaining({
866
+ resolution: NotificationResolutionEnum.TRANSFERRED,
867
+ supersedeOthers: true,
868
+ }),
869
+ );
870
+ });
871
+
872
+ it('lists pending inbox tasks by assignee', async (): Promise<void> => {
873
+ const fixture = createServiceFixture({
874
+ currentVersionId: 'template-version-1',
875
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
876
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
877
+ });
878
+
879
+ await fixture.service.listInboxTasks('member-finance');
880
+
881
+ expect(fixture.rootTaskFind).toHaveBeenCalledWith({
882
+ order: { createdAt: 'DESC' },
883
+ where: [
884
+ {
885
+ assigneeMemberId: 'member-finance',
886
+ status: expect.any(Object),
887
+ },
888
+ ],
889
+ });
890
+ });
891
+
892
+ it('lists completed approval history tasks by assignee', async (): Promise<void> => {
893
+ const fixture = createServiceFixture({
894
+ currentVersionId: 'template-version-1',
895
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
896
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
897
+ });
898
+
899
+ await fixture.service.listApprovalHistoryTasks('member-finance');
900
+
901
+ expect(fixture.rootTaskFind).toHaveBeenCalledWith({
902
+ order: { completedAt: 'DESC', createdAt: 'DESC' },
903
+ take: 50,
904
+ where: {
905
+ assigneeMemberId: 'member-finance',
906
+ status: TaskStatusEnum.COMPLETED,
907
+ },
908
+ });
909
+ });
910
+
911
+ it('counts and pages filtered approval instances without loading an unpaged list in the client', async (): Promise<void> => {
912
+ const fixture = createServiceFixture({
913
+ currentVersionId: 'template-version-1',
914
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
915
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
916
+ });
917
+ const instances = [
918
+ createApprovalInstance({
919
+ id: 'purchase-1',
920
+ title: '採購請款 A',
921
+ }),
922
+ createApprovalInstance({
923
+ id: 'purchase-2',
924
+ title: '採購請款 B',
925
+ }),
926
+ createApprovalInstance({
927
+ id: 'leave-1',
928
+ title: '請假申請',
929
+ }),
930
+ ];
931
+
932
+ fixture.rootInstanceFind.mockResolvedValue(instances);
933
+ fixture.rootInstanceQueryBuilder.getMany.mockResolvedValue([instances[0]]);
934
+ fixture.rootInstanceCount.mockResolvedValue(2);
935
+
936
+ await expect(
937
+ fixture.service.listApprovalInstances(undefined, {
938
+ page: 1,
939
+ pageSize: 1,
940
+ searchText: '採購',
941
+ view: ApprovalInstanceListViewEnum.ALL,
942
+ }),
943
+ ).resolves.toEqual([instances[0]]);
944
+ await expect(
945
+ fixture.service.countApprovalInstances(undefined, {
946
+ searchText: '採購',
947
+ view: ApprovalInstanceListViewEnum.ALL,
948
+ }),
949
+ ).resolves.toBe(2);
950
+ await expect(
951
+ fixture.service.readApprovalInstancePageInfo(undefined, {
952
+ page: 1,
953
+ pageSize: 1,
954
+ searchText: '採購',
955
+ view: ApprovalInstanceListViewEnum.ALL,
956
+ }),
957
+ ).resolves.toEqual({
958
+ hasNextPage: true,
959
+ hasPreviousPage: false,
960
+ page: 1,
961
+ pageSize: 1,
962
+ totalCount: 2,
963
+ totalPages: 2,
964
+ });
965
+ expect(fixture.rootInstanceQueryBuilder.skip).toHaveBeenCalledWith(0);
966
+ expect(fixture.rootInstanceQueryBuilder.take).toHaveBeenCalledWith(1);
967
+ expect(fixture.rootInstanceQueryBuilder.getMany).toHaveBeenCalledTimes(1);
968
+ expect(fixture.rootInstanceCount).toHaveBeenCalledTimes(2);
969
+ });
970
+
971
+ it('filters readable approval instances with SQL EXISTS conditions instead of loading readable id lists', async (): Promise<void> => {
972
+ const fixture = createServiceFixture({
973
+ currentVersionId: 'template-version-1',
974
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
975
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
976
+ });
977
+
978
+ await fixture.service.listApprovalInstances(
979
+ createAuthContext('member-finance'),
980
+ {
981
+ page: 1,
982
+ pageSize: 20,
983
+ view: ApprovalInstanceListViewEnum.ALL,
984
+ },
985
+ );
986
+
987
+ expect(fixture.rootInstanceQueryBuilder.andWhere).toHaveBeenCalledWith(
988
+ expect.stringContaining('FROM "task_candidates"'),
989
+ { readableMemberId: 'member-finance' },
990
+ );
991
+ expect(fixture.rootInstanceQueryBuilder.andWhere).toHaveBeenCalledWith(
992
+ expect.stringContaining('FROM "task_decisions"'),
993
+ { readableMemberId: 'member-finance' },
994
+ );
995
+ expect(fixture.rootTaskFind).not.toHaveBeenCalled();
996
+ expect(fixture.notificationFind).not.toHaveBeenCalled();
997
+ });
998
+
999
+ it('filters CC approval instances with a notification EXISTS condition', async (): Promise<void> => {
1000
+ const fixture = createServiceFixture({
1001
+ currentVersionId: 'template-version-1',
1002
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1003
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1004
+ });
1005
+
1006
+ await fixture.service.listApprovalInstances(
1007
+ createAuthContext('member-finance'),
1008
+ {
1009
+ view: ApprovalInstanceListViewEnum.CC,
1010
+ },
1011
+ );
1012
+
1013
+ expect(fixture.rootInstanceQueryBuilder.andWhere).toHaveBeenCalledWith(
1014
+ expect.stringContaining('FROM "notifications"'),
1015
+ { readableMemberId: 'member-finance' },
1016
+ );
1017
+ expect(fixture.notificationFind).not.toHaveBeenCalled();
1018
+ });
1019
+
1020
+ it('routes an exclusive gateway through the first matching condition', async (): Promise<void> => {
1021
+ const fixture = createServiceFixture({
1022
+ currentVersionId: 'template-version-1',
1023
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1024
+ processFormData: { amount: 1500 },
1025
+ processWorkflowSnapshot: createExclusiveGatewayWorkflow(),
1026
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1027
+ });
1028
+
1029
+ await fixture.service.processInstance('instance-1');
1030
+
1031
+ expect(fixture.savedTasks).toEqual([
1032
+ expect.objectContaining({
1033
+ assigneeMemberId: 'member-high',
1034
+ nodeId: 'task_high',
1035
+ status: TaskStatusEnum.PENDING,
1036
+ }),
1037
+ ]);
1038
+ });
1039
+
1040
+ it('completes an exclusive gateway branch after the selected task is approved', async (): Promise<void> => {
1041
+ const fixture = createServiceFixture({
1042
+ currentVersionId: 'template-version-1',
1043
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1044
+ processFormData: { amount: 1500 },
1045
+ processWorkflowSnapshot: createExclusiveGatewayWorkflow(),
1046
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1047
+ });
1048
+
1049
+ await fixture.service.processInstance('instance-1');
1050
+
1051
+ const taskHigh = readTaskByNodeId(fixture.savedTasks, 'task_high');
1052
+
1053
+ await fixture.service.decideTask({
1054
+ action: TaskDecisionActionEnum.APPROVED,
1055
+ comment: null,
1056
+ decidedByMemberId: taskHigh.assigneeMemberId ?? '',
1057
+ taskId: taskHigh.id,
1058
+ });
1059
+
1060
+ expect(fixture.savedInstance).toMatchObject({
1061
+ state: ApprovalInstanceStateEnum.APPROVED,
1062
+ });
1063
+ expect(fixture.savedWorkflowTokens).toEqual(
1064
+ expect.arrayContaining([
1065
+ expect.objectContaining({
1066
+ currentNodeId: 'end',
1067
+ status: WorkflowTokenStatusEnum.CONSUMED,
1068
+ }),
1069
+ ]),
1070
+ );
1071
+ });
1072
+
1073
+ it('routes an exclusive gateway through the default edge when no condition matches', async (): Promise<void> => {
1074
+ const fixture = createServiceFixture({
1075
+ currentVersionId: 'template-version-1',
1076
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1077
+ processFormData: { amount: 500 },
1078
+ processWorkflowSnapshot: createExclusiveGatewayWorkflow(),
1079
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1080
+ });
1081
+
1082
+ await fixture.service.processInstance('instance-1');
1083
+
1084
+ expect(fixture.savedTasks).toEqual([
1085
+ expect.objectContaining({
1086
+ assigneeMemberId: 'member-default',
1087
+ nodeId: 'task_default',
1088
+ status: TaskStatusEnum.PENDING,
1089
+ }),
1090
+ ]);
1091
+ });
1092
+
1093
+ it('routes an exclusive gateway with a pure CEL edge expression', async (): Promise<void> => {
1094
+ const fixture = createServiceFixture({
1095
+ currentVersionId: 'template-version-1',
1096
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1097
+ processFormData: { amount: 1500 },
1098
+ processWorkflowSnapshot: createExclusiveGatewayWorkflow({
1099
+ highCondition:
1100
+ 'form.amount > 1000 && initiator.memberId == "member-001"',
1101
+ includeStructuredCondition: false,
1102
+ }),
1103
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1104
+ });
1105
+
1106
+ await fixture.service.processInstance('instance-1');
1107
+
1108
+ expect(fixture.savedTasks).toEqual([
1109
+ expect.objectContaining({
1110
+ assigneeMemberId: 'member-high',
1111
+ nodeId: 'task_high',
1112
+ status: TaskStatusEnum.PENDING,
1113
+ }),
1114
+ ]);
1115
+ });
1116
+
1117
+ it('reports dry run edge labels, default routing, and entry condition results', (): void => {
1118
+ const fixture = createServiceFixture({
1119
+ currentVersionId: 'template-version-1',
1120
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1121
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1122
+ });
1123
+
1124
+ const result = fixture.service.dryRunApprovalWorkflow({
1125
+ formDataJson: '{"amount":500}',
1126
+ initiatorMemberId: 'member-001',
1127
+ initiatorMetadataSnapshotJson: null,
1128
+ workflowDefinitionJson: JSON.stringify(createExclusiveGatewayWorkflow()),
1129
+ });
1130
+
1131
+ expect(result.valid).toBe(true);
1132
+ expect(result.steps).toEqual(
1133
+ expect.arrayContaining([
1134
+ expect.objectContaining({
1135
+ edgeDefault: true,
1136
+ edgeLabel: '其他情況',
1137
+ edgeMatched: true,
1138
+ edgeReason: '其他條件不符合時採用預設路徑。',
1139
+ nodeId: 'task_default',
1140
+ status: 'WAITING',
1141
+ }),
1142
+ ]),
1143
+ );
1144
+ });
1145
+
1146
+ it('reports dry run matches for pure CEL edge expressions', (): void => {
1147
+ const fixture = createServiceFixture({
1148
+ currentVersionId: 'template-version-1',
1149
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1150
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1151
+ });
1152
+
1153
+ const result = fixture.service.dryRunApprovalWorkflow({
1154
+ formDataJson: '{"amount":1500}',
1155
+ initiatorMemberId: 'member-001',
1156
+ initiatorMetadataSnapshotJson: null,
1157
+ workflowDefinitionJson: JSON.stringify(
1158
+ createExclusiveGatewayWorkflow({
1159
+ highCondition:
1160
+ 'form.amount > 1000 && initiator.memberId == "member-001"',
1161
+ includeStructuredCondition: false,
1162
+ }),
1163
+ ),
1164
+ });
1165
+
1166
+ expect(result.valid).toBe(true);
1167
+ expect(result.steps).toEqual(
1168
+ expect.arrayContaining([
1169
+ expect.objectContaining({
1170
+ edgeDefault: false,
1171
+ edgeLabel: '金額大於 1000',
1172
+ edgeMatched: true,
1173
+ edgeReason:
1174
+ '條件成立:form.amount > 1000 && initiator.memberId == "member-001"',
1175
+ nodeId: 'task_high',
1176
+ status: 'WAITING',
1177
+ }),
1178
+ ]),
1179
+ );
1180
+ });
1181
+
1182
+ it('resubmits returned instances from the return point when configured', async (): Promise<void> => {
1183
+ const fixture = createServiceFixture({
1184
+ currentVersionId: 'template-version-1',
1185
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1186
+ instanceState: ApprovalInstanceStateEnum.RETURNED,
1187
+ latestReturnActivity: createActivityLog({
1188
+ eventType: ActivityLogEventTypeEnum.INSTANCE_RETURNED,
1189
+ nodeId: 'task_finance',
1190
+ payload: {
1191
+ resubmitStrategy: 'FROM_RETURN_POINT',
1192
+ returnedFromNodeId: 'task_finance',
1193
+ returnToNodeId: 'start',
1194
+ taskId: 'task-1',
1195
+ },
1196
+ }),
1197
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
1198
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1199
+ });
1200
+
1201
+ await fixture.service.resubmitApprovalInstance({
1202
+ formDataJson: '{"amount":1200}',
1203
+ initiatorMemberId: 'member-001',
1204
+ instanceId: 'instance-1',
1205
+ title: null,
1206
+ });
1207
+
1208
+ expect(fixture.savedWorkflowTokens).toEqual(
1209
+ expect.arrayContaining([
1210
+ expect.objectContaining({
1211
+ currentNodeId: 'task_finance',
1212
+ status: WorkflowTokenStatusEnum.WAITING,
1213
+ }),
1214
+ ]),
1215
+ );
1216
+ expect(fixture.savedTasks).toEqual(
1217
+ expect.arrayContaining([
1218
+ expect.objectContaining({
1219
+ nodeId: 'task_finance',
1220
+ status: TaskStatusEnum.PENDING,
1221
+ }),
1222
+ ]),
1223
+ );
1224
+ expect(fixture.savedSingleActivityLogs).toEqual(
1225
+ expect.arrayContaining([
1226
+ expect.objectContaining({
1227
+ eventType: ActivityLogEventTypeEnum.INSTANCE_RESUBMITTED,
1228
+ payload: { resubmitStrategy: 'FROM_RETURN_POINT' },
1229
+ }),
1230
+ ]),
1231
+ );
1232
+ });
1233
+
1234
+ it('forks a token across multiple outgoing edges and creates parallel tasks', async (): Promise<void> => {
1235
+ const fixture = createServiceFixture({
1236
+ currentVersionId: 'template-version-1',
1237
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1238
+ processWorkflowSnapshot: createParallelApprovalWorkflow('AND'),
1239
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1240
+ });
1241
+
1242
+ await fixture.service.processInstance('instance-1');
1243
+
1244
+ expect(
1245
+ fixture.savedTasks.map((task) => ({
1246
+ assigneeMemberId: task.assigneeMemberId,
1247
+ nodeId: task.nodeId,
1248
+ status: task.status,
1249
+ })),
1250
+ ).toEqual([
1251
+ {
1252
+ assigneeMemberId: 'member-a',
1253
+ nodeId: 'task_a',
1254
+ status: TaskStatusEnum.PENDING,
1255
+ },
1256
+ {
1257
+ assigneeMemberId: 'member-b',
1258
+ nodeId: 'task_b',
1259
+ status: TaskStatusEnum.PENDING,
1260
+ },
1261
+ ]);
1262
+ });
1263
+
1264
+ it('waits for every incoming branch before triggering an AND predecessor node', async (): Promise<void> => {
1265
+ const fixture = createServiceFixture({
1266
+ currentVersionId: 'template-version-1',
1267
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1268
+ processWorkflowSnapshot: createParallelApprovalWorkflow('AND'),
1269
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1270
+ });
1271
+
1272
+ await fixture.service.processInstance('instance-1');
1273
+
1274
+ const taskA = readTaskByNodeId(fixture.savedTasks, 'task_a');
1275
+ const taskB = readTaskByNodeId(fixture.savedTasks, 'task_b');
1276
+
1277
+ await fixture.service.decideTask({
1278
+ action: TaskDecisionActionEnum.APPROVED,
1279
+ comment: null,
1280
+ decidedByMemberId: taskA.assigneeMemberId ?? '',
1281
+ taskId: taskA.id,
1282
+ });
1283
+
1284
+ expect(
1285
+ fixture.savedTasks.some((task) => task.nodeId === 'task_final'),
1286
+ ).toBe(false);
1287
+ expect(
1288
+ fixture.savedWorkflowTokens.some(
1289
+ (token) =>
1290
+ token.currentNodeId === 'task_final' &&
1291
+ token.status === WorkflowTokenStatusEnum.WAITING,
1292
+ ),
1293
+ ).toBe(true);
1294
+
1295
+ await fixture.service.decideTask({
1296
+ action: TaskDecisionActionEnum.APPROVED,
1297
+ comment: null,
1298
+ decidedByMemberId: taskB.assigneeMemberId ?? '',
1299
+ taskId: taskB.id,
1300
+ });
1301
+
1302
+ expect(fixture.savedTasks).toEqual(
1303
+ expect.arrayContaining([
1304
+ expect.objectContaining({
1305
+ assigneeMemberId: 'member-final',
1306
+ nodeId: 'task_final',
1307
+ status: TaskStatusEnum.PENDING,
1308
+ }),
1309
+ ]),
1310
+ );
1311
+ });
1312
+
1313
+ it('triggers an OR predecessor node from the first completed branch and cancels alternatives', async (): Promise<void> => {
1314
+ const fixture = createServiceFixture({
1315
+ currentVersionId: 'template-version-1',
1316
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1317
+ processWorkflowSnapshot: createParallelApprovalWorkflow('OR'),
1318
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1319
+ });
1320
+
1321
+ await fixture.service.processInstance('instance-1');
1322
+
1323
+ const taskA = readTaskByNodeId(fixture.savedTasks, 'task_a');
1324
+ const taskB = readTaskByNodeId(fixture.savedTasks, 'task_b');
1325
+
1326
+ await fixture.service.decideTask({
1327
+ action: TaskDecisionActionEnum.APPROVED,
1328
+ comment: null,
1329
+ decidedByMemberId: taskA.assigneeMemberId ?? '',
1330
+ taskId: taskA.id,
1331
+ });
1332
+
1333
+ expect(fixture.savedTasks).toEqual(
1334
+ expect.arrayContaining([
1335
+ expect.objectContaining({
1336
+ assigneeMemberId: 'member-final',
1337
+ nodeId: 'task_final',
1338
+ status: TaskStatusEnum.PENDING,
1339
+ }),
1340
+ expect.objectContaining({
1341
+ id: taskB.id,
1342
+ nodeId: 'task_b',
1343
+ status: TaskStatusEnum.CANCELLED,
1344
+ }),
1345
+ ]),
1346
+ );
1347
+ });
1348
+
1349
+ it('allows an instance initiator to read the approval instance', async (): Promise<void> => {
1350
+ const fixture = createServiceFixture({
1351
+ currentVersionId: 'template-version-1',
1352
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1353
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1354
+ });
1355
+
1356
+ await expect(
1357
+ fixture.service.getApprovalInstance(
1358
+ 'instance-1',
1359
+ createAuthContext('member-001'),
1360
+ ),
1361
+ ).resolves.toMatchObject({ id: 'instance-1' });
1362
+ });
1363
+
1364
+ it('allows a related task assignee to read the approval instance', async (): Promise<void> => {
1365
+ const fixture = createServiceFixture({
1366
+ currentVersionId: 'template-version-1',
1367
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1368
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1369
+ });
1370
+
1371
+ fixture.rootTaskFind.mockResolvedValueOnce([
1372
+ createTask({ assigneeMemberId: 'member-202' }),
1373
+ ]);
1374
+
1375
+ await expect(
1376
+ fixture.service.getApprovalInstance(
1377
+ 'instance-1',
1378
+ createAuthContext('member-202'),
1379
+ ),
1380
+ ).resolves.toMatchObject({ id: 'instance-1' });
1381
+ });
1382
+
1383
+ it('hides an approval instance from unrelated members', async (): Promise<void> => {
1384
+ const fixture = createServiceFixture({
1385
+ currentVersionId: 'template-version-1',
1386
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1387
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1388
+ });
1389
+
1390
+ await expect(
1391
+ fixture.service.getApprovalInstance(
1392
+ 'instance-1',
1393
+ createAuthContext('member-unrelated'),
1394
+ ),
1395
+ ).rejects.toThrow('Approval instance instance-1 was not found');
1396
+ });
1397
+
1398
+ it('rejects ad-hoc countersign requests when the node disallows added signers', async (): Promise<void> => {
1399
+ const fixture = createServiceFixture({
1400
+ currentVersionId: 'template-version-1',
1401
+ decisionTask: createTask({
1402
+ nodeId: 'task_finance',
1403
+ status: TaskStatusEnum.PENDING,
1404
+ tokenId: 'token-1',
1405
+ }),
1406
+ decisionToken: createWorkflowToken({
1407
+ currentNodeId: 'task_finance',
1408
+ status: WorkflowTokenStatusEnum.WAITING,
1409
+ }),
1410
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1411
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
1412
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1413
+ });
1414
+
1415
+ await expect(
1416
+ fixture.service.requestAdhocCountersign({
1417
+ requestedByMemberId: 'member-finance',
1418
+ target: {
1419
+ kind: AdhocTargetKindEnum.MEMBER,
1420
+ memberIds: ['member-d'],
1421
+ },
1422
+ taskId: 'task-1',
1423
+ }),
1424
+ ).rejects.toThrow('does not allow ad-hoc signers');
1425
+ });
1426
+
1427
+ it('creates a blocking ad-hoc pre-approval task on the current node', async (): Promise<void> => {
1428
+ const fixture = createServiceFixture({
1429
+ currentVersionId: 'template-version-1',
1430
+ decisionTask: createTask({
1431
+ nodeId: 'task_finance',
1432
+ status: TaskStatusEnum.PENDING,
1433
+ tokenId: 'token-1',
1434
+ }),
1435
+ decisionToken: createWorkflowToken({
1436
+ currentNodeId: 'task_finance',
1437
+ status: WorkflowTokenStatusEnum.WAITING,
1438
+ }),
1439
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1440
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
1441
+ allowAddSigner: true,
1442
+ }),
1443
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1444
+ });
1445
+
1446
+ const adhocTask = await fixture.service.requestAdhocPreApproval({
1447
+ comment: '請先確認預算',
1448
+ onReject: AdhocPreApprovalRejectBehaviorEnum.REJECT_INSTANCE,
1449
+ requestedByMemberId: 'member-finance',
1450
+ target: {
1451
+ kind: AdhocTargetKindEnum.MEMBER,
1452
+ memberIds: ['member-d'],
1453
+ },
1454
+ taskId: 'task-1',
1455
+ });
1456
+
1457
+ expect(adhocTask).toMatchObject({
1458
+ adhocType: AdhocDirectiveTypeEnum.PRE_APPROVAL,
1459
+ assigneeMemberId: 'member-d',
1460
+ isAdhoc: true,
1461
+ nodeId: 'task_finance',
1462
+ status: TaskStatusEnum.PENDING,
1463
+ tokenId: 'token-1',
1464
+ });
1465
+ expect(fixture.savedAdhocDirectives).toContainEqual(
1466
+ expect.objectContaining({
1467
+ onReject: AdhocPreApprovalRejectBehaviorEnum.REJECT_INSTANCE,
1468
+ status: AdhocDirectiveStatusEnum.CONSUMED,
1469
+ type: AdhocDirectiveTypeEnum.PRE_APPROVAL,
1470
+ }),
1471
+ );
1472
+ expect(
1473
+ fixture.notificationService.createTaskAssignedNotification,
1474
+ ).toHaveBeenCalledWith(
1475
+ expect.objectContaining({
1476
+ task: expect.objectContaining({ assigneeMemberId: 'member-d' }),
1477
+ }),
1478
+ );
1479
+ });
1480
+
1481
+ it('holds the token until every ad-hoc task on the node is approved', async (): Promise<void> => {
1482
+ const fixture = createServiceFixture({
1483
+ additionalProcessTasks: [
1484
+ createTask({
1485
+ adhocDirectiveId: 'directive-seed-1',
1486
+ adhocOriginTaskId: 'task-1',
1487
+ adhocType: AdhocDirectiveTypeEnum.PRE_APPROVAL,
1488
+ assigneeMemberId: 'member-d',
1489
+ id: 'task-90',
1490
+ isAdhoc: true,
1491
+ nodeId: 'task_finance',
1492
+ originalAssigneeMemberId: 'member-d',
1493
+ status: TaskStatusEnum.PENDING,
1494
+ tokenId: 'token-1',
1495
+ }),
1496
+ ],
1497
+ currentVersionId: 'template-version-1',
1498
+ decisionTask: createTask({
1499
+ nodeId: 'task_finance',
1500
+ status: TaskStatusEnum.PENDING,
1501
+ tokenId: 'token-1',
1502
+ }),
1503
+ decisionToken: createWorkflowToken({
1504
+ currentNodeId: 'task_finance',
1505
+ status: WorkflowTokenStatusEnum.WAITING,
1506
+ }),
1507
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1508
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
1509
+ allowAddSigner: true,
1510
+ }),
1511
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1512
+ });
1513
+
1514
+ await fixture.service.decideTask({
1515
+ action: TaskDecisionActionEnum.APPROVED,
1516
+ comment: null,
1517
+ decidedByMemberId: 'member-finance',
1518
+ taskId: 'task-1',
1519
+ });
1520
+
1521
+ // The origin task completed but the ad-hoc pre-approval is still open —
1522
+ // the token must not advance and the instance must stay running.
1523
+ expect(fixture.savedProcessToken).toBeNull();
1524
+ expect(fixture.savedInstance).toBeNull();
1525
+
1526
+ await fixture.service.decideTask({
1527
+ action: TaskDecisionActionEnum.APPROVED,
1528
+ comment: null,
1529
+ decidedByMemberId: 'member-d',
1530
+ taskId: 'task-90',
1531
+ });
1532
+
1533
+ expect(fixture.savedProcessToken).toMatchObject({
1534
+ currentNodeId: 'end',
1535
+ status: WorkflowTokenStatusEnum.CONSUMED,
1536
+ });
1537
+ expect(fixture.savedInstance).toMatchObject({
1538
+ state: ApprovalInstanceStateEnum.APPROVED,
1539
+ });
1540
+ });
1541
+
1542
+ it('spawns a parallel countersign task when the next user task is created', async (): Promise<void> => {
1543
+ const fixture = createServiceFixture({
1544
+ currentVersionId: 'template-version-1',
1545
+ decisionToken: createWorkflowToken({
1546
+ currentNodeId: 'start',
1547
+ status: WorkflowTokenStatusEnum.ACTIVE,
1548
+ }),
1549
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1550
+ processAdhocDirectives: [
1551
+ createAdhocDirective({
1552
+ id: 'directive-seed-1',
1553
+ originNodeId: 'task_origin',
1554
+ status: AdhocDirectiveStatusEnum.PENDING,
1555
+ type: AdhocDirectiveTypeEnum.COUNTERSIGN,
1556
+ }),
1557
+ ],
1558
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
1559
+ allowAddSigner: true,
1560
+ }),
1561
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1562
+ });
1563
+
1564
+ await fixture.service.processInstance('instance-1');
1565
+
1566
+ const mainTask = fixture.savedTasks.find(
1567
+ (task) => !task.isAdhoc && task.nodeId === 'task_finance',
1568
+ );
1569
+ const countersignTask = fixture.savedTasks.find(
1570
+ (task) =>
1571
+ task.isAdhoc && task.adhocType === AdhocDirectiveTypeEnum.COUNTERSIGN,
1572
+ );
1573
+
1574
+ expect(mainTask).toMatchObject({
1575
+ assigneeMemberId: 'member-finance',
1576
+ status: TaskStatusEnum.PENDING,
1577
+ });
1578
+ expect(countersignTask).toMatchObject({
1579
+ adhocDirectiveId: 'directive-seed-1',
1580
+ assigneeMemberId: 'member-d',
1581
+ nodeId: 'task_finance',
1582
+ status: TaskStatusEnum.PENDING,
1583
+ });
1584
+ expect(fixture.savedAdhocDirectives).toContainEqual(
1585
+ expect.objectContaining({
1586
+ id: 'directive-seed-1',
1587
+ status: AdhocDirectiveStatusEnum.CONSUMED,
1588
+ }),
1589
+ );
1590
+ });
1591
+
1592
+ it('returns a rejected ad-hoc pre-approval to the origin approver without rejecting the instance', async (): Promise<void> => {
1593
+ const fixture = createServiceFixture({
1594
+ additionalProcessTasks: [
1595
+ createTask({
1596
+ adhocDirectiveId: 'directive-seed-1',
1597
+ adhocOriginTaskId: 'task-1',
1598
+ adhocType: AdhocDirectiveTypeEnum.PRE_APPROVAL,
1599
+ assigneeMemberId: 'member-d',
1600
+ id: 'task-90',
1601
+ isAdhoc: true,
1602
+ nodeId: 'task_finance',
1603
+ originalAssigneeMemberId: 'member-d',
1604
+ status: TaskStatusEnum.PENDING,
1605
+ tokenId: 'token-1',
1606
+ }),
1607
+ ],
1608
+ currentVersionId: 'template-version-1',
1609
+ decisionTask: createTask({
1610
+ nodeId: 'task_finance',
1611
+ status: TaskStatusEnum.PENDING,
1612
+ tokenId: 'token-1',
1613
+ }),
1614
+ decisionToken: createWorkflowToken({
1615
+ currentNodeId: 'task_finance',
1616
+ status: WorkflowTokenStatusEnum.WAITING,
1617
+ }),
1618
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1619
+ processAdhocDirectives: [
1620
+ createAdhocDirective({
1621
+ consumedAt: new Date('2026-05-04T09:00:00.000Z'),
1622
+ id: 'directive-seed-1',
1623
+ onReject: AdhocPreApprovalRejectBehaviorEnum.RETURN_TO_ORIGIN,
1624
+ status: AdhocDirectiveStatusEnum.CONSUMED,
1625
+ type: AdhocDirectiveTypeEnum.PRE_APPROVAL,
1626
+ }),
1627
+ ],
1628
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
1629
+ allowAddSigner: true,
1630
+ }),
1631
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1632
+ });
1633
+
1634
+ const decision = await fixture.service.decideTask({
1635
+ action: TaskDecisionActionEnum.REJECTED,
1636
+ comment: '需要更多資訊',
1637
+ decidedByMemberId: 'member-d',
1638
+ taskId: 'task-90',
1639
+ });
1640
+
1641
+ expect(decision).toMatchObject({
1642
+ action: TaskDecisionActionEnum.REJECTED,
1643
+ taskId: 'task-90',
1644
+ });
1645
+ // RETURN_TO_ORIGIN must not reject the instance.
1646
+ expect(fixture.savedInstance).toBeNull();
1647
+ expect(
1648
+ fixture.notificationService.createAdhocWorkflowNotifications,
1649
+ ).toHaveBeenCalledWith(
1650
+ expect.objectContaining({
1651
+ recipientMemberIds: ['member-finance'],
1652
+ }),
1653
+ );
1654
+ expect(fixture.savedSingleActivityLogs).toContainEqual(
1655
+ expect.objectContaining({
1656
+ eventType: ActivityLogEventTypeEnum.ADHOC_PRE_APPROVAL_RETURNED,
1657
+ }),
1658
+ );
1659
+ });
1660
+
1661
+ it('rejects the whole instance when an ad-hoc pre-approval is rejected with REJECT_INSTANCE', async (): Promise<void> => {
1662
+ const fixture = createServiceFixture({
1663
+ additionalProcessTasks: [
1664
+ createTask({
1665
+ adhocDirectiveId: 'directive-seed-1',
1666
+ adhocOriginTaskId: 'task-1',
1667
+ adhocType: AdhocDirectiveTypeEnum.PRE_APPROVAL,
1668
+ assigneeMemberId: 'member-d',
1669
+ id: 'task-90',
1670
+ isAdhoc: true,
1671
+ nodeId: 'task_finance',
1672
+ originalAssigneeMemberId: 'member-d',
1673
+ status: TaskStatusEnum.PENDING,
1674
+ tokenId: 'token-1',
1675
+ }),
1676
+ ],
1677
+ currentVersionId: 'template-version-1',
1678
+ decisionTask: createTask({
1679
+ nodeId: 'task_finance',
1680
+ status: TaskStatusEnum.PENDING,
1681
+ tokenId: 'token-1',
1682
+ }),
1683
+ decisionToken: createWorkflowToken({
1684
+ currentNodeId: 'task_finance',
1685
+ status: WorkflowTokenStatusEnum.WAITING,
1686
+ }),
1687
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1688
+ processAdhocDirectives: [
1689
+ createAdhocDirective({
1690
+ consumedAt: new Date('2026-05-04T09:00:00.000Z'),
1691
+ id: 'directive-seed-1',
1692
+ onReject: AdhocPreApprovalRejectBehaviorEnum.REJECT_INSTANCE,
1693
+ status: AdhocDirectiveStatusEnum.CONSUMED,
1694
+ type: AdhocDirectiveTypeEnum.PRE_APPROVAL,
1695
+ }),
1696
+ ],
1697
+ processWorkflowSnapshot: createLinearUserTaskWorkflow({
1698
+ allowAddSigner: true,
1699
+ }),
1700
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1701
+ });
1702
+
1703
+ await fixture.service.decideTask({
1704
+ action: TaskDecisionActionEnum.REJECTED,
1705
+ comment: '不同意',
1706
+ decidedByMemberId: 'member-d',
1707
+ taskId: 'task-90',
1708
+ });
1709
+
1710
+ expect(fixture.savedInstance).toMatchObject({
1711
+ state: ApprovalInstanceStateEnum.REJECTED,
1712
+ });
1713
+ });
1714
+
1715
+ it('dispatches an ad-hoc stage notification when the stage is approved', async (): Promise<void> => {
1716
+ const fixture = createServiceFixture({
1717
+ currentVersionId: 'template-version-1',
1718
+ decisionTask: createTask({
1719
+ nodeId: 'task_finance',
1720
+ status: TaskStatusEnum.PENDING,
1721
+ tokenId: 'token-1',
1722
+ }),
1723
+ decisionToken: createWorkflowToken({
1724
+ currentNodeId: 'task_finance',
1725
+ status: WorkflowTokenStatusEnum.WAITING,
1726
+ }),
1727
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1728
+ processAdhocDirectives: [
1729
+ createAdhocDirective({
1730
+ id: 'directive-stage-1',
1731
+ originNodeId: 'task_finance',
1732
+ status: AdhocDirectiveStatusEnum.PENDING,
1733
+ targetValue: {
1734
+ kind: AdhocTargetKindEnum.MEMBER,
1735
+ memberIds: ['member-x'],
1736
+ },
1737
+ type: AdhocDirectiveTypeEnum.STAGE_NOTIFY,
1738
+ }),
1739
+ ],
1740
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
1741
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1742
+ });
1743
+
1744
+ await fixture.service.decideTask({
1745
+ action: TaskDecisionActionEnum.APPROVED,
1746
+ comment: null,
1747
+ decidedByMemberId: 'member-finance',
1748
+ taskId: 'task-1',
1749
+ });
1750
+
1751
+ expect(
1752
+ fixture.notificationService.createAdhocWorkflowNotifications,
1753
+ ).toHaveBeenCalledWith(
1754
+ expect.objectContaining({
1755
+ payload: expect.objectContaining({ outcome: 'APPROVED' }),
1756
+ recipientMemberIds: ['member-x'],
1757
+ }),
1758
+ );
1759
+ expect(fixture.savedAdhocDirectives).toContainEqual(
1760
+ expect.objectContaining({
1761
+ id: 'directive-stage-1',
1762
+ status: AdhocDirectiveStatusEnum.CONSUMED,
1763
+ }),
1764
+ );
1765
+ });
1766
+
1767
+ it('dispatches ad-hoc completion notifications on reject and cancels pending flow directives', async (): Promise<void> => {
1768
+ const fixture = createServiceFixture({
1769
+ currentVersionId: 'template-version-1',
1770
+ decisionTask: createTask({
1771
+ nodeId: 'task_finance',
1772
+ status: TaskStatusEnum.PENDING,
1773
+ tokenId: 'token-1',
1774
+ }),
1775
+ decisionToken: createWorkflowToken({
1776
+ currentNodeId: 'task_finance',
1777
+ status: WorkflowTokenStatusEnum.WAITING,
1778
+ }),
1779
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1780
+ processAdhocDirectives: [
1781
+ createAdhocDirective({
1782
+ id: 'directive-completion-1',
1783
+ status: AdhocDirectiveStatusEnum.PENDING,
1784
+ targetValue: {
1785
+ kind: AdhocTargetKindEnum.MEMBER,
1786
+ memberIds: ['member-x'],
1787
+ },
1788
+ type: AdhocDirectiveTypeEnum.COMPLETION_NOTIFY,
1789
+ }),
1790
+ createAdhocDirective({
1791
+ id: 'directive-countersign-1',
1792
+ status: AdhocDirectiveStatusEnum.PENDING,
1793
+ type: AdhocDirectiveTypeEnum.COUNTERSIGN,
1794
+ }),
1795
+ ],
1796
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
1797
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1798
+ });
1799
+
1800
+ await fixture.service.decideTask({
1801
+ action: TaskDecisionActionEnum.REJECTED,
1802
+ comment: '資料不足',
1803
+ decidedByMemberId: 'member-finance',
1804
+ taskId: 'task-1',
1805
+ });
1806
+
1807
+ expect(
1808
+ fixture.notificationService.createAdhocWorkflowNotifications,
1809
+ ).toHaveBeenCalledWith(
1810
+ expect.objectContaining({
1811
+ payload: expect.objectContaining({
1812
+ finalState: ApprovalInstanceStateEnum.REJECTED,
1813
+ }),
1814
+ recipientMemberIds: ['member-x'],
1815
+ }),
1816
+ );
1817
+ expect(fixture.savedAdhocDirectives).toContainEqual(
1818
+ expect.objectContaining({
1819
+ id: 'directive-completion-1',
1820
+ status: AdhocDirectiveStatusEnum.CONSUMED,
1821
+ }),
1822
+ );
1823
+ expect(fixture.savedAdhocDirectives).toContainEqual(
1824
+ expect.objectContaining({
1825
+ id: 'directive-countersign-1',
1826
+ status: AdhocDirectiveStatusEnum.CANCELLED,
1827
+ }),
1828
+ );
1829
+ });
1830
+
1831
+ it('records ad-hoc webhook notification failures without blocking the decision', async (): Promise<void> => {
1832
+ const failingDispatcher: BPMWorkflowServiceTaskDispatcher = {
1833
+ dispatchWebhook: jest.fn(() =>
1834
+ Promise.reject(new Error('webhook unreachable')),
1835
+ ),
1836
+ };
1837
+ const fixture = createServiceFixture({
1838
+ currentVersionId: 'template-version-1',
1839
+ decisionTask: createTask({
1840
+ nodeId: 'task_finance',
1841
+ status: TaskStatusEnum.PENDING,
1842
+ tokenId: 'token-1',
1843
+ }),
1844
+ decisionToken: createWorkflowToken({
1845
+ currentNodeId: 'task_finance',
1846
+ status: WorkflowTokenStatusEnum.WAITING,
1847
+ }),
1848
+ formVersionStatus: FormDefinitionVersionStatusEnum.PUBLISHED,
1849
+ processAdhocDirectives: [
1850
+ createAdhocDirective({
1851
+ id: 'directive-webhook-1',
1852
+ originNodeId: 'task_finance',
1853
+ status: AdhocDirectiveStatusEnum.PENDING,
1854
+ targetKind: AdhocTargetKindEnum.WEBHOOK,
1855
+ targetValue: {
1856
+ kind: AdhocTargetKindEnum.WEBHOOK,
1857
+ webhookUrl: 'https://example.com/hook',
1858
+ },
1859
+ type: AdhocDirectiveTypeEnum.STAGE_NOTIFY,
1860
+ }),
1861
+ ],
1862
+ processWorkflowSnapshot: createLinearUserTaskWorkflow(),
1863
+ serviceTaskDispatcher: failingDispatcher,
1864
+ templateVersionStatus: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1865
+ });
1866
+
1867
+ await fixture.service.decideTask({
1868
+ action: TaskDecisionActionEnum.APPROVED,
1869
+ comment: null,
1870
+ decidedByMemberId: 'member-finance',
1871
+ taskId: 'task-1',
1872
+ });
1873
+
1874
+ expect(fixture.savedInstance).toMatchObject({
1875
+ state: ApprovalInstanceStateEnum.APPROVED,
1876
+ });
1877
+ expect(fixture.savedSingleActivityLogs).toContainEqual(
1878
+ expect.objectContaining({
1879
+ eventType: ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
1880
+ payload: expect.objectContaining({
1881
+ action: 'ADHOC_WEBHOOK',
1882
+ directiveId: 'directive-webhook-1',
1883
+ }),
1884
+ }),
1885
+ );
1886
+ });
1887
+ });
1888
+
1889
+ interface ServiceFixture {
1890
+ readonly managerQuery: jest.Mock<
1891
+ Promise<unknown>,
1892
+ [string, readonly unknown[]]
1893
+ >;
1894
+ readonly notificationService: Pick<
1895
+ NotificationService,
1896
+ | 'createAdhocWorkflowNotifications'
1897
+ | 'createInstanceCompletedNotification'
1898
+ | 'createServiceTaskNotifications'
1899
+ | 'createTaskAssignedNotification'
1900
+ | 'resolveTaskNotifications'
1901
+ | 'supersedeInstanceTaskNotifications'
1902
+ >;
1903
+ readonly notificationFind: jest.Mock<
1904
+ Promise<readonly NotificationEntity[]>,
1905
+ [Readonly<Record<string, unknown>>]
1906
+ >;
1907
+ readonly rootInstanceCount: jest.Mock<Promise<number>, []>;
1908
+ readonly rootInstanceFind: jest.Mock<
1909
+ Promise<readonly ApprovalInstanceEntity[]>,
1910
+ [Readonly<Record<string, unknown>>?]
1911
+ >;
1912
+ readonly rootInstanceQueryBuilder: ApprovalInstanceQueryBuilderMock;
1913
+ readonly rootTaskFind: jest.Mock<
1914
+ Promise<readonly TaskEntity[]>,
1915
+ [Readonly<Record<string, unknown>>]
1916
+ >;
1917
+ readonly savedDecision: TaskDecisionEntity | null;
1918
+ readonly savedActivityLogs: readonly ActivityLogEntity[];
1919
+ readonly savedInstance: ApprovalInstanceEntity | null;
1920
+ readonly savedAdhocDirectives: readonly AdhocDirectiveEntity[];
1921
+ readonly savedProcessLog: ActivityLogEntity | null;
1922
+ readonly savedProcessToken: WorkflowTokenEntity | null;
1923
+ readonly savedSingleActivityLogs: readonly ActivityLogEntity[];
1924
+ readonly savedTasks: readonly TaskEntity[];
1925
+ readonly savedWorkflowTokens: readonly WorkflowTokenEntity[];
1926
+ readonly savedToken: WorkflowTokenEntity | null;
1927
+ readonly service: WorkflowEngineService;
1928
+ }
1929
+
1930
+ interface ApprovalInstanceQueryBuilderMock {
1931
+ readonly andWhere: jest.Mock<
1932
+ ApprovalInstanceQueryBuilderMock,
1933
+ [string, Readonly<Record<string, unknown>>?]
1934
+ >;
1935
+ readonly getCount: jest.Mock<Promise<number>, []>;
1936
+ readonly getMany: jest.Mock<Promise<readonly ApprovalInstanceEntity[]>, []>;
1937
+ readonly orderBy: jest.Mock<
1938
+ ApprovalInstanceQueryBuilderMock,
1939
+ [string, 'ASC' | 'DESC']
1940
+ >;
1941
+ readonly skip: jest.Mock<ApprovalInstanceQueryBuilderMock, [number]>;
1942
+ readonly take: jest.Mock<ApprovalInstanceQueryBuilderMock, [number]>;
1943
+ }
1944
+
1945
+ function createServiceFixture({
1946
+ additionalProcessTasks = [],
1947
+ currentVersionId,
1948
+ delegationResolution,
1949
+ decisionTask,
1950
+ decisionToken,
1951
+ formSchema,
1952
+ formVersionStatus,
1953
+ instanceState,
1954
+ latestReturnActivity,
1955
+ processAdhocDirectives = [],
1956
+ processFormData,
1957
+ processFormDefinitionSnapshot,
1958
+ processManagerResolutions = [],
1959
+ processMemberships = [],
1960
+ processNotifications = [],
1961
+ processOrgUnits = [],
1962
+ processWorkflowSnapshot,
1963
+ serviceTaskDispatcher,
1964
+ templateVersionStatus,
1965
+ }: {
1966
+ readonly additionalProcessTasks?: readonly TaskEntity[];
1967
+ readonly currentVersionId: string | null;
1968
+ readonly delegationResolution?: DelegationResolution;
1969
+ readonly decisionTask?: TaskEntity;
1970
+ readonly decisionToken?: WorkflowTokenEntity;
1971
+ readonly formSchema?: FormDefinitionSchema;
1972
+ readonly formVersionStatus: FormDefinitionVersionStatusEnum;
1973
+ readonly instanceState?: ApprovalInstanceStateEnum;
1974
+ readonly latestReturnActivity?: ActivityLogEntity | null;
1975
+ readonly processAdhocDirectives?: readonly AdhocDirectiveEntity[];
1976
+ readonly processFormData?: Readonly<Record<string, unknown>>;
1977
+ readonly processFormDefinitionSnapshot?: Readonly<Record<string, unknown>>;
1978
+ readonly processManagerResolutions?: readonly ManagerResolutionEntity[];
1979
+ readonly processMemberships?: readonly MembershipEntity[];
1980
+ readonly processNotifications?: readonly NotificationEntity[];
1981
+ readonly processOrgUnits?: readonly OrgUnitEntity[];
1982
+ readonly processWorkflowSnapshot?: WorkflowDefinition;
1983
+ readonly serviceTaskDispatcher?: BPMWorkflowServiceTaskDispatcher;
1984
+ readonly templateVersionStatus: ApprovalTemplateVersionStatusEnum;
1985
+ }): ServiceFixture {
1986
+ let savedToken: WorkflowTokenEntity | null = null;
1987
+ let tokenSequence = 1;
1988
+ let taskSequence = 1;
1989
+ let processTokens: readonly WorkflowTokenEntity[] = processWorkflowSnapshot
1990
+ ? [
1991
+ decisionToken ??
1992
+ createWorkflowToken({
1993
+ currentNodeId: 'start',
1994
+ status: WorkflowTokenStatusEnum.ACTIVE,
1995
+ }),
1996
+ ]
1997
+ : [];
1998
+ let processTasks: readonly TaskEntity[] = [
1999
+ ...(decisionTask ? [decisionTask] : []),
2000
+ ...additionalProcessTasks,
2001
+ ];
2002
+ let savedProcessToken: WorkflowTokenEntity | null = null;
2003
+ let savedDecision: TaskDecisionEntity | null = null;
2004
+ let savedInstance: ApprovalInstanceEntity | null = null;
2005
+ let savedProcessLog: ActivityLogEntity | null = null;
2006
+ let savedActivityLogs: readonly ActivityLogEntity[] = [];
2007
+ let savedSingleActivityLogs: readonly ActivityLogEntity[] = [];
2008
+ let savedTasks: readonly TaskEntity[] = [];
2009
+ const template = createTemplate(currentVersionId);
2010
+ const templateVersion = createTemplateVersion(templateVersionStatus);
2011
+ const formVersion = createFormVersion(formVersionStatus, formSchema);
2012
+ const rootInstanceFind = jest.fn<
2013
+ Promise<readonly ApprovalInstanceEntity[]>,
2014
+ [Readonly<Record<string, unknown>>?]
2015
+ >(() => Promise.resolve([createApprovalInstance()]));
2016
+ const rootInstanceCount = jest.fn<Promise<number>, []>(() =>
2017
+ rootInstanceFind().then((instances) => instances.length),
2018
+ );
2019
+ const rootInstanceQueryBuilder = createApprovalInstanceQueryBuilderMock(
2020
+ rootInstanceFind,
2021
+ rootInstanceCount,
2022
+ );
2023
+ const rootTaskFind = jest.fn<
2024
+ Promise<readonly TaskEntity[]>,
2025
+ [Readonly<Record<string, unknown>>]
2026
+ >(() => Promise.resolve([]));
2027
+ const instanceRepository = createRepository<ApprovalInstanceEntity>({
2028
+ createQueryBuilder: jest.fn(() => rootInstanceQueryBuilder),
2029
+ find: rootInstanceFind,
2030
+ findOne: jest.fn(() => Promise.resolve(createApprovalInstance())),
2031
+ });
2032
+ const tokenRepository = createRepository<WorkflowTokenEntity>({});
2033
+ const taskRepository = createRepository<TaskEntity>({
2034
+ find: rootTaskFind,
2035
+ });
2036
+ const taskCandidateRepository = createRepository<TaskCandidateEntity>({
2037
+ find: jest.fn(() => Promise.resolve([])),
2038
+ });
2039
+ const taskDecisionRepository = createRepository<TaskDecisionEntity>({
2040
+ find: jest.fn(() => Promise.resolve([])),
2041
+ });
2042
+ const notificationFind = jest.fn<
2043
+ Promise<readonly NotificationEntity[]>,
2044
+ [Readonly<Record<string, unknown>>]
2045
+ >(() => Promise.resolve(processNotifications));
2046
+ const notificationRepository = createRepository<NotificationEntity>({
2047
+ find: notificationFind,
2048
+ });
2049
+ const activityLogRepository = createRepository<ActivityLogEntity>({});
2050
+ let processAdhocDirectiveRows: readonly AdhocDirectiveEntity[] = [
2051
+ ...processAdhocDirectives,
2052
+ ];
2053
+ let directiveSequence = 0;
2054
+ const adhocDirectiveRepository = createRepository<AdhocDirectiveEntity>({
2055
+ find: jest.fn(() => Promise.resolve(processAdhocDirectiveRows)),
2056
+ });
2057
+ const transactionalAdhocDirectiveRepository =
2058
+ createRepository<AdhocDirectiveEntity>({
2059
+ create: jest.fn(
2060
+ (entity: Partial<AdhocDirectiveEntity>): AdhocDirectiveEntity =>
2061
+ Object.assign(new AdhocDirectiveEntity(), entity),
2062
+ ),
2063
+ find: jest.fn(
2064
+ (
2065
+ options?: Readonly<{
2066
+ where?: Readonly<Record<string, unknown>>;
2067
+ }>,
2068
+ ): Promise<readonly AdhocDirectiveEntity[]> =>
2069
+ Promise.resolve(
2070
+ processAdhocDirectiveRows.filter((directive) =>
2071
+ Object.entries(options?.where ?? {}).every(
2072
+ ([key, value]) =>
2073
+ // FindOperator values (e.g. In([...])) are treated as
2074
+ // match-all to keep the mock simple.
2075
+ typeof value === 'object' ||
2076
+ directive[key as keyof AdhocDirectiveEntity] === value,
2077
+ ),
2078
+ ),
2079
+ ),
2080
+ ),
2081
+ findOne: jest.fn(
2082
+ (
2083
+ options?: Readonly<{ where?: Readonly<{ id?: string }> }>,
2084
+ ): Promise<AdhocDirectiveEntity | null> =>
2085
+ Promise.resolve(
2086
+ processAdhocDirectiveRows.find(
2087
+ (directive) => directive.id === options?.where?.id,
2088
+ ) ?? null,
2089
+ ),
2090
+ ),
2091
+ save: jest.fn(
2092
+ (
2093
+ entityOrEntities: AdhocDirectiveEntity | AdhocDirectiveEntity[],
2094
+ ): Promise<AdhocDirectiveEntity | AdhocDirectiveEntity[]> => {
2095
+ const entities = Array.isArray(entityOrEntities)
2096
+ ? entityOrEntities
2097
+ : [entityOrEntities];
2098
+ const entitiesWithIds = entities.map((entity) => {
2099
+ if (entity.id) {
2100
+ return entity;
2101
+ }
2102
+
2103
+ directiveSequence += 1;
2104
+
2105
+ return Object.assign(new AdhocDirectiveEntity(), entity, {
2106
+ id: `directive-${directiveSequence}`,
2107
+ });
2108
+ });
2109
+ const entityIds = new Set(
2110
+ entitiesWithIds.map((entity) => entity.id),
2111
+ );
2112
+
2113
+ processAdhocDirectiveRows = [
2114
+ ...processAdhocDirectiveRows.filter(
2115
+ (directive) => !entityIds.has(directive.id),
2116
+ ),
2117
+ ...entitiesWithIds,
2118
+ ];
2119
+
2120
+ return Promise.resolve(
2121
+ Array.isArray(entityOrEntities)
2122
+ ? entitiesWithIds
2123
+ : (entitiesWithIds[0] ?? entityOrEntities),
2124
+ );
2125
+ },
2126
+ ),
2127
+ });
2128
+ const delegationService = {
2129
+ resolveAssignee: jest.fn(
2130
+ (assigneeMemberId: string): Promise<DelegationResolution> =>
2131
+ Promise.resolve(
2132
+ delegationResolution ?? {
2133
+ delegationChain: [],
2134
+ finalAssigneeMemberId: assigneeMemberId,
2135
+ },
2136
+ ),
2137
+ ),
2138
+ };
2139
+ const notificationService = {
2140
+ createAdhocWorkflowNotifications: jest.fn(() => Promise.resolve([])),
2141
+ createInstanceCompletedNotification: jest.fn(() => Promise.resolve([])),
2142
+ createServiceTaskNotifications: jest.fn(() => Promise.resolve([])),
2143
+ createTaskAssignedNotification: jest.fn(() => Promise.resolve([])),
2144
+ resolveTaskNotifications: jest.fn(() => Promise.resolve()),
2145
+ supersedeInstanceTaskNotifications: jest.fn(() => Promise.resolve()),
2146
+ };
2147
+ const attachmentService = {
2148
+ bindFormDataAttachmentsToInstance: jest.fn(() => Promise.resolve()),
2149
+ };
2150
+ const signatureService = {
2151
+ signTaskDecision: jest.fn(() =>
2152
+ Promise.resolve(
2153
+ Object.assign(new SignatureEntity(), {
2154
+ id: 'signature-1',
2155
+ signedPayloadHash: 'signed-payload-hash-1',
2156
+ }),
2157
+ ),
2158
+ ),
2159
+ };
2160
+ const templateRepository = createRepository<ApprovalTemplateEntity>({
2161
+ findOne: jest.fn(() => Promise.resolve(template)),
2162
+ });
2163
+ const templateVersionRepository =
2164
+ createRepository<ApprovalTemplateVersionEntity>({
2165
+ findOne: jest.fn(() => Promise.resolve(templateVersion)),
2166
+ });
2167
+ const formVersionRepository = createRepository<FormDefinitionVersionEntity>({
2168
+ findOne: jest.fn(() => Promise.resolve(formVersion)),
2169
+ });
2170
+ const transactionalInstanceRepository =
2171
+ createRepository<ApprovalInstanceEntity>({
2172
+ create: jest.fn(
2173
+ (entity: Partial<ApprovalInstanceEntity>): ApprovalInstanceEntity =>
2174
+ Object.assign(createApprovalInstance(), entity),
2175
+ ),
2176
+ findOne: jest.fn(() =>
2177
+ Promise.resolve(
2178
+ createApprovalInstance({
2179
+ formData: processFormData,
2180
+ formDefinitionSnapshot: processFormDefinitionSnapshot,
2181
+ state: instanceState,
2182
+ workflowSnapshot: processWorkflowSnapshot,
2183
+ }),
2184
+ ),
2185
+ ),
2186
+ save: jest.fn((entity: ApprovalInstanceEntity) => {
2187
+ savedInstance = entity;
2188
+
2189
+ return Promise.resolve(entity);
2190
+ }),
2191
+ });
2192
+ const transactionalTokenRepository = createRepository<WorkflowTokenEntity>({
2193
+ create: jest.fn(
2194
+ (entity: Partial<WorkflowTokenEntity>): WorkflowTokenEntity => ({
2195
+ consumedAt: null,
2196
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2197
+ currentNodeId: entity.currentNodeId ?? 'start',
2198
+ id: entity.id ?? `token-${(tokenSequence += 1)}`,
2199
+ instanceId: entity.instanceId ?? 'instance-1',
2200
+ parentTokenId: entity.parentTokenId ?? null,
2201
+ status: entity.status ?? WorkflowTokenStatusEnum.ACTIVE,
2202
+ }),
2203
+ ),
2204
+ save: jest.fn(
2205
+ (
2206
+ entityOrEntities: WorkflowTokenEntity | WorkflowTokenEntity[],
2207
+ ): Promise<WorkflowTokenEntity | WorkflowTokenEntity[]> => {
2208
+ if (processWorkflowSnapshot) {
2209
+ const entities = Array.isArray(entityOrEntities)
2210
+ ? entityOrEntities
2211
+ : [entityOrEntities];
2212
+ const entitiesWithIds = entities.map((entity) => {
2213
+ const token = { ...entity };
2214
+
2215
+ if (!token.id) {
2216
+ tokenSequence += 1;
2217
+
2218
+ return { ...token, id: `token-${tokenSequence}` };
2219
+ }
2220
+
2221
+ if (/^token-\d+$/u.test(token.id)) {
2222
+ const idNumber = Number(token.id.replace('token-', ''));
2223
+
2224
+ tokenSequence = Math.max(tokenSequence, idNumber);
2225
+ }
2226
+
2227
+ return token;
2228
+ });
2229
+ const entityIds = new Set(entitiesWithIds.map((entity) => entity.id));
2230
+
2231
+ processTokens = [
2232
+ ...processTokens.filter((token) => !entityIds.has(token.id)),
2233
+ ...entitiesWithIds,
2234
+ ];
2235
+ savedProcessToken =
2236
+ entitiesWithIds[entitiesWithIds.length - 1] ?? null;
2237
+
2238
+ return Promise.resolve(
2239
+ Array.isArray(entityOrEntities)
2240
+ ? entitiesWithIds
2241
+ : (entitiesWithIds[0] ?? entityOrEntities),
2242
+ );
2243
+ } else {
2244
+ savedToken = entityOrEntities as WorkflowTokenEntity;
2245
+ }
2246
+
2247
+ return Promise.resolve(entityOrEntities);
2248
+ },
2249
+ ),
2250
+ find: jest.fn(() =>
2251
+ Promise.resolve([...processTokens].sort(compareTokenCreatedAt)),
2252
+ ),
2253
+ findOne: jest.fn(
2254
+ (
2255
+ options?: Readonly<{
2256
+ where?: Readonly<{ id?: string }>;
2257
+ }>,
2258
+ ) =>
2259
+ Promise.resolve(
2260
+ options?.where?.id
2261
+ ? (processTokens.find((token) => token.id === options.where?.id) ??
2262
+ null)
2263
+ : (processTokens
2264
+ .filter(
2265
+ (token) => token.status === WorkflowTokenStatusEnum.ACTIVE,
2266
+ )
2267
+ .sort(compareTokenCreatedAt)[0] ?? null),
2268
+ ),
2269
+ ),
2270
+ });
2271
+ const transactionalTaskRepository = createRepository<TaskEntity>({
2272
+ create: jest.fn(
2273
+ (entity: Partial<TaskEntity>): TaskEntity =>
2274
+ Object.assign(new TaskEntity(), {
2275
+ adhocDirectiveId: entity.adhocDirectiveId ?? null,
2276
+ adhocOriginTaskId: entity.adhocOriginTaskId ?? null,
2277
+ adhocType: entity.adhocType ?? null,
2278
+ isAdhoc: entity.isAdhoc ?? false,
2279
+ assigneeMemberId: entity.assigneeMemberId ?? 'member-finance',
2280
+ assignmentType:
2281
+ entity.assignmentType ?? TaskAssignmentTypeEnum.DIRECT_MEMBER,
2282
+ completedAt: entity.completedAt ?? null,
2283
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2284
+ delegationChain: entity.delegationChain ?? [],
2285
+ id: entity.id ?? `task-${(taskSequence += 1)}`,
2286
+ instanceId: entity.instanceId ?? 'instance-1',
2287
+ nodeId: entity.nodeId ?? 'task_finance',
2288
+ openedAt: entity.openedAt ?? null,
2289
+ originalAssigneeMemberId:
2290
+ entity.originalAssigneeMemberId ?? 'member-finance',
2291
+ decisionPolicySnapshot: entity.decisionPolicySnapshot ?? {
2292
+ type: 'SINGLE',
2293
+ },
2294
+ slaDueAt: entity.slaDueAt ?? null,
2295
+ status: entity.status ?? TaskStatusEnum.PENDING,
2296
+ tokenId: entity.tokenId ?? 'token-1',
2297
+ }),
2298
+ ),
2299
+ find: jest.fn(() => Promise.resolve(processTasks)),
2300
+ findOne: jest.fn(
2301
+ (
2302
+ options?: Readonly<{
2303
+ where?: Readonly<{ id?: string; nodeId?: string; tokenId?: string }>;
2304
+ }>,
2305
+ ) =>
2306
+ Promise.resolve(
2307
+ options?.where?.id
2308
+ ? (processTasks.find((task) => task.id === options.where?.id) ??
2309
+ null)
2310
+ : options?.where?.tokenId
2311
+ ? (processTasks.find(
2312
+ (task) =>
2313
+ task.tokenId === options.where?.tokenId &&
2314
+ (!options.where?.nodeId ||
2315
+ task.nodeId === options.where.nodeId),
2316
+ ) ?? null)
2317
+ : null,
2318
+ ),
2319
+ ),
2320
+ save: jest.fn(
2321
+ (
2322
+ entityOrEntities: TaskEntity | TaskEntity[],
2323
+ ): Promise<TaskEntity | TaskEntity[]> => {
2324
+ const entities = Array.isArray(entityOrEntities)
2325
+ ? entityOrEntities
2326
+ : [entityOrEntities];
2327
+ const entitiesWithIds = entities.map((entity) => {
2328
+ const task = Object.assign(new TaskEntity(), entity);
2329
+
2330
+ if (!task.id) {
2331
+ taskSequence += 1;
2332
+
2333
+ return Object.assign(new TaskEntity(), task, {
2334
+ id: `task-${taskSequence}`,
2335
+ });
2336
+ }
2337
+
2338
+ if (/^task-\d+$/u.test(task.id)) {
2339
+ const idNumber = Number(task.id.replace('task-', ''));
2340
+
2341
+ taskSequence = Math.max(taskSequence, idNumber);
2342
+ }
2343
+
2344
+ return task;
2345
+ });
2346
+ const entityIds = new Set(entitiesWithIds.map((entity) => entity.id));
2347
+
2348
+ processTasks = [
2349
+ ...processTasks.filter((task) => !entityIds.has(task.id)),
2350
+ ...entitiesWithIds,
2351
+ ];
2352
+ savedTasks = [...savedTasks, ...entitiesWithIds];
2353
+
2354
+ return Promise.resolve(
2355
+ Array.isArray(entityOrEntities)
2356
+ ? entitiesWithIds
2357
+ : (entitiesWithIds[0] ?? entityOrEntities),
2358
+ );
2359
+ },
2360
+ ),
2361
+ });
2362
+ const transactionalTaskCandidateRepository =
2363
+ createRepository<TaskCandidateEntity>({
2364
+ create: jest.fn(
2365
+ (entity: Partial<TaskCandidateEntity>): TaskCandidateEntity =>
2366
+ Object.assign(new TaskCandidateEntity(), {
2367
+ claimedAt: entity.claimedAt ?? null,
2368
+ createdAt: entity.createdAt ?? new Date('2026-05-04T09:00:00.000Z'),
2369
+ decidedAt: entity.decidedAt ?? null,
2370
+ delegationChain: entity.delegationChain ?? [],
2371
+ id: entity.id ?? 'task-candidate-1',
2372
+ memberId: entity.memberId ?? 'member-finance',
2373
+ originalMemberId: entity.originalMemberId ?? 'member-finance',
2374
+ sourceType: entity.sourceType ?? 'DIRECT',
2375
+ status: entity.status ?? TaskCandidateStatusEnum.PENDING,
2376
+ taskId: entity.taskId ?? 'task-1',
2377
+ }),
2378
+ ),
2379
+ find: jest.fn(() => Promise.resolve([])),
2380
+ save: jest.fn(
2381
+ (
2382
+ entityOrEntities: TaskCandidateEntity | TaskCandidateEntity[],
2383
+ ): Promise<TaskCandidateEntity | TaskCandidateEntity[]> =>
2384
+ Promise.resolve(entityOrEntities),
2385
+ ),
2386
+ });
2387
+ const transactionalTaskDecisionRepository =
2388
+ createRepository<TaskDecisionEntity>({
2389
+ create: jest.fn(
2390
+ (entity: Partial<TaskDecisionEntity>): TaskDecisionEntity =>
2391
+ Object.assign(new TaskDecisionEntity(), {
2392
+ action: entity.action ?? TaskDecisionActionEnum.APPROVED,
2393
+ comment: entity.comment ?? null,
2394
+ decidedAt: entity.decidedAt ?? new Date('2026-05-04T09:00:00.000Z'),
2395
+ decidedByMemberId: entity.decidedByMemberId ?? 'member-finance',
2396
+ id: entity.id ?? 'decision-1',
2397
+ returnToNodeId: entity.returnToNodeId ?? null,
2398
+ signatureId: entity.signatureId ?? null,
2399
+ taskId: entity.taskId ?? 'task-1',
2400
+ transferToMemberId: entity.transferToMemberId ?? null,
2401
+ }),
2402
+ ),
2403
+ save: jest.fn((entity: TaskDecisionEntity) => {
2404
+ savedDecision = entity;
2405
+
2406
+ return Promise.resolve(entity);
2407
+ }),
2408
+ });
2409
+ const transactionalActivityRepository = createRepository<ActivityLogEntity>({
2410
+ create: jest.fn(
2411
+ (entity: Partial<ActivityLogEntity>): ActivityLogEntity =>
2412
+ Object.assign(new ActivityLogEntity(), {
2413
+ actorMemberId: entity.actorMemberId ?? null,
2414
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2415
+ eventType:
2416
+ entity.eventType ??
2417
+ ActivityLogEventTypeEnum.ENGINE_PROCESS_REQUESTED,
2418
+ id: entity.id ?? 'activity-log',
2419
+ instanceId: entity.instanceId ?? 'instance-1',
2420
+ nodeId: entity.nodeId ?? null,
2421
+ payload: entity.payload ?? {},
2422
+ taskId: entity.taskId ?? null,
2423
+ }),
2424
+ ),
2425
+ findOne: jest.fn(() => Promise.resolve(latestReturnActivity ?? null)),
2426
+ save: jest.fn(
2427
+ (
2428
+ entityOrEntities: ActivityLogEntity | ActivityLogEntity[],
2429
+ ): Promise<ActivityLogEntity | ActivityLogEntity[]> => {
2430
+ if (Array.isArray(entityOrEntities)) {
2431
+ savedActivityLogs = [...entityOrEntities];
2432
+
2433
+ return Promise.resolve(entityOrEntities);
2434
+ }
2435
+
2436
+ savedProcessLog = entityOrEntities;
2437
+ savedSingleActivityLogs = [
2438
+ ...savedSingleActivityLogs,
2439
+ entityOrEntities,
2440
+ ];
2441
+
2442
+ return Promise.resolve(entityOrEntities);
2443
+ },
2444
+ ),
2445
+ });
2446
+ const transactionalMembershipRepository = createRepository<MembershipEntity>({
2447
+ find: jest.fn(() => Promise.resolve(processMemberships)),
2448
+ });
2449
+ const transactionalManagerResolutionRepository =
2450
+ createRepository<ManagerResolutionEntity>({
2451
+ find: jest.fn(() => Promise.resolve(processManagerResolutions)),
2452
+ });
2453
+ const orgUnitQueryBuilder = {
2454
+ andWhere: jest.fn().mockReturnThis(),
2455
+ getMany: jest.fn(() => Promise.resolve(processOrgUnits)),
2456
+ where: jest.fn().mockReturnThis(),
2457
+ };
2458
+ const transactionalOrgUnitRepository = createRepository<OrgUnitEntity>({
2459
+ createQueryBuilder: jest.fn(() => orgUnitQueryBuilder),
2460
+ find: jest.fn(() => Promise.resolve(processOrgUnits)),
2461
+ });
2462
+ const managerQuery = jest.fn<Promise<unknown>, [string, readonly unknown[]]>(
2463
+ () => Promise.resolve([]),
2464
+ );
2465
+ const transactionManager = {
2466
+ getRepository: jest.fn((target: unknown): unknown => {
2467
+ if (target === ApprovalInstanceEntity) {
2468
+ return transactionalInstanceRepository;
2469
+ }
2470
+
2471
+ if (target === WorkflowTokenEntity) {
2472
+ return transactionalTokenRepository;
2473
+ }
2474
+
2475
+ if (target === ActivityLogEntity) {
2476
+ return transactionalActivityRepository;
2477
+ }
2478
+
2479
+ if (target === TaskEntity) {
2480
+ return transactionalTaskRepository;
2481
+ }
2482
+
2483
+ if (target === TaskCandidateEntity) {
2484
+ return transactionalTaskCandidateRepository;
2485
+ }
2486
+
2487
+ if (target === TaskDecisionEntity) {
2488
+ return transactionalTaskDecisionRepository;
2489
+ }
2490
+
2491
+ if (target === NotificationEntity) {
2492
+ return notificationRepository;
2493
+ }
2494
+
2495
+ if (target === MembershipEntity) {
2496
+ return transactionalMembershipRepository;
2497
+ }
2498
+
2499
+ if (target === ManagerResolutionEntity) {
2500
+ return transactionalManagerResolutionRepository;
2501
+ }
2502
+
2503
+ if (target === OrgUnitEntity) {
2504
+ return transactionalOrgUnitRepository;
2505
+ }
2506
+
2507
+ if (target === AdhocDirectiveEntity) {
2508
+ return transactionalAdhocDirectiveRepository;
2509
+ }
2510
+
2511
+ return createRepository<ObjectLiteral>({});
2512
+ }),
2513
+ query: managerQuery,
2514
+ };
2515
+ const transaction = jest.fn(
2516
+ <TResult>(
2517
+ callback: (manager: typeof transactionManager) => Promise<TResult>,
2518
+ ) => callback(transactionManager),
2519
+ );
2520
+ Object.assign(instanceRepository, {
2521
+ manager: {
2522
+ getRepository: jest.fn((target: unknown): unknown => {
2523
+ if (target === MembershipEntity) {
2524
+ return transactionalMembershipRepository;
2525
+ }
2526
+
2527
+ return createRepository<ObjectLiteral>({});
2528
+ }),
2529
+ transaction,
2530
+ },
2531
+ });
2532
+
2533
+ return {
2534
+ managerQuery,
2535
+ notificationFind,
2536
+ rootInstanceCount,
2537
+ rootInstanceFind,
2538
+ rootInstanceQueryBuilder,
2539
+ rootTaskFind,
2540
+ get savedAdhocDirectives(): readonly AdhocDirectiveEntity[] {
2541
+ return processAdhocDirectiveRows;
2542
+ },
2543
+ get savedDecision(): TaskDecisionEntity | null {
2544
+ return savedDecision;
2545
+ },
2546
+ get savedActivityLogs(): readonly ActivityLogEntity[] {
2547
+ return savedActivityLogs;
2548
+ },
2549
+ get savedInstance(): ApprovalInstanceEntity | null {
2550
+ return savedInstance;
2551
+ },
2552
+ get savedProcessLog(): ActivityLogEntity | null {
2553
+ return savedProcessLog;
2554
+ },
2555
+ get savedProcessToken(): WorkflowTokenEntity | null {
2556
+ return savedProcessToken;
2557
+ },
2558
+ get savedSingleActivityLogs(): readonly ActivityLogEntity[] {
2559
+ return savedSingleActivityLogs;
2560
+ },
2561
+ get savedTasks(): readonly TaskEntity[] {
2562
+ return savedTasks;
2563
+ },
2564
+ get savedWorkflowTokens(): readonly WorkflowTokenEntity[] {
2565
+ return processTokens;
2566
+ },
2567
+ get savedToken(): WorkflowTokenEntity | null {
2568
+ return savedToken;
2569
+ },
2570
+ service: new WorkflowEngineService(
2571
+ instanceRepository,
2572
+ tokenRepository,
2573
+ taskRepository,
2574
+ taskCandidateRepository,
2575
+ taskDecisionRepository,
2576
+ notificationRepository,
2577
+ activityLogRepository,
2578
+ adhocDirectiveRepository,
2579
+ templateRepository,
2580
+ templateVersionRepository,
2581
+ formVersionRepository,
2582
+ attachmentService as unknown as AttachmentService,
2583
+ new ConditionService(),
2584
+ delegationService as unknown as DelegationService,
2585
+ notificationService as unknown as NotificationService,
2586
+ signatureService as unknown as SignatureService,
2587
+ serviceTaskDispatcher,
2588
+ ),
2589
+ notificationService,
2590
+ };
2591
+ }
2592
+
2593
+ function createRepository<TEntity extends ObjectLiteral>(
2594
+ value: Readonly<Record<string, unknown>>,
2595
+ ): jest.Mocked<Partial<import('typeorm').Repository<TEntity>>> &
2596
+ import('typeorm').Repository<TEntity> {
2597
+ return value as unknown as jest.Mocked<
2598
+ Partial<import('typeorm').Repository<TEntity>>
2599
+ > &
2600
+ import('typeorm').Repository<TEntity>;
2601
+ }
2602
+
2603
+ function createApprovalInstanceQueryBuilderMock(
2604
+ rootInstanceFind: jest.Mock<
2605
+ Promise<readonly ApprovalInstanceEntity[]>,
2606
+ [Readonly<Record<string, unknown>>?]
2607
+ >,
2608
+ rootInstanceCount: jest.Mock<Promise<number>, []>,
2609
+ ): ApprovalInstanceQueryBuilderMock {
2610
+ const queryBuilder: ApprovalInstanceQueryBuilderMock = {
2611
+ andWhere: jest.fn<
2612
+ ApprovalInstanceQueryBuilderMock,
2613
+ [string, Readonly<Record<string, unknown>>?]
2614
+ >((): ApprovalInstanceQueryBuilderMock => queryBuilder),
2615
+ getCount: rootInstanceCount,
2616
+ getMany: jest.fn(() => rootInstanceFind()),
2617
+ orderBy: jest.fn<
2618
+ ApprovalInstanceQueryBuilderMock,
2619
+ [string, 'ASC' | 'DESC']
2620
+ >((): ApprovalInstanceQueryBuilderMock => queryBuilder),
2621
+ skip: jest.fn<ApprovalInstanceQueryBuilderMock, [number]>(
2622
+ (): ApprovalInstanceQueryBuilderMock => queryBuilder,
2623
+ ),
2624
+ take: jest.fn<ApprovalInstanceQueryBuilderMock, [number]>(
2625
+ (): ApprovalInstanceQueryBuilderMock => queryBuilder,
2626
+ ),
2627
+ };
2628
+
2629
+ return queryBuilder;
2630
+ }
2631
+
2632
+ function createTemplate(
2633
+ currentVersionId: string | null,
2634
+ ): ApprovalTemplateEntity {
2635
+ return {
2636
+ category: null,
2637
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2638
+ createdByMemberId: null,
2639
+ currentVersionId,
2640
+ deletedAt: null,
2641
+ description: null,
2642
+ id: 'template-1',
2643
+ name: '費用申請',
2644
+ updatedAt: new Date('2026-05-04T09:00:00.000Z'),
2645
+ };
2646
+ }
2647
+
2648
+ function createTemplateVersion(
2649
+ status: ApprovalTemplateVersionStatusEnum,
2650
+ ): ApprovalTemplateVersionEntity {
2651
+ return {
2652
+ archivedAt: null,
2653
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2654
+ formDefinitionVersionId: 'form-version-1',
2655
+ id: 'template-version-1',
2656
+ initiatorPolicyCel: null,
2657
+ notificationConfig: null,
2658
+ notificationConfigJson: null,
2659
+ publishedAt: new Date('2026-05-04T09:00:00.000Z'),
2660
+ publishedByMemberId: null,
2661
+ slaDefaults: null,
2662
+ slaDefaultsJson: null,
2663
+ status,
2664
+ templateId: 'template-1',
2665
+ updatedAt: new Date('2026-05-04T09:00:00.000Z'),
2666
+ version: 1,
2667
+ workflowDefinition: {
2668
+ edges: [
2669
+ {
2670
+ data: {},
2671
+ id: 'edge_start_end',
2672
+ source: 'start',
2673
+ target: 'end',
2674
+ type: 'smoothstep',
2675
+ },
2676
+ ],
2677
+ meta: { schemaVersion: 1 },
2678
+ nodes: [
2679
+ {
2680
+ data: { label: '開始' },
2681
+ id: 'start',
2682
+ position: { x: 80, y: 160 },
2683
+ type: 'startEvent',
2684
+ },
2685
+ {
2686
+ data: { endState: 'APPROVED', label: '完成' },
2687
+ id: 'end',
2688
+ position: { x: 520, y: 160 },
2689
+ type: 'endEvent',
2690
+ },
2691
+ ],
2692
+ },
2693
+ workflowDefinitionJson: '',
2694
+ };
2695
+ }
2696
+
2697
+ function createFormVersion(
2698
+ status: FormDefinitionVersionStatusEnum,
2699
+ schema: FormDefinitionSchema = { fields: [], schemaVersion: 1 },
2700
+ ): FormDefinitionVersionEntity {
2701
+ return {
2702
+ archivedAt: null,
2703
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2704
+ formDefinitionId: 'form-1',
2705
+ id: 'form-version-1',
2706
+ publishedAt: new Date('2026-05-04T09:00:00.000Z'),
2707
+ publishedByMemberId: null,
2708
+ schema,
2709
+ schemaJson: '',
2710
+ status,
2711
+ uiSchema: {
2712
+ layout: [],
2713
+ schemaVersion: 1,
2714
+ },
2715
+ uiSchemaJson: '',
2716
+ updatedAt: new Date('2026-05-04T09:00:00.000Z'),
2717
+ version: 1,
2718
+ };
2719
+ }
2720
+
2721
+ function createRequiredReasonFormSchema(): FormDefinitionSchema {
2722
+ return {
2723
+ fields: [
2724
+ {
2725
+ fieldKey: 'reason',
2726
+ label: '事由',
2727
+ required: true,
2728
+ type: 'text',
2729
+ },
2730
+ ],
2731
+ schemaVersion: 1,
2732
+ };
2733
+ }
2734
+
2735
+ function createConditionalAttachmentFormSchema(): FormDefinitionSchema {
2736
+ return {
2737
+ fields: [
2738
+ {
2739
+ fieldKey: 'needsAttachment',
2740
+ label: '需要附件',
2741
+ required: false,
2742
+ type: 'boolean',
2743
+ },
2744
+ {
2745
+ fieldKey: 'attachments',
2746
+ label: '附件',
2747
+ required: false,
2748
+ requiredWhen: 'form.needsAttachment == true',
2749
+ type: 'file_upload',
2750
+ },
2751
+ ],
2752
+ schemaVersion: 1,
2753
+ };
2754
+ }
2755
+
2756
+ function createApprovalInstance({
2757
+ formDefinitionSnapshot,
2758
+ formData,
2759
+ id,
2760
+ initiatorMemberId,
2761
+ state,
2762
+ title,
2763
+ workflowSnapshot,
2764
+ }: {
2765
+ readonly formDefinitionSnapshot?: Readonly<Record<string, unknown>>;
2766
+ readonly formData?: Readonly<Record<string, unknown>>;
2767
+ readonly id?: string;
2768
+ readonly initiatorMemberId?: string;
2769
+ readonly state?: ApprovalInstanceStateEnum;
2770
+ readonly title?: string;
2771
+ readonly workflowSnapshot?: WorkflowDefinition;
2772
+ } = {}): ApprovalInstanceEntity {
2773
+ return Object.assign(new ApprovalInstanceEntity(), {
2774
+ completedAt: null,
2775
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2776
+ formData: formData ?? {},
2777
+ formDefinitionSnapshot: formDefinitionSnapshot ?? {
2778
+ schema: {
2779
+ fields: [],
2780
+ schemaVersion: 1,
2781
+ },
2782
+ },
2783
+ id: id ?? 'instance-1',
2784
+ initiatorMemberId: initiatorMemberId ?? 'member-001',
2785
+ initiatorMetadataSnapshot: {},
2786
+ startedAt: new Date('2026-05-04T09:00:00.000Z'),
2787
+ state: state ?? ApprovalInstanceStateEnum.RUNNING,
2788
+ templateId: 'template-1',
2789
+ templateVersionId: 'template-version-1',
2790
+ title: title ?? '費用申請',
2791
+ updatedAt: new Date('2026-05-04T09:00:00.000Z'),
2792
+ workflowSnapshot: workflowSnapshot ?? {
2793
+ edges: [],
2794
+ meta: { schemaVersion: 1 },
2795
+ nodes: [],
2796
+ },
2797
+ });
2798
+ }
2799
+
2800
+ function createActivityLog(
2801
+ value: Partial<ActivityLogEntity>,
2802
+ ): ActivityLogEntity {
2803
+ return Object.assign(new ActivityLogEntity(), {
2804
+ actorMemberId: value.actorMemberId ?? null,
2805
+ createdAt: value.createdAt ?? new Date('2026-05-04T09:00:00.000Z'),
2806
+ eventType:
2807
+ value.eventType ?? ActivityLogEventTypeEnum.ENGINE_PROCESS_REQUESTED,
2808
+ id: value.id ?? 'activity-log',
2809
+ instanceId: value.instanceId ?? 'instance-1',
2810
+ nodeId: value.nodeId ?? null,
2811
+ payload: value.payload ?? {},
2812
+ taskId: value.taskId ?? null,
2813
+ });
2814
+ }
2815
+
2816
+ function compareTokenCreatedAt(
2817
+ left: WorkflowTokenEntity,
2818
+ right: WorkflowTokenEntity,
2819
+ ): number {
2820
+ return left.createdAt.getTime() - right.createdAt.getTime();
2821
+ }
2822
+
2823
+ function createWorkflowToken(
2824
+ value: Partial<WorkflowTokenEntity>,
2825
+ ): WorkflowTokenEntity {
2826
+ return {
2827
+ consumedAt: value.consumedAt ?? null,
2828
+ createdAt: value.createdAt ?? new Date('2026-05-04T09:00:00.000Z'),
2829
+ currentNodeId: value.currentNodeId ?? 'start',
2830
+ id: value.id ?? 'token-1',
2831
+ instanceId: value.instanceId ?? 'instance-1',
2832
+ parentTokenId: value.parentTokenId ?? null,
2833
+ status: value.status ?? WorkflowTokenStatusEnum.ACTIVE,
2834
+ };
2835
+ }
2836
+
2837
+ function createTask(value: Partial<TaskEntity>): TaskEntity {
2838
+ return Object.assign(new TaskEntity(), {
2839
+ adhocDirectiveId: value.adhocDirectiveId ?? null,
2840
+ adhocOriginTaskId: value.adhocOriginTaskId ?? null,
2841
+ adhocType: value.adhocType ?? null,
2842
+ assigneeMemberId: value.assigneeMemberId ?? 'member-finance',
2843
+ completedAt: value.completedAt ?? null,
2844
+ createdAt: value.createdAt ?? new Date('2026-05-04T09:00:00.000Z'),
2845
+ delegationChain: value.delegationChain ?? [],
2846
+ id: value.id ?? 'task-1',
2847
+ instanceId: value.instanceId ?? 'instance-1',
2848
+ isAdhoc: value.isAdhoc ?? false,
2849
+ nodeId: value.nodeId ?? 'task_finance',
2850
+ openedAt: value.openedAt ?? null,
2851
+ originalAssigneeMemberId:
2852
+ value.originalAssigneeMemberId ?? 'member-finance',
2853
+ slaDueAt: value.slaDueAt ?? null,
2854
+ status: value.status ?? TaskStatusEnum.PENDING,
2855
+ tokenId: value.tokenId ?? 'token-1',
2856
+ });
2857
+ }
2858
+
2859
+ function createAdhocDirective(
2860
+ value: Partial<AdhocDirectiveEntity>,
2861
+ ): AdhocDirectiveEntity {
2862
+ return Object.assign(new AdhocDirectiveEntity(), {
2863
+ channels: value.channels ?? null,
2864
+ comment: value.comment ?? null,
2865
+ consumedAt: value.consumedAt ?? null,
2866
+ createdAt: value.createdAt ?? new Date('2026-05-04T09:00:00.000Z'),
2867
+ createdByMemberId: value.createdByMemberId ?? 'member-finance',
2868
+ id: value.id ?? 'directive-seed-1',
2869
+ instanceId: value.instanceId ?? 'instance-1',
2870
+ onReject: value.onReject ?? null,
2871
+ originNodeId: value.originNodeId ?? 'task_finance',
2872
+ originTaskId: value.originTaskId ?? 'task-1',
2873
+ status: value.status ?? AdhocDirectiveStatusEnum.PENDING,
2874
+ targetKind: value.targetKind ?? AdhocTargetKindEnum.MEMBER,
2875
+ targetValue: value.targetValue ?? {
2876
+ kind: AdhocTargetKindEnum.MEMBER,
2877
+ memberIds: ['member-d'],
2878
+ },
2879
+ type: value.type ?? AdhocDirectiveTypeEnum.COUNTERSIGN,
2880
+ });
2881
+ }
2882
+
2883
+ function createAuthContext(memberId: string): BPMAuthContext {
2884
+ return {
2885
+ memberId,
2886
+ metadata: {},
2887
+ permissions: ['instance.read'],
2888
+ roles: ['REQUESTER'],
2889
+ };
2890
+ }
2891
+
2892
+ function createMembership(value: Partial<MembershipEntity>): MembershipEntity {
2893
+ return Object.assign(new MembershipEntity(), {
2894
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2895
+ effectiveFrom: value.effectiveFrom ?? '2026-01-01',
2896
+ effectiveTo: value.effectiveTo ?? null,
2897
+ id: value.id ?? 'membership-1',
2898
+ isPrimary: value.isPrimary ?? false,
2899
+ memberId: value.memberId ?? 'member-001',
2900
+ orgUnitId: value.orgUnitId ?? 'org-finance',
2901
+ positionId: value.positionId ?? null,
2902
+ updatedAt: new Date('2026-05-04T09:00:00.000Z'),
2903
+ });
2904
+ }
2905
+
2906
+ function createManagerResolution(
2907
+ value: Partial<ManagerResolutionEntity>,
2908
+ ): ManagerResolutionEntity {
2909
+ return Object.assign(new ManagerResolutionEntity(), {
2910
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2911
+ effectiveFrom: value.effectiveFrom ?? '2026-01-01',
2912
+ effectiveTo: value.effectiveTo ?? null,
2913
+ id: value.id ?? 'manager-resolution-1',
2914
+ managerMemberId: value.managerMemberId ?? 'member-manager',
2915
+ priority: value.priority ?? 0,
2916
+ scopeId: value.scopeId ?? 'org-finance',
2917
+ scopeType: value.scopeType ?? ManagerResolutionScopeTypeEnum.ORG_UNIT,
2918
+ });
2919
+ }
2920
+
2921
+ function createOrgUnit(value: Partial<OrgUnitEntity>): OrgUnitEntity {
2922
+ return Object.assign(new OrgUnitEntity(), {
2923
+ code: value.code ?? 'FIN',
2924
+ createdAt: new Date('2026-05-04T09:00:00.000Z'),
2925
+ deletedAt: value.deletedAt ?? null,
2926
+ id: value.id ?? 'org-finance',
2927
+ metadata: value.metadata ?? {},
2928
+ name: value.name ?? '財務部',
2929
+ parentId: value.parentId ?? null,
2930
+ path: value.path ?? 'org.finance',
2931
+ type: value.type ?? OrgUnitTypeEnum.DEPARTMENT,
2932
+ updatedAt: new Date('2026-05-04T09:00:00.000Z'),
2933
+ });
2934
+ }
2935
+
2936
+ function createLinearUserTaskWorkflow({
2937
+ allowAddSigner = false,
2938
+ approverResolver = {
2939
+ memberIds: ['member-finance'],
2940
+ type: 'DIRECT',
2941
+ },
2942
+ slaDuration = null,
2943
+ }: {
2944
+ readonly allowAddSigner?: boolean;
2945
+ readonly approverResolver?: ApproverResolver;
2946
+ readonly slaDuration?: string | null;
2947
+ } = {}): WorkflowDefinition {
2948
+ return {
2949
+ edges: [
2950
+ {
2951
+ data: {},
2952
+ id: 'edge_start_task',
2953
+ source: 'start',
2954
+ target: 'task_finance',
2955
+ type: 'smoothstep',
2956
+ },
2957
+ {
2958
+ data: {},
2959
+ id: 'edge_task_end',
2960
+ source: 'task_finance',
2961
+ target: 'end',
2962
+ type: 'smoothstep',
2963
+ },
2964
+ ],
2965
+ meta: { schemaVersion: 1 },
2966
+ nodes: [
2967
+ {
2968
+ data: { label: '開始' },
2969
+ id: 'start',
2970
+ position: { x: 80, y: 160 },
2971
+ type: 'startEvent',
2972
+ },
2973
+ {
2974
+ data: {
2975
+ allowAddSigner,
2976
+ allowReject: true,
2977
+ allowTransfer: false,
2978
+ approverResolver,
2979
+ decisionPolicy: { type: 'SINGLE' },
2980
+ label: '財務簽核',
2981
+ returnBehavior: {
2982
+ allowReturn: false,
2983
+ allowedTargets: 'PREVIOUS',
2984
+ },
2985
+ ...(slaDuration
2986
+ ? {
2987
+ sla: {
2988
+ duration: slaDuration,
2989
+ onTimeout: 'REMIND' as const,
2990
+ warningAt: 0.5,
2991
+ },
2992
+ }
2993
+ : {}),
2994
+ },
2995
+ id: 'task_finance',
2996
+ position: { x: 300, y: 160 },
2997
+ type: 'userTask',
2998
+ },
2999
+ {
3000
+ data: { endState: 'APPROVED', label: '完成' },
3001
+ id: 'end',
3002
+ position: { x: 520, y: 160 },
3003
+ type: 'endEvent',
3004
+ },
3005
+ ],
3006
+ };
3007
+ }
3008
+
3009
+ function createNotifyServiceTaskWorkflow(): WorkflowDefinition {
3010
+ return {
3011
+ edges: [
3012
+ {
3013
+ data: {},
3014
+ id: 'edge_start_notify',
3015
+ source: 'start',
3016
+ target: 'notify_finance',
3017
+ type: 'smoothstep',
3018
+ },
3019
+ ],
3020
+ meta: { schemaVersion: 1 },
3021
+ nodes: [
3022
+ {
3023
+ data: { label: '開始' },
3024
+ id: 'start',
3025
+ position: { x: 80, y: 160 },
3026
+ type: 'startEvent',
3027
+ },
3028
+ {
3029
+ data: {
3030
+ action: {
3031
+ channels: ['IN_APP'],
3032
+ recipients: {
3033
+ memberIds: ['member-finance', 'member-admin'],
3034
+ type: 'DIRECT',
3035
+ },
3036
+ template: '請留意案件 {{instanceTitle}}。',
3037
+ type: 'NOTIFY',
3038
+ },
3039
+ label: '財務知會',
3040
+ },
3041
+ id: 'notify_finance',
3042
+ position: { x: 300, y: 160 },
3043
+ type: 'serviceTask',
3044
+ },
3045
+ ],
3046
+ };
3047
+ }
3048
+
3049
+ function createWebhookServiceTaskWorkflow(): WorkflowDefinition {
3050
+ return {
3051
+ edges: [
3052
+ {
3053
+ data: {},
3054
+ id: 'edge_start_webhook',
3055
+ source: 'start',
3056
+ target: 'webhook_erp',
3057
+ type: 'smoothstep',
3058
+ },
3059
+ {
3060
+ data: {},
3061
+ id: 'edge_webhook_end',
3062
+ source: 'webhook_erp',
3063
+ target: 'end',
3064
+ type: 'smoothstep',
3065
+ },
3066
+ ],
3067
+ meta: { schemaVersion: 1 },
3068
+ nodes: [
3069
+ {
3070
+ data: { label: '開始' },
3071
+ id: 'start',
3072
+ position: { x: 80, y: 160 },
3073
+ type: 'startEvent',
3074
+ },
3075
+ {
3076
+ data: {
3077
+ action: {
3078
+ payload: '{ title: instance.title, amount: form.amount }',
3079
+ type: 'WEBHOOK',
3080
+ url: 'https://example.test/workflow-hook',
3081
+ },
3082
+ label: '同步 ERP',
3083
+ },
3084
+ id: 'webhook_erp',
3085
+ position: { x: 300, y: 160 },
3086
+ type: 'serviceTask',
3087
+ },
3088
+ {
3089
+ data: { endState: 'APPROVED', label: '完成' },
3090
+ id: 'end',
3091
+ position: { x: 520, y: 160 },
3092
+ type: 'endEvent',
3093
+ },
3094
+ ],
3095
+ };
3096
+ }
3097
+
3098
+ function createSetFormFieldServiceTaskWorkflow(): WorkflowDefinition {
3099
+ return {
3100
+ edges: [
3101
+ {
3102
+ data: {},
3103
+ id: 'edge_start_set_field',
3104
+ source: 'start',
3105
+ target: 'set_approval_level',
3106
+ type: 'smoothstep',
3107
+ },
3108
+ {
3109
+ data: {},
3110
+ id: 'edge_set_field_end',
3111
+ source: 'set_approval_level',
3112
+ target: 'end',
3113
+ type: 'smoothstep',
3114
+ },
3115
+ ],
3116
+ meta: { schemaVersion: 1 },
3117
+ nodes: [
3118
+ {
3119
+ data: { label: '開始' },
3120
+ id: 'start',
3121
+ position: { x: 80, y: 160 },
3122
+ type: 'startEvent',
3123
+ },
3124
+ {
3125
+ data: {
3126
+ action: {
3127
+ fieldPath: 'form.approvalLevel',
3128
+ type: 'SET_FORM_FIELD',
3129
+ value: '"主管簽核"',
3130
+ },
3131
+ label: '設定簽核層級',
3132
+ },
3133
+ id: 'set_approval_level',
3134
+ position: { x: 300, y: 160 },
3135
+ type: 'serviceTask',
3136
+ },
3137
+ {
3138
+ data: { endState: 'APPROVED', label: '完成' },
3139
+ id: 'end',
3140
+ position: { x: 520, y: 160 },
3141
+ type: 'endEvent',
3142
+ },
3143
+ ],
3144
+ };
3145
+ }
3146
+
3147
+ function createExclusiveGatewayWorkflow({
3148
+ highCondition = 'form.amount > 1000',
3149
+ includeStructuredCondition = true,
3150
+ }: {
3151
+ readonly highCondition?: string;
3152
+ readonly includeStructuredCondition?: boolean;
3153
+ } = {}): WorkflowDefinition {
3154
+ return {
3155
+ edges: [
3156
+ {
3157
+ data: {},
3158
+ id: 'edge_start_gateway',
3159
+ source: 'start',
3160
+ target: 'gateway_amount',
3161
+ type: 'smoothstep',
3162
+ },
3163
+ {
3164
+ data: {
3165
+ condition: highCondition,
3166
+ ...(includeStructuredCondition
3167
+ ? {
3168
+ conditionFieldKey: 'amount',
3169
+ conditionOperator: 'GREATER_THAN' as const,
3170
+ conditionValue: '1000',
3171
+ }
3172
+ : {}),
3173
+ label: '金額大於 1000',
3174
+ },
3175
+ id: 'edge_gateway_high',
3176
+ source: 'gateway_amount',
3177
+ target: 'task_high',
3178
+ type: 'smoothstep',
3179
+ },
3180
+ {
3181
+ data: { isDefault: true, label: '其他情況' },
3182
+ id: 'edge_gateway_default',
3183
+ source: 'gateway_amount',
3184
+ target: 'task_default',
3185
+ type: 'smoothstep',
3186
+ },
3187
+ {
3188
+ data: {},
3189
+ id: 'edge_high_end',
3190
+ source: 'task_high',
3191
+ target: 'end',
3192
+ type: 'smoothstep',
3193
+ },
3194
+ {
3195
+ data: {},
3196
+ id: 'edge_default_end',
3197
+ source: 'task_default',
3198
+ target: 'end',
3199
+ type: 'smoothstep',
3200
+ },
3201
+ ],
3202
+ meta: { schemaVersion: 1 },
3203
+ nodes: [
3204
+ {
3205
+ data: { label: '開始' },
3206
+ id: 'start',
3207
+ position: { x: 80, y: 160 },
3208
+ type: 'startEvent',
3209
+ },
3210
+ {
3211
+ data: { direction: 'split', label: '金額分流', triggerMode: 'AND' },
3212
+ id: 'gateway_amount',
3213
+ position: { x: 260, y: 160 },
3214
+ type: 'exclusiveGateway',
3215
+ },
3216
+ createUserTaskNode('task_high', '高額簽核', 'member-high', 440, 80),
3217
+ createUserTaskNode(
3218
+ 'task_default',
3219
+ '一般簽核',
3220
+ 'member-default',
3221
+ 440,
3222
+ 240,
3223
+ ),
3224
+ {
3225
+ data: { endState: 'APPROVED', label: '完成' },
3226
+ id: 'end',
3227
+ position: { x: 660, y: 160 },
3228
+ type: 'endEvent',
3229
+ },
3230
+ ],
3231
+ };
3232
+ }
3233
+
3234
+ function createParallelApprovalWorkflow(
3235
+ finalTriggerMode: 'AND' | 'OR',
3236
+ ): WorkflowDefinition {
3237
+ return {
3238
+ edges: [
3239
+ {
3240
+ data: {},
3241
+ id: 'edge_start_a',
3242
+ source: 'start',
3243
+ target: 'task_a',
3244
+ type: 'smoothstep',
3245
+ },
3246
+ {
3247
+ data: {},
3248
+ id: 'edge_start_b',
3249
+ source: 'start',
3250
+ target: 'task_b',
3251
+ type: 'smoothstep',
3252
+ },
3253
+ {
3254
+ data: {},
3255
+ id: 'edge_a_final',
3256
+ source: 'task_a',
3257
+ target: 'task_final',
3258
+ type: 'smoothstep',
3259
+ },
3260
+ {
3261
+ data: {},
3262
+ id: 'edge_b_final',
3263
+ source: 'task_b',
3264
+ target: 'task_final',
3265
+ type: 'smoothstep',
3266
+ },
3267
+ {
3268
+ data: {},
3269
+ id: 'edge_final_end',
3270
+ source: 'task_final',
3271
+ target: 'end',
3272
+ type: 'smoothstep',
3273
+ },
3274
+ ],
3275
+ meta: { schemaVersion: 1 },
3276
+ nodes: [
3277
+ {
3278
+ data: { label: '開始' },
3279
+ id: 'start',
3280
+ position: { x: 80, y: 160 },
3281
+ type: 'startEvent',
3282
+ },
3283
+ createUserTaskNode('task_a', 'A 簽核', 'member-a', 300, 80),
3284
+ createUserTaskNode('task_b', 'B 簽核', 'member-b', 300, 240),
3285
+ createUserTaskNode(
3286
+ 'task_final',
3287
+ '彙整簽核',
3288
+ 'member-final',
3289
+ 520,
3290
+ 160,
3291
+ finalTriggerMode,
3292
+ ),
3293
+ {
3294
+ data: { endState: 'APPROVED', label: '完成' },
3295
+ id: 'end',
3296
+ position: { x: 740, y: 160 },
3297
+ type: 'endEvent',
3298
+ },
3299
+ ],
3300
+ };
3301
+ }
3302
+
3303
+ function createUserTaskNode(
3304
+ id: string,
3305
+ label: string,
3306
+ memberId: string,
3307
+ x: number,
3308
+ y: number,
3309
+ triggerMode: 'AND' | 'OR' = 'AND',
3310
+ ): WorkflowDefinition['nodes'][number] {
3311
+ return {
3312
+ data: {
3313
+ allowAddSigner: false,
3314
+ allowReject: true,
3315
+ allowTransfer: false,
3316
+ approverResolver: {
3317
+ memberIds: [memberId],
3318
+ type: 'DIRECT',
3319
+ },
3320
+ decisionPolicy: { type: 'SINGLE' },
3321
+ label,
3322
+ returnBehavior: {
3323
+ allowReturn: false,
3324
+ allowedTargets: 'PREVIOUS',
3325
+ },
3326
+ triggerMode,
3327
+ },
3328
+ id,
3329
+ position: { x, y },
3330
+ type: 'userTask',
3331
+ };
3332
+ }
3333
+
3334
+ function readTaskByNodeId(
3335
+ tasks: readonly TaskEntity[],
3336
+ nodeId: string,
3337
+ ): TaskEntity {
3338
+ const task = tasks.find((candidate) => candidate.nodeId === nodeId);
3339
+
3340
+ if (!task) {
3341
+ throw new Error(`Task for node ${nodeId} was not found`);
3342
+ }
3343
+
3344
+ return task;
3345
+ }