@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7741.1.61a2652671

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 (49) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/api_key/index.js +2 -0
  3. package/dist/generated/entities/api_key/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/api_keys/data/entities.js +7 -0
  7. package/dist/modules/api_keys/data/entities.js.map +2 -2
  8. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
  9. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
  10. package/dist/modules/api_keys/services/apiKeyService.js +31 -9
  11. package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
  12. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
  13. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
  14. package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
  15. package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
  16. package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
  17. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  18. package/dist/modules/workflows/lib/instance-status.js +16 -0
  19. package/dist/modules/workflows/lib/instance-status.js.map +7 -0
  20. package/dist/modules/workflows/lib/task-handler.js +16 -8
  21. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  22. package/dist/modules/workflows/lib/task-sla.js +10 -0
  23. package/dist/modules/workflows/lib/task-sla.js.map +2 -2
  24. package/dist/modules/workflows/lib/workflow-executor.js +29 -1
  25. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  26. package/generated/entities/api_key/index.ts +1 -0
  27. package/generated/entity-fields-registry.ts +1 -0
  28. package/package.json +7 -7
  29. package/src/modules/api_keys/data/entities.ts +25 -1
  30. package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
  31. package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
  32. package/src/modules/api_keys/services/apiKeyService.ts +55 -9
  33. package/src/modules/auth/i18n/de.json +3 -0
  34. package/src/modules/auth/i18n/en.json +3 -0
  35. package/src/modules/auth/i18n/es.json +3 -0
  36. package/src/modules/auth/i18n/ko.json +3 -0
  37. package/src/modules/auth/i18n/pl.json +3 -0
  38. package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
  39. package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
  40. package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
  41. package/src/modules/workflows/i18n/de.json +2 -0
  42. package/src/modules/workflows/i18n/en.json +2 -0
  43. package/src/modules/workflows/i18n/es.json +2 -0
  44. package/src/modules/workflows/i18n/ko.json +2 -0
  45. package/src/modules/workflows/i18n/pl.json +2 -0
  46. package/src/modules/workflows/lib/instance-status.ts +29 -0
  47. package/src/modules/workflows/lib/task-handler.ts +18 -10
  48. package/src/modules/workflows/lib/task-sla.ts +13 -0
  49. package/src/modules/workflows/lib/workflow-executor.ts +44 -1
@@ -228,6 +228,8 @@
228
228
  "auth.acl.features.forms.submissions.export": "Export submission data (GDPR Art. 15/20 portability)",
229
229
  "auth.acl.features.forms.submissions.manage": "Manage submissions (reopen, assign actors, export)",
230
230
  "auth.acl.features.forms.view": "View forms and submissions",
231
+ "auth.acl.features.gateway_autopay.configure": "Configure Autopay gateway settings",
232
+ "auth.acl.features.gateway_autopay.view": "View Autopay gateway configuration",
231
233
  "auth.acl.features.gateway_stripe.configure": "Configure Stripe gateway settings",
232
234
  "auth.acl.features.gateway_stripe.view": "View Stripe gateway configuration",
233
235
  "auth.acl.features.gateway_tpay.configure": "Tpay 게이트웨이 설정 구성",
@@ -459,6 +461,7 @@
459
461
  "auth.acl.modules.example": "Example Module",
460
462
  "auth.acl.modules.example_customers_sync": "Example Customers Sync",
461
463
  "auth.acl.modules.feature_toggles": "Feature Toggles",
464
+ "auth.acl.modules.gateway_autopay": "Autopay Payment Gateway",
462
465
  "auth.acl.modules.gateway_stripe": "Stripe Payment Gateway",
463
466
  "auth.acl.modules.gateway_tpay": "Tpay 결제 게이트웨이",
464
467
  "auth.acl.modules.inbox_ops": "InboxOps — Email-to-ERP Agent",
@@ -228,6 +228,8 @@
228
228
  "auth.acl.features.forms.submissions.export": "Eksport danych zgłoszeń (przenoszenie danych, RODO art. 15/20)",
229
229
  "auth.acl.features.forms.submissions.manage": "Zarządzanie zgłoszeniami (ponowne otwieranie, przypisywanie uczestników, eksport)",
