@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_keys/data/entities.js +7 -0
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +31 -9
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/instance-status.js +16 -0
- package/dist/modules/workflows/lib/instance-status.js.map +7 -0
- package/dist/modules/workflows/lib/task-handler.js +16 -8
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-sla.js +10 -0
- package/dist/modules/workflows/lib/task-sla.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +29 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/api_keys/data/entities.ts +25 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
- package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
- package/src/modules/api_keys/services/apiKeyService.ts +55 -9
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/ko.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- 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 (
|
|
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
|
-
|
|
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
|