@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.
Files changed (198) 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/entities/attachment_partition/index.js +2 -0
  5. package/dist/generated/entities/attachment_partition/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
  9. package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
  10. package/dist/modules/api_keys/data/entities.js +7 -0
  11. package/dist/modules/api_keys/data/entities.js.map +2 -2
  12. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
  13. package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
  14. package/dist/modules/api_keys/services/apiKeyService.js +31 -9
  15. package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
  16. package/dist/modules/attachments/api/file/[id]/route.js +16 -10
  17. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  18. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
  19. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  20. package/dist/modules/attachments/api/library/[id]/route.js +100 -56
  21. package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
  22. package/dist/modules/attachments/api/library/route.js +56 -17
  23. package/dist/modules/attachments/api/library/route.js.map +2 -2
  24. package/dist/modules/attachments/api/partitions/route.js +2 -0
  25. package/dist/modules/attachments/api/partitions/route.js.map +2 -2
  26. package/dist/modules/attachments/api/route.js +74 -34
  27. package/dist/modules/attachments/api/route.js.map +3 -3
  28. package/dist/modules/attachments/api/transfer/route.js +63 -37
  29. package/dist/modules/attachments/api/transfer/route.js.map +2 -2
  30. package/dist/modules/attachments/data/entities.js +3 -0
  31. package/dist/modules/attachments/data/entities.js.map +2 -2
  32. package/dist/modules/attachments/data/validators.js +41 -0
  33. package/dist/modules/attachments/data/validators.js.map +7 -0
  34. package/dist/modules/attachments/di.js +5 -2
  35. package/dist/modules/attachments/di.js.map +2 -2
  36. package/dist/modules/attachments/generators.js +29 -0
  37. package/dist/modules/attachments/generators.js.map +7 -0
  38. package/dist/modules/attachments/index.js +8 -1
  39. package/dist/modules/attachments/index.js.map +2 -2
  40. package/dist/modules/attachments/lib/access-errors.js +35 -0
  41. package/dist/modules/attachments/lib/access-errors.js.map +7 -0
  42. package/dist/modules/attachments/lib/access-mutation.js +23 -0
  43. package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
  44. package/dist/modules/attachments/lib/access-protection.js +73 -0
  45. package/dist/modules/attachments/lib/access-protection.js.map +7 -0
  46. package/dist/modules/attachments/lib/access-query.js +74 -0
  47. package/dist/modules/attachments/lib/access-query.js.map +7 -0
  48. package/dist/modules/attachments/lib/access-registry.js +87 -0
  49. package/dist/modules/attachments/lib/access-registry.js.map +7 -0
  50. package/dist/modules/attachments/lib/access-runner.js +161 -0
  51. package/dist/modules/attachments/lib/access-runner.js.map +7 -0
  52. package/dist/modules/attachments/lib/access-types.js +1 -0
  53. package/dist/modules/attachments/lib/access-types.js.map +7 -0
  54. package/dist/modules/attachments/lib/attachment-service.js +14 -6
  55. package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
  56. package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
  57. package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
  58. package/dist/modules/attachments/lib/partitions.js +3 -2
  59. package/dist/modules/attachments/lib/partitions.js.map +2 -2
  60. package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
  61. package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
  62. package/dist/modules/attachments/lib/target-access-service.js +12 -3
  63. package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
  64. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
  65. package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
  66. package/dist/modules/attachments/setup.js +10 -0
  67. package/dist/modules/attachments/setup.js.map +2 -2
  68. package/dist/modules/notifications/api/[id]/action/route.js +8 -0
  69. package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
  70. package/dist/modules/notifications/api/route.js +17 -0
  71. package/dist/modules/notifications/api/route.js.map +2 -2
  72. package/dist/modules/notifications/data/validators.js +1 -1
  73. package/dist/modules/notifications/data/validators.js.map +2 -2
  74. package/dist/modules/notifications/lib/notificationService.js +45 -3
  75. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  76. package/dist/modules/notifications/lib/routeHelpers.js +27 -0
  77. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  78. package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
  79. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  80. package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
  81. package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
  82. package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
  83. package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
  84. package/dist/modules/staff/notifications.js +4 -2
  85. package/dist/modules/staff/notifications.js.map +2 -2
  86. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
  87. package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
  88. package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
  89. package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
  90. package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
  91. package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
  92. package/dist/modules/workflows/data/entities.js.map +2 -2
  93. package/dist/modules/workflows/lib/activity-executor.js +18 -4
  94. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  95. package/dist/modules/workflows/lib/activity-types.js +2 -2
  96. package/dist/modules/workflows/lib/activity-types.js.map +2 -2
  97. package/dist/modules/workflows/lib/condition-handler.js +7 -1
  98. package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
  99. package/dist/modules/workflows/lib/duration.js +13 -0
  100. package/dist/modules/workflows/lib/duration.js.map +2 -2
  101. package/dist/modules/workflows/lib/instance-status.js +16 -0
  102. package/dist/modules/workflows/lib/instance-status.js.map +7 -0
  103. package/dist/modules/workflows/lib/step-handler.js +9 -2
  104. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  105. package/dist/modules/workflows/lib/task-handler.js +16 -8
  106. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  107. package/dist/modules/workflows/lib/task-quick-action.js +13 -10
  108. package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
  109. package/dist/modules/workflows/lib/task-sla.js +10 -0
  110. package/dist/modules/workflows/lib/task-sla.js.map +2 -2
  111. package/dist/modules/workflows/lib/transition-handler.js +2 -1
  112. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  113. package/dist/modules/workflows/lib/workflow-executor.js +29 -1
  114. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  115. package/dist/modules/workflows/notifications.js +4 -0
  116. package/dist/modules/workflows/notifications.js.map +2 -2
  117. package/generated/entities/api_key/index.ts +1 -0
  118. package/generated/entities/attachment_partition/index.ts +1 -0
  119. package/generated/entity-fields-registry.ts +2 -0
  120. package/package.json +7 -7
  121. package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
  122. package/src/modules/api_keys/data/entities.ts +25 -1
  123. package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
  124. package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
  125. package/src/modules/api_keys/services/apiKeyService.ts +55 -9
  126. package/src/modules/attachments/api/file/[id]/route.ts +12 -9
  127. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
  128. package/src/modules/attachments/api/library/[id]/route.ts +87 -58
  129. package/src/modules/attachments/api/library/route.ts +40 -11
  130. package/src/modules/attachments/api/partitions/route.ts +2 -0
  131. package/src/modules/attachments/api/route.ts +68 -35
  132. package/src/modules/attachments/api/transfer/route.ts +65 -42
  133. package/src/modules/attachments/data/entities.ts +4 -0
  134. package/src/modules/attachments/data/validators.ts +36 -0
  135. package/src/modules/attachments/di.ts +4 -1
  136. package/src/modules/attachments/generators.ts +27 -0
  137. package/src/modules/attachments/i18n/de.json +4 -0
  138. package/src/modules/attachments/i18n/en.json +4 -0
  139. package/src/modules/attachments/i18n/es.json +4 -0
  140. package/src/modules/attachments/i18n/ko.json +4 -0
  141. package/src/modules/attachments/i18n/pl.json +4 -0
  142. package/src/modules/attachments/index.ts +18 -0
  143. package/src/modules/attachments/lib/access-errors.ts +32 -0
  144. package/src/modules/attachments/lib/access-mutation.ts +26 -0
  145. package/src/modules/attachments/lib/access-protection.ts +83 -0
  146. package/src/modules/attachments/lib/access-query.ts +86 -0
  147. package/src/modules/attachments/lib/access-registry.ts +117 -0
  148. package/src/modules/attachments/lib/access-runner.ts +183 -0
  149. package/src/modules/attachments/lib/access-types.ts +78 -0
  150. package/src/modules/attachments/lib/attachment-service.ts +8 -4
  151. package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
  152. package/src/modules/attachments/lib/partitions.ts +3 -2
  153. package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
  154. package/src/modules/attachments/lib/target-access-service.ts +10 -3
  155. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
  156. package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
  157. package/src/modules/attachments/setup.ts +10 -0
  158. package/src/modules/auth/i18n/de.json +3 -0
  159. package/src/modules/auth/i18n/en.json +3 -0
  160. package/src/modules/auth/i18n/es.json +3 -0
  161. package/src/modules/auth/i18n/ko.json +3 -0
  162. package/src/modules/auth/i18n/pl.json +3 -0
  163. package/src/modules/notifications/api/[id]/action/route.ts +8 -0
  164. package/src/modules/notifications/api/route.ts +17 -0
  165. package/src/modules/notifications/data/validators.ts +1 -1
  166. package/src/modules/notifications/i18n/de.json +2 -0
  167. package/src/modules/notifications/i18n/en.json +2 -0
  168. package/src/modules/notifications/i18n/es.json +2 -0
  169. package/src/modules/notifications/i18n/ko.json +2 -0
  170. package/src/modules/notifications/i18n/pl.json +2 -0
  171. package/src/modules/notifications/lib/notificationService.ts +67 -1
  172. package/src/modules/notifications/lib/routeHelpers.ts +38 -0
  173. package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
  174. package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
  175. package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
  176. package/src/modules/staff/notifications.ts +2 -0
  177. package/src/modules/workflows/AGENTS.md +2 -2
  178. package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
  179. package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
  180. package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
  181. package/src/modules/workflows/data/entities.ts +6 -0
  182. package/src/modules/workflows/i18n/de.json +2 -0
  183. package/src/modules/workflows/i18n/en.json +2 -0
  184. package/src/modules/workflows/i18n/es.json +2 -0
  185. package/src/modules/workflows/i18n/ko.json +2 -0
  186. package/src/modules/workflows/i18n/pl.json +2 -0
  187. package/src/modules/workflows/lib/activity-executor.ts +36 -4
  188. package/src/modules/workflows/lib/activity-types.ts +2 -2
  189. package/src/modules/workflows/lib/condition-handler.ts +16 -1
  190. package/src/modules/workflows/lib/duration.ts +25 -0
  191. package/src/modules/workflows/lib/instance-status.ts +29 -0
  192. package/src/modules/workflows/lib/step-handler.ts +16 -1
  193. package/src/modules/workflows/lib/task-handler.ts +18 -10
  194. package/src/modules/workflows/lib/task-quick-action.ts +19 -10
  195. package/src/modules/workflows/lib/task-sla.ts +13 -0
  196. package/src/modules/workflows/lib/transition-handler.ts +2 -1
  197. package/src/modules/workflows/lib/workflow-executor.ts +44 -1
  198. 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` | Hard cap on WAIT_FOR_CONDITION poll re-enqueues; reaching it forces `CONDITION_TIMED_OUT` so a never-satisfiable predicate cannot saturate the queue. | `1000` |
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 (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 })
@@ -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 >= resolveMaxConditionAttempts()
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'
@@ -522,7 +522,8 @@ export async function executeTransitionForToken(
522
522
  em,
523
523
  container,
524
524
  transition.activities as ActivityDefinition[],
525
- activityContext
525
+ activityContext,
526
+ { queueLongInlineWaits: true }
526
527
  )
527
528
 
528
529
  activityResults.push(...results)