230
230
  "auth.acl.features.forms.view": "Przeglądanie formularzy i zgłoszeń",
231
+ "auth.acl.features.gateway_autopay.configure": "Konfiguruj ustawienia bramki Autopay",
232
+ "auth.acl.features.gateway_autopay.view": "Wyświetl konfigurację bramki Autopay",
231
233
  "auth.acl.features.gateway_stripe.configure": "Konfiguruj ustawienia bramki Stripe",
232
234
  "auth.acl.features.gateway_stripe.view": "Wyświetl konfigurację bramki Stripe",
233
235
  "auth.acl.features.gateway_tpay.configure": "Konfiguruj ustawienia bramki Tpay",
@@ -459,6 +461,7 @@
459
461
  "auth.acl.modules.example": "Moduł przykładowy",
460
462
  "auth.acl.modules.example_customers_sync": "Przykładowa synchronizacja klientów",
461
463
  "auth.acl.modules.feature_toggles": "Przełączniki funkcji",
464
+ "auth.acl.modules.gateway_autopay": "Bramka płatnicza Autopay",
462
465
  "auth.acl.modules.gateway_stripe": "Bramka płatnicza Stripe",
463
466
  "auth.acl.modules.gateway_tpay": "Bramka płatnicza Tpay",
464
467
  "auth.acl.modules.inbox_ops": "InboxOps — agent e-mail do ERP",
