@rytass/bpm-core-nestjs-module 0.1.2 → 0.1.3

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