@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7748.1.d745ba61aa
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/entities/attachment_partition/index.js +2 -0
- package/dist/generated/entities/attachment_partition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
- package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
- 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/attachments/api/file/[id]/route.js +16 -10
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +100 -56
- package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
- package/dist/modules/attachments/api/library/route.js +56 -17
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +2 -0
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +74 -34
- package/dist/modules/attachments/api/route.js.map +3 -3
- package/dist/modules/attachments/api/transfer/route.js +63 -37
- package/dist/modules/attachments/api/transfer/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +41 -0
- package/dist/modules/attachments/data/validators.js.map +7 -0
- package/dist/modules/attachments/di.js +5 -2
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/generators.js +29 -0
- package/dist/modules/attachments/generators.js.map +7 -0
- package/dist/modules/attachments/index.js +8 -1
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-errors.js +35 -0
- package/dist/modules/attachments/lib/access-errors.js.map +7 -0
- package/dist/modules/attachments/lib/access-mutation.js +23 -0
- package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
- package/dist/modules/attachments/lib/access-protection.js +73 -0
- package/dist/modules/attachments/lib/access-protection.js.map +7 -0
- package/dist/modules/attachments/lib/access-query.js +74 -0
- package/dist/modules/attachments/lib/access-query.js.map +7 -0
- package/dist/modules/attachments/lib/access-registry.js +87 -0
- package/dist/modules/attachments/lib/access-registry.js.map +7 -0
- package/dist/modules/attachments/lib/access-runner.js +161 -0
- package/dist/modules/attachments/lib/access-runner.js.map +7 -0
- package/dist/modules/attachments/lib/access-types.js +1 -0
- package/dist/modules/attachments/lib/access-types.js.map +7 -0
- package/dist/modules/attachments/lib/attachment-service.js +14 -6
- package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/partitions.js +3 -2
- package/dist/modules/attachments/lib/partitions.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/target-access-service.js +12 -3
- package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
- package/dist/modules/attachments/setup.js +10 -0
- package/dist/modules/attachments/setup.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +8 -0
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -0
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +1 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +45 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +27 -0
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/staff/notifications.js +4 -2
- package/dist/modules/staff/notifications.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/data/entities.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +18 -4
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-types.js +2 -2
- package/dist/modules/workflows/lib/activity-types.js.map +2 -2
- package/dist/modules/workflows/lib/condition-handler.js +7 -1
- package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +13 -0
- package/dist/modules/workflows/lib/duration.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/step-handler.js +9 -2
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- 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-quick-action.js +13 -10
- package/dist/modules/workflows/lib/task-quick-action.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/transition-handler.js +2 -1
- package/dist/modules/workflows/lib/transition-handler.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/dist/modules/workflows/notifications.js +4 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entities/attachment_partition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
- 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/attachments/api/file/[id]/route.ts +12 -9
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
- package/src/modules/attachments/api/library/[id]/route.ts +87 -58
- package/src/modules/attachments/api/library/route.ts +40 -11
- package/src/modules/attachments/api/partitions/route.ts +2 -0
- package/src/modules/attachments/api/route.ts +68 -35
- package/src/modules/attachments/api/transfer/route.ts +65 -42
- package/src/modules/attachments/data/entities.ts +4 -0
- package/src/modules/attachments/data/validators.ts +36 -0
- package/src/modules/attachments/di.ts +4 -1
- package/src/modules/attachments/generators.ts +27 -0
- package/src/modules/attachments/i18n/de.json +4 -0
- package/src/modules/attachments/i18n/en.json +4 -0
- package/src/modules/attachments/i18n/es.json +4 -0
- package/src/modules/attachments/i18n/ko.json +4 -0
- package/src/modules/attachments/i18n/pl.json +4 -0
- package/src/modules/attachments/index.ts +18 -0
- package/src/modules/attachments/lib/access-errors.ts +32 -0
- package/src/modules/attachments/lib/access-mutation.ts +26 -0
- package/src/modules/attachments/lib/access-protection.ts +83 -0
- package/src/modules/attachments/lib/access-query.ts +86 -0
- package/src/modules/attachments/lib/access-registry.ts +117 -0
- package/src/modules/attachments/lib/access-runner.ts +183 -0
- package/src/modules/attachments/lib/access-types.ts +78 -0
- package/src/modules/attachments/lib/attachment-service.ts +8 -4
- package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
- package/src/modules/attachments/lib/partitions.ts +3 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
- package/src/modules/attachments/lib/target-access-service.ts +10 -3
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
- package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
- package/src/modules/attachments/setup.ts +10 -0
- 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/notifications/api/[id]/action/route.ts +8 -0
- package/src/modules/notifications/api/route.ts +17 -0
- package/src/modules/notifications/data/validators.ts +1 -1
- package/src/modules/notifications/i18n/de.json +2 -0
- package/src/modules/notifications/i18n/en.json +2 -0
- package/src/modules/notifications/i18n/es.json +2 -0
- package/src/modules/notifications/i18n/ko.json +2 -0
- package/src/modules/notifications/i18n/pl.json +2 -0
- package/src/modules/notifications/lib/notificationService.ts +67 -1
- package/src/modules/notifications/lib/routeHelpers.ts +38 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
- package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
- package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
- package/src/modules/staff/notifications.ts +2 -0
- package/src/modules/workflows/AGENTS.md +2 -2
- 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/data/entities.ts +6 -0
- 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/activity-executor.ts +36 -4
- package/src/modules/workflows/lib/activity-types.ts +2 -2
- package/src/modules/workflows/lib/condition-handler.ts +16 -1
- package/src/modules/workflows/lib/duration.ts +25 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/step-handler.ts +16 -1
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-quick-action.ts +19 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/transition-handler.ts +2 -1
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
- package/src/modules/workflows/notifications.ts +4 -0
|
@@ -42,3 +42,22 @@ export function canApplyManualAction(action: ManualGatewayAction, from: UnifiedP
|
|
|
42
42
|
if (targets.includes(from)) return true
|
|
43
43
|
return targets.some((target) => isValidTransition(from, target))
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
const REFUND_STATUSES: Set<UnifiedPaymentStatus> = new Set(['refunded', 'partially_refunded'])
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A provider-confirmed refund proves the payment was captured. When a refund status reaches a
|
|
50
|
+
* transaction whose capture was not recorded yet (the capture webhook is late, or was never
|
|
51
|
+
* delivered), the transaction passes through `captured` on its way to the refund status instead
|
|
52
|
+
* of dropping the refund. Returns that intermediate status, or `null` when no bridge applies.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveImpliedCaptureStatus(
|
|
55
|
+
from: UnifiedPaymentStatus,
|
|
56
|
+
to: UnifiedPaymentStatus,
|
|
57
|
+
): UnifiedPaymentStatus | null {
|
|
58
|
+
if (!REFUND_STATUSES.has(to)) return null
|
|
59
|
+
if (isValidTransition(from, to)) return null
|
|
60
|
+
if (!isValidTransition(from, 'captured')) return null
|
|
61
|
+
if (!isValidTransition('captured', to)) return null
|
|
62
|
+
return 'captured'
|
|
63
|
+
}
|
|
@@ -103,7 +103,7 @@ export async function processPaymentGatewayWebhookJob(
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const providerStatus = typeof event.data.status === 'string' ? event.data.status : ''
|
|
106
|
-
const unifiedStatus = adapter.mapStatus(providerStatus, event.eventType)
|
|
106
|
+
const unifiedStatus = adapter.mapStatus(providerStatus, event.eventType, { ...event, timestamp: new Date(event.timestamp) })
|
|
107
107
|
await writeTransactionLog(integrationLogService, providerKey, scope, transaction.id, 'info', 'Payment gateway webhook received', {
|
|
108
108
|
eventType: event.eventType,
|
|
109
109
|
providerStatus,
|
|
@@ -17,6 +17,7 @@ export const notificationTypes: NotificationTypeDefinition[] = [
|
|
|
17
17
|
variant: 'default',
|
|
18
18
|
icon: 'check',
|
|
19
19
|
commandId: 'staff.leave-requests.accept',
|
|
20
|
+
requiredFeatures: ['staff.leave_requests.manage'],
|
|
20
21
|
},
|
|
21
22
|
{
|
|
22
23
|
id: 'reject',
|
|
@@ -24,6 +25,7 @@ export const notificationTypes: NotificationTypeDefinition[] = [
|
|
|
24
25
|
variant: 'destructive',
|
|
25
26
|
icon: 'x',
|
|
26
27
|
commandId: 'staff.leave-requests.reject',
|
|
28
|
+
requiredFeatures: ['staff.leave_requests.manage'],
|
|
27
29
|
},
|
|
28
30
|
],
|
|
29
31
|
primaryActionId: 'approve',
|
|
@@ -100,7 +100,7 @@ Definition → startWorkflow() → Instance → executeWorkflow() loop
|
|
|
100
100
|
| `UPDATE_ENTITY` | Mutate an entity via the command bus. `CommandBus.execute` does not declare a signal; a write that has already begun cannot be cancelled on timeout. |
|
|
101
101
|
| `EMIT_EVENT` | Emit a domain event to the event bus. `eventBus.emitEvent` does not declare a signal; an emit that has already begun cannot be cancelled on timeout. |
|
|
102
102
|
| `EXECUTE_FUNCTION` | Run a registered custom function. Functions receive `(args, context, signal?: AbortSignal)` — the third arg is additive; existing two-arg functions are unaffected. |
|
|
103
|
-
| `WAIT` | Delay execution for a configured duration |
|
|
103
|
+
| `WAIT` | Delay execution for a configured duration. A sync WAIT over `MAX_INLINE_WAIT_MS` (60 s) that ENDS its transition is queued like an async one (`executeActivities` `queueLongInlineWaits`, opted into by the transition handler only — the AUTOMATED-step caller does not park on a queued result). An in-process WAIT never hands `setTimeout` more than `MAX_TIMER_DELAY_MS` (2^31−1 ms): Node would fire it after 1 ms. |
|
|
104
104
|
| `SET_VARIABLE` | Write values into workflow context at dot paths (assignments land at top-level context, not namespaced under the activity) |
|
|
105
105
|
|
|
106
106
|
## Context Backbone (contextSchema · ledger · picker · samples · test step)
|
|
@@ -934,7 +934,7 @@ Treat a regression here as a broken feature, not a nit.
|
|
|
934
934
|
| Variable | Effect | Default |
|
|
935
935
|
|----------|--------|---------|
|
|
936
936
|
| `OM_WORKFLOWS_ALLOW_PRIVATE_URLS` | When `1`/`true`/`yes`, bypasses the SSRF guard in `CALL_WEBHOOK` so workflow authors can hit `localhost`, RFC1918, and `.internal` targets. For dev only — MUST remain unset in production. | unset (guard enforced) |
|
|
937
|
-
| `OM_WORKFLOWS_MAX_CONDITION_ATTEMPTS` |
|
|
937
|
+
| `OM_WORKFLOWS_MAX_CONDITION_ATTEMPTS` | Floor of the cap on WAIT_FOR_CONDITION poll re-enqueues; reaching the cap forces `CONDITION_TIMED_OUT` so a never-satisfiable predicate cannot saturate the queue. Each waiter's cap is raised to the polls its own timeout needs (`resolveConditionAttemptCap`: `timeoutMs / pollIntervalMs + 1`), so a long timeout is never cut short by the floor. | `1000` |
|
|
938
938
|
| `OM_WORKFLOWS_ENV_INTERPOLATION_ALLOWLIST` | Comma-separated non-secret process env keys allowed for `{{env.*}}` interpolation in workflow activity config. `APP_URL` is always allowed. Never include secrets. | unset (`APP_URL` only) |
|
|
939
939
|
|
|
940
940
|
## DI Services
|
|
@@ -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 })
|
|
@@ -675,6 +675,12 @@ export class StepInstance {
|
|
|
675
675
|
@Property({ name: 'exited_at', type: Date, nullable: true })
|
|
676
676
|
exitedAt?: Date | null
|
|
677
677
|
|
|
678
|
+
/**
|
|
679
|
+
* @deprecated Written on the COMPLETED path only and `null` for any step that
|
|
680
|
+
* lasted 2^31 ms (~24.86 days) or longer, which a Postgres `integer` cannot
|
|
681
|
+
* hold. Derive the duration from `exitedAt - enteredAt` instead; the column
|
|
682
|
+
* is scheduled for removal no earlier than 0.11.0 (see UPGRADE_NOTES.md).
|
|
683
|
+
*/
|
|
678
684
|
@Property({ name: 'execution_time_ms', type: 'integer', nullable: true })
|
|
679
685
|
executionTimeMs?: number | null
|
|
680
686
|
|
|
@@ -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",
|
|
@@ -62,7 +62,7 @@ import './activity-registry-bootstrap'
|
|
|
62
62
|
import { bindActivityExecutor } from './activity-types'
|
|
63
63
|
import { getActivityType } from './activity-registry'
|
|
64
64
|
import { logWorkflowEvent } from './event-logger'
|
|
65
|
-
import { calculateWaitDelayMs, parseDuration } from './duration'
|
|
65
|
+
import { MAX_INLINE_WAIT_MS, assertInProcessWaitDelay, calculateWaitDelayMs, parseDuration } from './duration'
|
|
66
66
|
|
|
67
67
|
export { calculateWaitDelayMs } from './duration'
|
|
68
68
|
import { resolveActivityTimeoutMs } from './activityTimeoutFields'
|
|
@@ -683,6 +683,31 @@ export async function executeActivity(
|
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
+
export interface ExecuteActivitiesOptions {
|
|
687
|
+
/**
|
|
688
|
+
* Hand a synchronous activity with a long `enqueueDelayMs` (a WAIT over
|
|
689
|
+
* `MAX_INLINE_WAIT_MS`) to the activity queue instead of sleeping in-process.
|
|
690
|
+
* Only the transition's LAST activity qualifies, so nothing authored after the
|
|
691
|
+
* wait can run before it elapses. Only callers that park on an `async` result
|
|
692
|
+
* (the transition handler) may opt in — a caller that does not would let the
|
|
693
|
+
* run move on without waiting at all.
|
|
694
|
+
*/
|
|
695
|
+
queueLongInlineWaits?: boolean
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function isLongInlineWait(activity: ActivityDefinition, context: ActivityContext): boolean {
|
|
699
|
+
const entry = getActivityType(activity.activityType)
|
|
700
|
+
if (!entry?.enqueueDelayMs || entry.async.capable === false) return false
|
|
701
|
+
try {
|
|
702
|
+
const delayMs = entry.enqueueDelayMs(
|
|
703
|
+
interpolateActivityConfig(activity.config, context, activity.activityType, activity.activityName)
|
|
704
|
+
)
|
|
705
|
+
return delayMs !== null && delayMs > MAX_INLINE_WAIT_MS
|
|
706
|
+
} catch {
|
|
707
|
+
return false
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
686
711
|
/**
|
|
687
712
|
* Execute multiple activities in sequence
|
|
688
713
|
* Supports both synchronous and asynchronous (queued) execution
|
|
@@ -697,7 +722,8 @@ export async function executeActivities(
|
|
|
697
722
|
em: EntityManager,
|
|
698
723
|
container: AwilixContainer,
|
|
699
724
|
activities: ActivityDefinition[],
|
|
700
|
-
context: ActivityContext
|
|
725
|
+
context: ActivityContext,
|
|
726
|
+
options: ExecuteActivitiesOptions = {}
|
|
701
727
|
): Promise<ActivityExecutionResult[]> {
|
|
702
728
|
const results: ActivityExecutionResult[] = []
|
|
703
729
|
|
|
@@ -709,9 +735,15 @@ export async function executeActivities(
|
|
|
709
735
|
|
|
710
736
|
for (let i = 0; i < activities.length; i++) {
|
|
711
737
|
const activity = activities[i]
|
|
738
|
+
const isLastActivity = i === activities.length - 1
|
|
739
|
+
const queueAsLongWait =
|
|
740
|
+
!activity.async &&
|
|
741
|
+
options.queueLongInlineWaits === true &&
|
|
742
|
+
isLastActivity &&
|
|
743
|
+
isLongInlineWait(activity, context)
|
|
712
744
|
|
|
713
745
|
// Check if activity should run async
|
|
714
|
-
if (activity.async && !forceSynchronous) {
|
|
746
|
+
if ((activity.async || queueAsLongWait) && !forceSynchronous) {
|
|
715
747
|
// Enqueue for background execution
|
|
716
748
|
const jobId = await enqueueActivity(em, activity, context)
|
|
717
749
|
|
|
@@ -1389,7 +1421,7 @@ export async function executeFunction(
|
|
|
1389
1421
|
* this handler returns immediately when called from the worker
|
|
1390
1422
|
*/
|
|
1391
1423
|
export async function executeWait(config: any): Promise<any> {
|
|
1392
|
-
const durationMs = calculateWaitDelayMs(config)
|
|
1424
|
+
const durationMs = assertInProcessWaitDelay(calculateWaitDelayMs(config))
|
|
1393
1425
|
|
|
1394
1426
|
// In sync mode, actually sleep for the duration
|
|
1395
1427
|
// In async mode (called from worker), the delay already happened via queue scheduling
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { z, type ZodTypeAny } from 'zod'
|
|
24
24
|
import { commandRegistry } from '@open-mercato/shared/lib/commands/registry'
|
|
25
25
|
import { getActivityType, registerActivityType } from './activity-registry'
|
|
26
|
-
import { calculateWaitDelayMs } from './duration'
|
|
26
|
+
import { assertInProcessWaitDelay, calculateWaitDelayMs } from './duration'
|
|
27
27
|
import {
|
|
28
28
|
callApiConfigSchema,
|
|
29
29
|
callWebhookConfigSchema,
|
|
@@ -342,7 +342,7 @@ export function registerBuiltinActivityTypes(): void {
|
|
|
342
342
|
{ id: 'until', component: 'datetime' },
|
|
343
343
|
],
|
|
344
344
|
execute: (config) => {
|
|
345
|
-
const durationMs = calculateWaitDelayMs(config)
|
|
345
|
+
const durationMs = assertInProcessWaitDelay(calculateWaitDelayMs(config))
|
|
346
346
|
return new Promise((resolve) => {
|
|
347
347
|
setTimeout(() => resolve({ waited: true, durationMs }), durationMs)
|
|
348
348
|
})
|
|
@@ -106,6 +106,21 @@ export function resolveMaxConditionAttempts(): number {
|
|
|
106
106
|
return parsed
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The attempt cap one waiter actually gets: the configured floor, raised to the
|
|
111
|
+
* number of polls its own timeout needs (`timeoutMs / pollIntervalMs`, plus
|
|
112
|
+
* the entry poll). A fixed 1000 would force a 90-day wait polled hourly to
|
|
113
|
+
* time out after ~42 days. The deadline still ends the wait first; this only
|
|
114
|
+
* bounds a loop whose deadline arithmetic went wrong.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveConditionAttemptCap(
|
|
117
|
+
config: Pick<WaitForConditionConfig, 'timeoutMs' | 'pollIntervalMs'>
|
|
118
|
+
): number {
|
|
119
|
+
const floor = resolveMaxConditionAttempts()
|
|
120
|
+
if (!(config.timeoutMs > 0) || !(config.pollIntervalMs > 0)) return floor
|
|
121
|
+
return Math.max(floor, Math.ceil(config.timeoutMs / config.pollIntervalMs) + 1)
|
|
122
|
+
}
|
|
123
|
+
|
|
109
124
|
/**
|
|
110
125
|
* Read and normalise a WAIT_FOR_CONDITION step's config. Fails closed: the
|
|
111
126
|
* predicate and the timeout are both mandatory, and the poll interval is
|
|
@@ -499,7 +514,7 @@ export async function evaluateWaitCondition(
|
|
|
499
514
|
|
|
500
515
|
const deadlineMs = Date.parse(options.deadlineAt)
|
|
501
516
|
const deadlinePassed = Number.isFinite(deadlineMs) ? nowMs >= deadlineMs : true
|
|
502
|
-
const attemptCapReached = attempts >=
|
|
517
|
+
const attemptCapReached = attempts >= resolveConditionAttemptCap(waiter.config)
|
|
503
518
|
|
|
504
519
|
if (deadlinePassed || attemptCapReached) {
|
|
505
520
|
const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')
|
|
@@ -147,6 +147,31 @@ export function calculateDueDate(duration: string, from: Date = new Date()): Dat
|
|
|
147
147
|
return new Date(from.getTime() + parseDuration(duration))
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Largest delay Node's `setTimeout` honours (a signed 32-bit millisecond count,
|
|
152
|
+
* ~24.86 days). A longer delay is not rejected — it fires after 1 ms with only a
|
|
153
|
+
* `TimeoutOverflowWarning` — so an in-process wait must refuse it explicitly.
|
|
154
|
+
*/
|
|
155
|
+
export const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A WAIT longer than this is not slept inside the executing request: when it is
|
|
159
|
+
* the last activity of its transition it is handed to the activity queue (the
|
|
160
|
+
* path an `async: true` WAIT already takes). One minute keeps the short pauses
|
|
161
|
+
* people use for pacing inline while anything that would hold a request or a
|
|
162
|
+
* worker slot open for minutes becomes durable.
|
|
163
|
+
*/
|
|
164
|
+
export const MAX_INLINE_WAIT_MS = 60_000
|
|
165
|
+
|
|
166
|
+
export function assertInProcessWaitDelay(delayMs: number): number {
|
|
167
|
+
if (delayMs > MAX_TIMER_DELAY_MS) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`WAIT activity: a ${delayMs}ms delay cannot be slept in-process (limit ${MAX_TIMER_DELAY_MS}ms); make it the last activity of its transition, mark it async, or use a WAIT_FOR_TIMER step`
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
return delayMs
|
|
173
|
+
}
|
|
174
|
+
|
|
150
175
|
export function calculateWaitDelayMs(config: { duration?: string; until?: string }): number {
|
|
151
176
|
if (config.until) {
|
|
152
177
|
const targetDate = new Date(config.until)
|
|
@@ -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
|
+
}
|
|
@@ -222,6 +222,21 @@ async function emitStepMilestone(em: EntityManager, stepInstance: StepInstance):
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* `step_instances.execution_time_ms` is a Postgres `integer`, so a step that
|
|
227
|
+
* lasted 2^31 ms (~24.86 days) or longer cannot be stored there — the flush
|
|
228
|
+
* that completes the step would throw after its task already committed,
|
|
229
|
+
* stranding the run. Such a duration persists as `null` (every reader already
|
|
230
|
+
* falls back to `exitedAt - enteredAt`); the exact value still travels on the
|
|
231
|
+
* `STEP_EXITED` event, whose payload is jsonb.
|
|
232
|
+
*/
|
|
233
|
+
export const MAX_PERSISTED_EXECUTION_TIME_MS = 2_147_483_647
|
|
234
|
+
|
|
235
|
+
export function toPersistableExecutionTimeMs(executionTimeMs: number | null): number | null {
|
|
236
|
+
if (executionTimeMs === null || !Number.isFinite(executionTimeMs)) return null
|
|
237
|
+
return Math.abs(executionTimeMs) > MAX_PERSISTED_EXECUTION_TIME_MS ? null : executionTimeMs
|
|
238
|
+
}
|
|
239
|
+
|
|
225
240
|
export async function exitStep(
|
|
226
241
|
em: EntityManager,
|
|
227
242
|
stepInstance: StepInstance,
|
|
@@ -239,7 +254,7 @@ export async function exitStep(
|
|
|
239
254
|
stepInstance.status = 'COMPLETED'
|
|
240
255
|
stepInstance.outputData = outputData || null
|
|
241
256
|
stepInstance.exitedAt = now
|
|
242
|
-
stepInstance.executionTimeMs = executionTimeMs
|
|
257
|
+
stepInstance.executionTimeMs = toPersistableExecutionTimeMs(executionTimeMs)
|
|
243
258
|
stepInstance.updatedAt = now
|
|
244
259
|
|
|
245
260
|
await em.flush()
|
|
@@ -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
|
//
|
|
@@ -43,6 +43,24 @@ export const TASK_QUICK_ACTION_VIEW_ID = 'view'
|
|
|
43
43
|
|
|
44
44
|
export const TASK_DEEP_LINK_HREF = '/backend/tasks/{sourceEntityId}'
|
|
45
45
|
|
|
46
|
+
/** Feature the clicking user must hold to complete a task from its notification. */
|
|
47
|
+
export const TASK_COMPLETE_REQUIRED_FEATURE = 'workflows.tasks.complete'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The one-click completion button. Shared by {@link buildTaskQuickActions} and the
|
|
51
|
+
* `workflows.task.assigned` type declaration, so the stored action id and command id always
|
|
52
|
+
* match what the notifications module allows to dispatch.
|
|
53
|
+
*/
|
|
54
|
+
export function buildTaskQuickCompleteAction(): NotificationTypeAction {
|
|
55
|
+
return {
|
|
56
|
+
id: TASK_QUICK_ACTION_COMPLETE_ID,
|
|
57
|
+
labelKey: 'workflows.notifications.task.actions.complete',
|
|
58
|
+
variant: 'default',
|
|
59
|
+
icon: 'check',
|
|
60
|
+
commandId: TASK_COMPLETE_COMMAND_ID,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
export interface TaskQuickActionInput {
|
|
47
65
|
/** Decision buttons resolved for this task. */
|
|
48
66
|
decisions?: { id: string }[] | null
|
|
@@ -90,16 +108,7 @@ export function buildTaskQuickActions(input: TaskQuickActionInput): Notification
|
|
|
90
108
|
|
|
91
109
|
if (!isOneClickTask(input)) return [viewAction]
|
|
92
110
|
|
|
93
|
-
return [
|
|
94
|
-
viewAction,
|
|
95
|
-
{
|
|
96
|
-
id: TASK_QUICK_ACTION_COMPLETE_ID,
|
|
97
|
-
labelKey: 'workflows.notifications.task.actions.complete',
|
|
98
|
-
variant: 'default',
|
|
99
|
-
icon: 'check',
|
|
100
|
-
commandId: TASK_COMPLETE_COMMAND_ID,
|
|
101
|
-
},
|
|
102
|
-
]
|
|
111
|
+
return [viewAction, buildTaskQuickCompleteAction()]
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
/**
|
|
@@ -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'
|