@@ -128,7 +128,11 @@ export async function POST(request: NextRequest, { params }: { params: { id: str
128
128
  // Anything the handler refuses on ownership or status grounds collapses into
129
129
  // the same 404 the predicate would have produced, so the act path discloses
130
130
  // no more than the read path.
131
- if (code === 'TASK_NOT_FOUND' || code === 'TASK_ASSIGNED_TO_ANOTHER_USER') {
131
+ if (
132
+ code === 'TASK_NOT_FOUND' ||
133
+ code === 'TASK_ASSIGNED_TO_ANOTHER_USER' ||
134
+ code === 'WORKFLOW_NOT_ACTIVE'
135
+ ) {
132
136
  return NextResponse.json(PORTAL_TASK_REFUSAL.body, { status: PORTAL_TASK_REFUSAL.status })
133
137
  }
134
138
  logger.error('Error completing portal task', { err: error })
@@ -164,6 +164,12 @@ export async function POST(
164
164
  { status: 409 }
165
165
  )
166
166
  }
167
+ if ((error as { code?: unknown }).code === 'WORKFLOW_NOT_ACTIVE') {
168
+ return NextResponse.json(
169
+ { error: error.message, code: 'WORKFLOW_NOT_ACTIVE' },
170
+ { status: 409 }
171
+ )
172
+ }
167
173
  if (error.message.includes('not found')) {
168
174
  return NextResponse.json(
169
175
  { error: error.message },
@@ -213,7 +219,7 @@ export const openApi: OpenApiRouteDoc = {
213
219
  { status: 401, description: 'Unauthorized', schema: workflowErrorSchema },
214
220
  { status: 403, description: 'The task is visible to the caller but is not theirs to act on — it has no assignee and no role queue, so it must be reassigned first (§6.4: administration widens seeing, never acting).', schema: workflowErrorSchema },
215
221
  { status: 404, description: 'Task not found, or not visible to the caller', schema: workflowErrorSchema },
216
- { status: 409, description: 'Task already completed', schema: workflowErrorSchema },
222
+ { status: 409, description: 'Task already completed, assigned to another user, or its workflow run is no longer active (`code: WORKFLOW_NOT_ACTIVE` — the run is completed, failed, cancelled, compensating or compensated)', schema: workflowErrorSchema },
217
223
  { status: 500, description: 'Internal server error', schema: workflowErrorSchema },
218
224
  ],
219
225
  },
@@ -222,8 +222,12 @@ export default function UserTaskDetailPage({ params }: { params: { id: string }
222
222
  flash(t('workflows.tasks.messages.completed'), 'success')
223
223
  refreshTask()
224
224
  } else {
225
- const failure = result.result as { error?: string } | null
226
- flash(failure?.error || t('workflows.tasks.messages.completeFailed'), 'error')
225
+ const failure = result.result as { error?: string; code?: string } | null
226
+ const failureMessage =
227
+ failure?.code === 'WORKFLOW_NOT_ACTIVE'
228
+ ? t('workflows.tasks.messages.workflowNotActive')
229
+ : failure?.error || t('workflows.tasks.messages.completeFailed')
230
+ flash(failureMessage, 'error')
227
231
  }
228
232
  } catch (err) {
229
233
  logger.error('Error completing task', { err })
@@ -729,6 +729,7 @@
729
729
  "workflows.events.types.TransitionFired": "Übergang ausgelöst",
730
730
  "workflows.events.types.TransitionPausedForActivities": "Übergang für Aktivitäten pausiert",
731
731
  "workflows.events.types.USER_TASK_ASSIGNED": "Benutzeraufgabe zugewiesen",
732
+ "workflows.events.types.USER_TASK_CANCELLED": "Benutzeraufgabe abgebrochen",
732
733
  "workflows.events.types.USER_TASK_CLAIMED": "Benutzeraufgabe übernommen",
733
734
  "workflows.events.types.USER_TASK_COMPLETED": "Benutzeraufgabe abgeschlossen",
734
735
  "workflows.events.types.USER_TASK_CREATED": "Benutzeraufgabe erstellt",
@@ -1851,6 +1852,7 @@
1851
1852
  "workflows.tasks.messages.loadFailed": "Benutzeraufgaben konnten nicht geladen werden",
1852
1853
  "workflows.tasks.messages.reassignFailed": "Aufgabe konnte nicht neu zugewiesen werden",
1853
1854
  "workflows.tasks.messages.reassigned": "Aufgabe erfolgreich neu zugewiesen",
1855
+ "workflows.tasks.messages.workflowNotActive": "Der Workflow ist nicht mehr aktiv",
1854
1856
  "workflows.tasks.myTasks": "Meine Aufgaben",
1855
1857
  "workflows.tasks.overdue": "Überfällig",
1856
1858
  "workflows.tasks.plural": "Benutzeraufgaben",
@@ -729,6 +729,7 @@
729
729
  "workflows.events.types.TransitionFired": "Transition Fired",
730
730
  "workflows.events.types.TransitionPausedForActivities": "Transition Paused for Activities",
731
731
  "workflows.events.types.USER_TASK_ASSIGNED": "User Task Assigned",
732
+ "workflows.events.types.USER_TASK_CANCELLED": "User Task Cancelled",
732
733
  "workflows.events.types.USER_TASK_CLAIMED": "User Task Claimed",
733
734
  "workflows.events.types.USER_TASK_COMPLETED": "User Task Completed",
734
735
  "workflows.events.types.USER_TASK_CREATED": "User Task Created",
@@ -1851,6 +1852,7 @@
1851
1852
  "workflows.tasks.messages.loadFailed": "Failed to load user tasks",
1852
1853
  "workflows.tasks.messages.reassignFailed": "Failed to reassign task",
1853
1854
  "workflows.tasks.messages.reassigned": "Task reassigned successfully",
1855
+ "workflows.tasks.messages.workflowNotActive": "Workflow is no longer active",
1854
1856
  "workflows.tasks.myTasks": "My Tasks",
1855
1857
  "workflows.tasks.overdue": "Overdue",
1856
1858
  "workflows.tasks.plural": "User Tasks",
@@ -729,6 +729,7 @@
729
729
  "workflows.events.types.TransitionFired": "Transicion activada",
730
730
  "workflows.events.types.TransitionPausedForActivities": "Transicion pausada por actividades",
731
731
  "workflows.events.types.USER_TASK_ASSIGNED": "Tarea de usuario asignada",
732
+ "workflows.events.types.USER_TASK_CANCELLED": "Tarea de usuario cancelada",
732
733
  "workflows.events.types.USER_TASK_CLAIMED": "Tarea de usuario reclamada",
733
734
  "workflows.events.types.USER_TASK_COMPLETED": "Tarea de usuario completada",
734
735
  "workflows.events.types.USER_TASK_CREATED": "Tarea de usuario creada",
@@ -1851,6 +1852,7 @@
1851
1852
  "workflows.tasks.messages.loadFailed": "No se pudieron cargar las tareas de usuario",
1852
1853
  "workflows.tasks.messages.reassignFailed": "No se pudo reasignar la tarea",
1853
1854
  "workflows.tasks.messages.reassigned": "Tarea reasignada correctamente",
1855
+ "workflows.tasks.messages.workflowNotActive": "El flujo de trabajo ya no está activo",
1854
1856
  "workflows.tasks.myTasks": "Mis tareas",
1855
1857
  "workflows.tasks.overdue": "Vencida",
1856
1858
  "workflows.tasks.plural": "Tareas de usuario",
@@ -729,6 +729,7 @@
729
729
  "workflows.events.types.TransitionFired": "전환 발동됨",
730
730
  "workflows.events.types.TransitionPausedForActivities": "활동 대기로 전환 일시 중지",
731
731
  "workflows.events.types.USER_TASK_ASSIGNED": "사용자 작업 할당됨",
732
+ "workflows.events.types.USER_TASK_CANCELLED": "사용자 작업 취소됨",
732
733
  "workflows.events.types.USER_TASK_CLAIMED": "사용자 작업 수령됨",
733
734
  "workflows.events.types.USER_TASK_COMPLETED": "사용자 작업 완료",
734
735
  "workflows.events.types.USER_TASK_CREATED": "사용자 작업 생성됨",
@@ -1851,6 +1852,7 @@
1851
1852
  "workflows.tasks.messages.loadFailed": "사용자 작업을 불러오지 못했습니다",
1852
1853
  "workflows.tasks.messages.reassignFailed": "Failed to reassign task",
1853
1854
  "workflows.tasks.messages.reassigned": "Task reassigned successfully",
1855
+ "workflows.tasks.messages.workflowNotActive": "워크플로가 더 이상 활성 상태가 아닙니다",
1854
1856
  "workflows.tasks.myTasks": "내 작업",
1855
1857
  "workflows.tasks.overdue": "기한 초과",
1856
1858
  "workflows.tasks.plural": "사용자 작업",
@@ -729,6 +729,7 @@
729
729
  "workflows.events.types.TransitionFired": "Przejście Wykonane",
730
730
  "workflows.events.types.TransitionPausedForActivities": "Przejście Wstrzymane dla Działań",
731
731
  "workflows.events.types.USER_TASK_ASSIGNED": "Zadanie Użytkownika Przypisane",
732
+ "workflows.events.types.USER_TASK_CANCELLED": "Zadanie Użytkownika Anulowane",
732
733
  "workflows.events.types.USER_TASK_CLAIMED": "Zadanie Użytkownika Przejęte",
733
734
  "workflows.events.types.USER_TASK_COMPLETED": "Zadanie Użytkownika Ukończone",
734
735
  "workflows.events.types.USER_TASK_CREATED": "Zadanie Użytkownika Utworzone",
@@ -1851,6 +1852,7 @@
1851
1852
  "workflows.tasks.messages.loadFailed": "Nie udało się załadować zadań użytkowników",
1852
1853
  "workflows.tasks.messages.reassignFailed": "Nie udało się ponownie przypisać zadania",
1853
1854
  "workflows.tasks.messages.reassigned": "Zadanie zostało ponownie przypisane",
1855
+ "workflows.tasks.messages.workflowNotActive": "Przepływ pracy nie jest już aktywny",
1854
1856
  "workflows.tasks.myTasks": "Moje Zadania",
1855
1857
  "workflows.tasks.overdue": "Po Terminie",
1856
1858
  "workflows.tasks.plural": "Zadania Użytkownika",
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Workflow instance statuses a user-task completion or a user-task SLA job must
3
+ * not touch.
4
+ *
5
+ * PURE: a type-only import, so the engine does not depend on the metrics
6
+ * module's `WORKFLOW_TERMINAL_STATUSES` for a runtime decision.
7
+ *
8
+ * The name means "a completion must not touch this run", NOT "its tasks are
9
+ * closed": a FAILED run keeps its open task so a retry can pick it up.
10
+ * `COMPENSATING` is included although it is not terminal — `compensateWorkflow`
11
+ * holds it while rollback activities run and then overwrites it with
12
+ * `COMPENSATED` or `FAILED`, and a completion in that window would advance a
13
+ * run that is being rolled back.
14
+ */
15
+
16
+ import type { WorkflowInstanceStatus } from '../data/entities'
17
+
18
+ export const RUN_STATUSES_CLOSED_TO_USER_TASKS: readonly WorkflowInstanceStatus[] = [
19
+ 'COMPLETED',
20
+ 'FAILED',
21
+ 'CANCELLED',
22
+ 'COMPENSATING',
23
+ 'COMPENSATED',
24
+ ]
25
+
26
+ export function isRunClosedToUserTasks(status: string | null | undefined): boolean {
27
+ if (!status) return false
28
+ return (RUN_STATUSES_CLOSED_TO_USER_TASKS as readonly string[]).includes(status)
29
+ }
@@ -27,6 +27,7 @@ import {
27
27
  } from './task-decisions'
28
28
  import { loadTaskDecisionContext } from './task-decision-context'
29
29
  import { readRequiredFormFields } from './task-form-schema'
30
+ import { isRunClosedToUserTasks } from './instance-status'
30
31
  import * as stepHandler from './step-handler'
31
32
  import * as transitionHandler from './transition-handler'
32
33
  import { createLogger } from '@open-mercato/shared/lib/logger'
@@ -174,6 +175,23 @@ export async function completeUserTask(
174
175
  )
175
176
  }
176
177
 
178
+ const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)
179
+ if (!instance) {
180
+ throw new UserTaskError(
181
+ 'Workflow instance not found',
182
+ 'INSTANCE_NOT_FOUND',
183
+ { workflowInstanceId: task.workflowInstanceId }
184
+ )
185
+ }
186
+
187
+ if (isRunClosedToUserTasks(instance.status)) {
188
+ throw new UserTaskError(
189
+ 'Workflow is no longer active',
190
+ 'WORKFLOW_NOT_ACTIVE',
191
+ { taskId, workflowInstanceId: instance.id, status: instance.status }
192
+ )
193
+ }
194
+
177
195
  // The chosen decision rides along inside the completion payload the task
178
196
  // already persists — no new column, and downstream route conditions can read
179
197
  // it once the payload is merged into the run context.
@@ -190,16 +208,6 @@ export async function completeUserTask(
190
208
 
191
209
  await em.flush()
192
210
 
193
- // Fetch workflow instance
194
- const instance = await em.findOne(WorkflowInstance, task.workflowInstanceId)
195
- if (!instance) {
196
- throw new UserTaskError(
197
- 'Workflow instance not found',
198
- 'INSTANCE_NOT_FOUND',
199
- { workflowInstanceId: task.workflowInstanceId }
200
- )
201
- }
202
-
203
211
  // Branch-scoped completion: when the task belongs to a parallel branch,
204
212
  // merge form data into the branch namespace and resume just that branch.
205
213
  //
@@ -32,6 +32,7 @@ import {
32
32
  type AgentReviewStepLike,
33
33
  } from './agent-review'
34
34
  import { findDefinitionForInstance } from './find-definition'
35
+ import { isRunClosedToUserTasks } from './instance-status'
35
36
  import { emitWorkflowsEvent } from '../events'
36
37
  import type * as stepHandlerModule from './step-handler'
37
38
  import type * as transitionHandlerModule from './transition-handler'
@@ -300,7 +301,9 @@ type BreachOutcome =
300
301
  | 'reassigned'
301
302
  | 'routed'
302
303
  | 'route_skipped_branch'
304
+ | 'route_skipped_closed_run'
303
305
  | 'attention'
306
+ | 'attention_skipped_closed_run'
304
307
 
305
308
  interface AppliedBreachHandling {
306
309
  outcome: BreachOutcome
@@ -393,6 +396,9 @@ async function applyBreachHandling(
393
396
 
394
397
  if (resolution.kind === 'attention') {
395
398
  if (!instance) return { outcome: 'none' }
399
+ if (isRunClosedToUserTasks(instance.status)) {
400
+ return { outcome: 'attention_skipped_closed_run', detail: { stepId, status: instance.status } }
401
+ }
396
402
  await markInstanceForAttention(em, instance, stepId, task, now)
397
403
  return { outcome: 'attention', detail: { stepId } }
398
404
  }
@@ -430,6 +436,13 @@ async function applyBreachHandling(
430
436
  return { outcome: 'route_skipped_branch', detail: { transitionId: resolution.transition.transitionId } }
431
437
  }
432
438
 
439
+ if (isRunClosedToUserTasks(instance?.status)) {
440
+ return {
441
+ outcome: 'route_skipped_closed_run',
442
+ detail: { transitionId: resolution.transition.transitionId, status: instance?.status },
443
+ }
444
+ }
445
+
433
446
  // The task is superseded by the route, not abandoned: `ESCALATED` is an
434
447
  // existing `UserTaskStatus`, so no state machine changes.
435
448
  task.status = 'ESCALATED'
@@ -13,8 +13,9 @@
13
13
  import { EntityManager, LockMode } from '@mikro-orm/core'
14
14
  import type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'
15
15
  import type { AwilixContainer } from 'awilix'
16
- import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
16
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
17
17
  import {
18
+ UserTask,
18
19
  WorkflowDefinition,
19
20
  WorkflowInstance,
20
21
  WorkflowEvent,
@@ -1487,6 +1488,28 @@ export async function completeWorkflow(
1487
1488
  }
1488
1489
 
1489
1490
  // Original completion logic (no compensation needed or status is COMPLETED/CANCELLED)
1491
+
1492
+ // A cancelled run has no recovery path, so its open tasks can never become
1493
+ // actionable again: close them in the same flush as the status. CANCELLED
1494
+ // only — a FAILED run parked on a task is retried against that same task.
1495
+ // Loaded before the run is mutated so no read sits between a scalar change
1496
+ // and the flush that persists it.
1497
+ const cancelledTasks =
1498
+ status === 'CANCELLED'
1499
+ ? await findWithDecryption(
1500
+ em as PostgreSqlEntityManager,
1501
+ UserTask,
1502
+ {
1503
+ workflowInstanceId: instance.id,
1504
+ tenantId: instance.tenantId,
1505
+ organizationId: instance.organizationId,
1506
+ status: { $in: ['PENDING', 'IN_PROGRESS'] },
1507
+ },
1508
+ undefined,
1509
+ { tenantId: instance.tenantId, organizationId: instance.organizationId }
1510
+ )
1511
+ : []
1512
+
1490
1513
  const now = new Date()
1491
1514
  instance.status = status
1492
1515
  instance.updatedAt = now
@@ -1514,8 +1537,27 @@ export async function completeWorkflow(
1514
1537
 
1515
1538
  await writeRunOutcome(em, instance, status)
1516
1539
 
1540
+ for (const task of cancelledTasks) {
1541
+ task.status = 'CANCELLED'
1542
+ task.updatedAt = now
1543
+ }
1544
+
1517
1545
  await em.flush()
1518
1546
 
1547
+ // Logged before the terminal event so WORKFLOW_CANCELLED stays last in the
1548
+ // run's timeline.
1549
+ for (const task of cancelledTasks) {
1550
+ await logWorkflowEvent(em, {
1551
+ workflowInstanceId: instanceId,
1552
+ stepInstanceId: task.stepInstanceId,
1553
+ ...(task.branchInstanceId ? { branchInstanceId: task.branchInstanceId } : {}),
1554
+ eventType: 'USER_TASK_CANCELLED',
1555
+ eventData: { taskId: task.id, taskName: task.taskName, reason: 'workflow-cancelled' },
1556
+ tenantId: instance.tenantId,
1557
+ organizationId: instance.organizationId,
1558
+ })
1559
+ }
1560
+
1519
1561
  // Log completion event
1520
1562
  const eventType =
1521
1563
  status === 'COMPLETED'
@@ -2044,6 +2086,7 @@ async function logWorkflowEvent(
2044
2086
  event: {
2045
2087
  workflowInstanceId: string
2046
2088
  stepInstanceId?: string
2089
+ branchInstanceId?: string
2047
2090
  eventType: string
2048
2091
  eventData: any
2049
2092
  userId?: string