@open-mercato/core 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3894.1.352abf4240

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 (140) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/file/[id]/route.js +7 -2
  3. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  4. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -4
  5. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  6. package/dist/modules/audit_logs/services/accessLogService.js +127 -8
  7. package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
  8. package/dist/modules/auth/backend/auth/profile/page.js +1 -1
  9. package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
  10. package/dist/modules/auth/backend/profile/change-password/page.js +1 -1
  11. package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
  12. package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  14. package/dist/modules/auth/backend/users/create/page.js +6 -1
  15. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  16. package/dist/modules/auth/di.js +17 -3
  17. package/dist/modules/auth/di.js.map +2 -2
  18. package/dist/modules/auth/services/rbacDefaultCache.js +110 -0
  19. package/dist/modules/auth/services/rbacDefaultCache.js.map +7 -0
  20. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +8 -1
  21. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  22. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
  23. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  24. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +3 -2
  25. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
  26. package/dist/modules/configs/cli.js +27 -14
  27. package/dist/modules/configs/cli.js.map +2 -2
  28. package/dist/modules/currencies/api/currencies/route.js +3 -4
  29. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  30. package/dist/modules/currencies/api/exchange-rates/route.js +3 -4
  31. package/dist/modules/currencies/api/exchange-rates/route.js.map +2 -2
  32. package/dist/modules/customers/api/people/route.js +26 -24
  33. package/dist/modules/customers/api/people/route.js.map +2 -2
  34. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +26 -0
  35. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +7 -0
  36. package/dist/modules/directory/utils/organizationScope.js +85 -0
  37. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  38. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  39. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  40. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +1 -1
  41. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  42. package/dist/modules/sales/components/channels/ChannelOfferForm.js +1 -1
  43. package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
  44. package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -1
  45. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  46. package/dist/modules/workflows/backend/definitions/create/page.js +4 -2
  47. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  48. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +20 -3
  49. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  50. package/dist/modules/workflows/components/ActivitiesEditor.js +34 -1
  51. package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
  52. package/dist/modules/workflows/components/NodeEditDialog.js +153 -17
  53. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  54. package/dist/modules/workflows/components/StepsEditor.js +31 -0
  55. package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
  56. package/dist/modules/workflows/components/WorkflowGraph.js +3 -2
  57. package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
  58. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +54 -0
  59. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +7 -0
  60. package/dist/modules/workflows/components/nodes/index.js +3 -1
  61. package/dist/modules/workflows/components/nodes/index.js.map +2 -2
  62. package/dist/modules/workflows/data/validators.js +117 -0
  63. package/dist/modules/workflows/data/validators.js.map +2 -2
  64. package/dist/modules/workflows/di.js +5 -1
  65. package/dist/modules/workflows/di.js.map +2 -2
  66. package/dist/modules/workflows/lib/activity-executor.js +42 -1
  67. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  68. package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
  69. package/dist/modules/workflows/lib/activity-worker-handler.js +24 -0
  70. package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
  71. package/dist/modules/workflows/lib/duration.js +32 -0
  72. package/dist/modules/workflows/lib/duration.js.map +7 -0
  73. package/dist/modules/workflows/lib/event-logger.js +1 -0
  74. package/dist/modules/workflows/lib/event-logger.js.map +2 -2
  75. package/dist/modules/workflows/lib/format-validation-error.js +12 -0
  76. package/dist/modules/workflows/lib/format-validation-error.js.map +7 -0
  77. package/dist/modules/workflows/lib/graph-utils.js +6 -3
  78. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  79. package/dist/modules/workflows/lib/node-type-icons.js +9 -5
  80. package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
  81. package/dist/modules/workflows/lib/signal-handler.js +55 -23
  82. package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
  83. package/dist/modules/workflows/lib/step-handler.js +79 -29
  84. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  85. package/dist/modules/workflows/lib/timer-handler.js +159 -0
  86. package/dist/modules/workflows/lib/timer-handler.js.map +7 -0
  87. package/dist/modules/workflows/lib/workflow-executor.js +1 -1
  88. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  89. package/dist/modules/workflows/workers/workflow-activities.worker.js +20 -4
  90. package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
  91. package/package.json +7 -7
  92. package/src/modules/attachments/api/file/[id]/route.ts +7 -2
  93. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -4
  94. package/src/modules/audit_logs/services/accessLogService.ts +179 -15
  95. package/src/modules/auth/backend/auth/profile/page.tsx +1 -1
  96. package/src/modules/auth/backend/profile/change-password/page.tsx +1 -1
  97. package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
  98. package/src/modules/auth/backend/users/create/page.tsx +6 -1
  99. package/src/modules/auth/di.ts +26 -3
  100. package/src/modules/auth/services/rbacDefaultCache.ts +145 -0
  101. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +8 -1
  102. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +3 -2
  103. package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +3 -2
  104. package/src/modules/configs/cli.ts +34 -13
  105. package/src/modules/currencies/api/currencies/route.ts +3 -4
  106. package/src/modules/currencies/api/exchange-rates/route.ts +3 -4
  107. package/src/modules/customers/api/people/route.ts +27 -25
  108. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +39 -0
  109. package/src/modules/directory/utils/organizationScope.ts +121 -0
  110. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  111. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +1 -1
  112. package/src/modules/sales/components/channels/ChannelOfferForm.tsx +1 -1
  113. package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -2
  114. package/src/modules/workflows/backend/definitions/create/page.tsx +4 -2
  115. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +18 -1
  116. package/src/modules/workflows/components/ActivitiesEditor.tsx +40 -0
  117. package/src/modules/workflows/components/NodeEditDialog.tsx +218 -30
  118. package/src/modules/workflows/components/StepsEditor.tsx +36 -0
  119. package/src/modules/workflows/components/WorkflowGraph.tsx +2 -1
  120. package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +70 -0
  121. package/src/modules/workflows/components/nodes/index.ts +3 -0
  122. package/src/modules/workflows/data/validators.ts +121 -0
  123. package/src/modules/workflows/di.ts +4 -0
  124. package/src/modules/workflows/i18n/de.json +10 -1
  125. package/src/modules/workflows/i18n/en.json +10 -1
  126. package/src/modules/workflows/i18n/es.json +10 -1
  127. package/src/modules/workflows/i18n/pl.json +10 -1
  128. package/src/modules/workflows/lib/activity-executor.ts +86 -2
  129. package/src/modules/workflows/lib/activity-queue-types.ts +18 -11
  130. package/src/modules/workflows/lib/activity-worker-handler.ts +29 -0
  131. package/src/modules/workflows/lib/duration.ts +51 -0
  132. package/src/modules/workflows/lib/event-logger.ts +1 -0
  133. package/src/modules/workflows/lib/format-validation-error.ts +30 -0
  134. package/src/modules/workflows/lib/graph-utils.ts +3 -0
  135. package/src/modules/workflows/lib/node-type-icons.ts +6 -2
  136. package/src/modules/workflows/lib/signal-handler.ts +62 -24
  137. package/src/modules/workflows/lib/step-handler.ts +107 -50
  138. package/src/modules/workflows/lib/timer-handler.ts +213 -0
  139. package/src/modules/workflows/lib/workflow-executor.ts +1 -1
  140. package/src/modules/workflows/workers/workflow-activities.worker.ts +33 -7
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { Handle, Position, NodeProps } from '@xyflow/react'
4
+ import { WorkflowNodeCard } from '../WorkflowNodeCard'
5
+ import { WorkflowStatus } from '../../lib/status-colors'
6
+
7
+ export interface WaitForTimerNodeData {
8
+ label: string
9
+ description?: string
10
+ duration?: string
11
+ until?: string
12
+ config?: { duration?: string; until?: string }
13
+ version?: number
14
+ status?: 'pending' | 'running' | 'completed' | 'error' | 'not_started' | 'in_progress'
15
+ stepNumber?: number
16
+ badge?: string
17
+ tooltip?: string
18
+ executionStatus?: 'completed' | 'active' | 'pending' | 'failed' | 'skipped'
19
+ }
20
+
21
+ /**
22
+ * WaitForTimerNode - Pauses workflow for a duration or until a specific datetime
23
+ * Uses WorkflowNodeCard for consistent styling
24
+ */
25
+ export function WaitForTimerNode({ data, isConnectable, selected }: NodeProps) {
26
+ const nodeData = data as unknown as WaitForTimerNodeData
27
+
28
+ const mapStatus = (status?: string): WorkflowStatus => {
29
+ if (!status || status === 'pending') return 'not_started'
30
+ if (status === 'running' || status === 'in_progress') return 'in_progress'
31
+ if (status === 'completed') return 'completed'
32
+ if (status === 'error') return 'not_started'
33
+ return 'not_started'
34
+ }
35
+
36
+ const workflowStatus = mapStatus(nodeData.status)
37
+
38
+ const duration = nodeData.duration || nodeData.config?.duration
39
+ const until = nodeData.until || nodeData.config?.until
40
+ const description = nodeData.description ||
41
+ (duration ? `Wait for ${duration}` : until ? `Wait until ${until}` : 'Timer-based pause')
42
+
43
+ return (
44
+ <div className="wait-for-timer-node" title={nodeData.tooltip}>
45
+ <Handle
46
+ type="target"
47
+ position={Position.Top}
48
+ id="target"
49
+ isConnectable={isConnectable}
50
+ className="!w-3 !h-3 !bg-[#0080FE] !border-2 !border-white"
51
+ />
52
+
53
+ <WorkflowNodeCard
54
+ title={nodeData.label}
55
+ description={description}
56
+ status={workflowStatus}
57
+ nodeType="waitForTimer"
58
+ selected={selected}
59
+ />
60
+
61
+ <Handle
62
+ type="source"
63
+ position={Position.Bottom}
64
+ id="source"
65
+ isConnectable={isConnectable}
66
+ className="!w-3 !h-3 !bg-[#0080FE] !border-2 !border-white"
67
+ />
68
+ </div>
69
+ )
70
+ }
@@ -15,3 +15,6 @@ export type { SubWorkflowNodeData } from './SubWorkflowNode'
15
15
 
16
16
  export { WaitForSignalNode } from './WaitForSignalNode'
17
17
  export type { WaitForSignalNodeData } from './WaitForSignalNode'
18
+
19
+ export { WaitForTimerNode } from './WaitForTimerNode'
20
+ export type { WaitForTimerNodeData } from './WaitForTimerNode'
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import { parseDuration } from '../lib/duration'
2
3
 
3
4
  /**
4
5
  * Workflows Module - Zod Validators
@@ -8,6 +9,40 @@ import { z } from 'zod'
8
9
 
9
10
  const uuid = z.uuid()
10
11
 
12
+ // Variable interpolation tokens (e.g., {{context.timeout}}) are resolved at
13
+ // run time, so we must skip strict syntax checks on them at save time.
14
+ const containsTemplate = (value: string) => value.includes('{{')
15
+
16
+ export function isValidDurationString(value: unknown): boolean {
17
+ if (typeof value !== 'string' || value.length === 0) return false
18
+ if (containsTemplate(value)) return true
19
+ try {
20
+ const ms = parseDuration(value)
21
+ return Number.isFinite(ms) && ms > 0
22
+ } catch {
23
+ return false
24
+ }
25
+ }
26
+
27
+ export function isValidIsoDateString(value: unknown): boolean {
28
+ if (typeof value !== 'string' || value.length === 0) return false
29
+ if (containsTemplate(value)) return true
30
+ const d = new Date(value)
31
+ return !Number.isNaN(d.getTime())
32
+ }
33
+
34
+ export function isFutureIsoDateString(value: unknown): boolean {
35
+ if (typeof value !== 'string' || value.length === 0) return false
36
+ if (containsTemplate(value)) return true
37
+ const d = new Date(value)
38
+ if (Number.isNaN(d.getTime())) return false
39
+ return d.getTime() > Date.now()
40
+ }
41
+
42
+ const DURATION_ERROR = 'Invalid duration. Use ISO 8601 (e.g., PT5M, PT1H, P1D) or simple format (5m, 1h, 3d)'
43
+ const UNTIL_ERROR = 'Invalid "until". Provide an ISO 8601 datetime string'
44
+ const UNTIL_PAST_ERROR = '"until" must be a future datetime'
45
+
11
46
  // ============================================================================
12
47
  // Enum Schemas - Workflow Types and Statuses
13
48
  // ============================================================================
@@ -169,6 +204,49 @@ export const activityDefinitionSchema = z.object({
169
204
  activityId: z.string().min(1), // ID of compensation activity
170
205
  automatic: z.boolean().default(true).optional() // Auto-trigger on failure
171
206
  }).optional(), // Compensation configuration (Phase 8.2)
207
+ }).superRefine((activity, ctx) => {
208
+ if (activity.activityType !== 'WAIT') return
209
+ const config = activity.config || {}
210
+ const hasDuration = config.duration != null && config.duration !== ''
211
+ const hasUntil = config.until != null && config.until !== ''
212
+ if (!hasDuration && !hasUntil) {
213
+ ctx.addIssue({
214
+ code: 'custom',
215
+ path: ['config'],
216
+ message: 'WAIT activity requires "duration" or "until"',
217
+ })
218
+ return
219
+ }
220
+ if (hasDuration && hasUntil) {
221
+ ctx.addIssue({
222
+ code: 'custom',
223
+ path: ['config'],
224
+ message: 'WAIT activity accepts "duration" OR "until", not both',
225
+ })
226
+ return
227
+ }
228
+ if (hasDuration && !isValidDurationString(config.duration)) {
229
+ ctx.addIssue({
230
+ code: 'custom',
231
+ path: ['config', 'duration'],
232
+ message: DURATION_ERROR,
233
+ })
234
+ }
235
+ if (hasUntil) {
236
+ if (!isValidIsoDateString(config.until)) {
237
+ ctx.addIssue({
238
+ code: 'custom',
239
+ path: ['config', 'until'],
240
+ message: UNTIL_ERROR,
241
+ })
242
+ } else if (!isFutureIsoDateString(config.until)) {
243
+ ctx.addIssue({
244
+ code: 'custom',
245
+ path: ['config', 'until'],
246
+ message: UNTIL_PAST_ERROR,
247
+ })
248
+ }
249
+ }
172
250
  })
173
251
 
174
252
  // Localized validation message schema (for START step pre-conditions)
@@ -201,6 +279,49 @@ export const workflowStepSchema = z.object({
201
279
  retryPolicy: retryPolicySchema.optional(),
202
280
  // Pre-conditions for START step (business rules to validate before workflow can be started)
203
281
  preConditions: z.array(startPreConditionSchema).optional(),
282
+ }).superRefine((step, ctx) => {
283
+ if (step.stepType !== 'WAIT_FOR_TIMER') return
284
+ const config = step.config || {}
285
+ const hasDuration = config.duration != null && config.duration !== ''
286
+ const hasUntil = config.until != null && config.until !== ''
287
+ if (!hasDuration && !hasUntil) {
288
+ ctx.addIssue({
289
+ code: 'custom',
290
+ path: ['config'],
291
+ message: 'WAIT_FOR_TIMER step requires "duration" or "until"',
292
+ })
293
+ return
294
+ }
295
+ if (hasDuration && hasUntil) {
296
+ ctx.addIssue({
297
+ code: 'custom',
298
+ path: ['config'],
299
+ message: 'WAIT_FOR_TIMER step accepts "duration" OR "until", not both',
300
+ })
301
+ return
302
+ }
303
+ if (hasDuration && !isValidDurationString(config.duration)) {
304
+ ctx.addIssue({
305
+ code: 'custom',
306
+ path: ['config', 'duration'],
307
+ message: DURATION_ERROR,
308
+ })
309
+ }
310
+ if (hasUntil) {
311
+ if (!isValidIsoDateString(config.until)) {
312
+ ctx.addIssue({
313
+ code: 'custom',
314
+ path: ['config', 'until'],
315
+ message: UNTIL_ERROR,
316
+ })
317
+ } else if (!isFutureIsoDateString(config.until)) {
318
+ ctx.addIssue({
319
+ code: 'custom',
320
+ path: ['config', 'until'],
321
+ message: UNTIL_PAST_ERROR,
322
+ })
323
+ }
324
+ }
204
325
  })
205
326
 
206
327
  // Transition condition (reference to business rule)
@@ -11,6 +11,8 @@ import * as stepHandler from './lib/step-handler'
11
11
  import * as transitionHandler from './lib/transition-handler'
12
12
  import * as activityExecutor from './lib/activity-executor'
13
13
  import * as eventLogger from './lib/event-logger'
14
+ import * as signalHandler from './lib/signal-handler'
15
+ import * as timerHandler from './lib/timer-handler'
14
16
 
15
17
  export function register(container: AwilixContainer): void {
16
18
  container.register({
@@ -19,5 +21,7 @@ export function register(container: AwilixContainer): void {
19
21
  transitionHandler: asFunction(() => transitionHandler).scoped(),
20
22
  activityExecutor: asFunction(() => activityExecutor).scoped(),
21
23
  eventLogger: asFunction(() => eventLogger).scoped(),
24
+ signalHandler: asFunction(() => signalHandler).scoped(),
25
+ timerHandler: asFunction(() => timerHandler).scoped(),
22
26
  })
23
27
  }
@@ -93,7 +93,13 @@
93
93
  "workflows.activities.types.EXECUTE_FUNCTION": "Funktion ausführen",
94
94
  "workflows.activities.types.SEND_EMAIL": "E-Mail senden",
95
95
  "workflows.activities.types.UPDATE_ENTITY": "Entität aktualisieren",
96
- "workflows.activities.types.WAIT": "Warten",
96
+ "workflows.activities.types.WAIT": "Warten / Verzögerung",
97
+ "workflows.activities.waitDuration": "Dauer",
98
+ "workflows.activities.waitDurationDescription": "ISO 8601 Dauer (z.B. PT5M für 5 Minuten, PT1H für 1 Stunde, P1D für 1 Tag) oder einfaches Format (5m, 1h, 3d)",
99
+ "workflows.activities.waitDurationPlaceholder": "PT5M",
100
+ "workflows.activities.waitOr": "oder",
101
+ "workflows.activities.waitUntil": "Warten bis",
102
+ "workflows.activities.waitUntilDescription": "Bestimmtes Datum und Uhrzeit, bis zu dem gewartet werden soll",
97
103
  "workflows.backToList": "Zurück zu Workflows",
98
104
  "workflows.backend.definitions.visual_editor.title": "Visueller Workflow-Editor",
99
105
  "workflows.checkoutDemo.cart.empty.hint": "Füge Produkte aus dem Dropdown oben hinzu",
@@ -910,6 +916,7 @@
910
916
  "workflows.nodeTypes.subWorkflow": "SUB-WORKFLOW",
911
917
  "workflows.nodeTypes.userTask": "BENUTZERAUFGABE",
912
918
  "workflows.nodeTypes.waitForSignal": "AUF SIGNAL WARTEN",
919
+ "workflows.nodeTypes.waitForTimer": "AUF TIMER WARTEN",
913
920
  "workflows.notifications.task.assigned.body": "Ihnen wurde die Aufgabe \"{taskName}\" im Workflow \"{workflowName}\" zugewiesen{dueDate, select, other { (fällig: {dueDate})}}",
914
921
  "workflows.notifications.task.assigned.title": "Aufgabe zugewiesen",
915
922
  "workflows.orderApproval.approveButton": "Genehmigen",
@@ -1135,6 +1142,7 @@
1135
1142
  "workflows.triggers.status.disabled": "Deaktiviert",
1136
1143
  "workflows.triggers.title": "Ereignis-Trigger",
1137
1144
  "workflows.validation.descriptionMaxLength": "Beschreibung darf maximal 5000 Zeichen lang sein",
1145
+ "workflows.validation.invalidDuration": "Ungültige Dauer. Verwenden Sie ISO 8601 (z. B. PT5M, PT1H, P1D) oder ein einfaches Format (5m, 1h, 3d).",
1138
1146
  "workflows.validation.invalidFormat": "Ungültiges Format",
1139
1147
  "workflows.validation.invalidTransition": "Ungültige Übergangskonfiguration",
1140
1148
  "workflows.validation.invalidWorkflowId": "Workflow-ID darf nur Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche enthalten",
@@ -1144,6 +1152,7 @@
1144
1152
  "workflows.validation.noOrphanSteps": "Alle Schritte müssen verbunden sein",
1145
1153
  "workflows.validation.required": "Dieses Feld ist erforderlich",
1146
1154
  "workflows.validation.uniqueId": "ID muss eindeutig sein",
1155
+ "workflows.validation.untilMustBeFuture": "„Warten bis“ muss ein Datum und eine Uhrzeit in der Zukunft sein.",
1147
1156
  "workflows.validation.workflowIdFormat": "Workflow-ID darf nur Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche enthalten",
1148
1157
  "workflows.validation.workflowIdMaxLength": "Workflow-ID darf maximal 50 Zeichen lang sein",
1149
1158
  "workflows.validation.workflowIdRequired": "Workflow-ID ist erforderlich",
@@ -93,7 +93,13 @@
93
93
  "workflows.activities.types.EXECUTE_FUNCTION": "Execute Function",
94
94
  "workflows.activities.types.SEND_EMAIL": "Send Email",
95
95
  "workflows.activities.types.UPDATE_ENTITY": "Update Entity",
96
- "workflows.activities.types.WAIT": "Wait",
96
+ "workflows.activities.types.WAIT": "Wait / Delay",
97
+ "workflows.activities.waitDuration": "Duration",
98
+ "workflows.activities.waitDurationDescription": "ISO 8601 duration (e.g., PT5M for 5 minutes, PT1H for 1 hour, P1D for 1 day) or simple format (5m, 1h, 3d)",
99
+ "workflows.activities.waitDurationPlaceholder": "PT5M",
100
+ "workflows.activities.waitOr": "or",
101
+ "workflows.activities.waitUntil": "Wait Until",
102
+ "workflows.activities.waitUntilDescription": "Specific date and time to wait until",
97
103
  "workflows.backToList": "Back to workflows",
98
104
  "workflows.backend.definitions.visual_editor.title": "Visual Workflow Editor",
99
105
  "workflows.checkoutDemo.cart.empty.hint": "Add products from the dropdown above",
@@ -910,6 +916,7 @@
910
916
  "workflows.nodeTypes.subWorkflow": "SUB-WORKFLOW",
911
917
  "workflows.nodeTypes.userTask": "USER TASK",
912
918
  "workflows.nodeTypes.waitForSignal": "WAIT FOR SIGNAL",
919
+ "workflows.nodeTypes.waitForTimer": "WAIT FOR TIMER",
913
920
  "workflows.notifications.task.assigned.body": "You have been assigned to task \"{taskName}\" in workflow \"{workflowName}\"{dueDate, select, other { (due: {dueDate})}}",
914
921
  "workflows.notifications.task.assigned.title": "Task Assigned",
915
922
  "workflows.orderApproval.approveButton": "Approve",
@@ -1135,6 +1142,7 @@
1135
1142
  "workflows.triggers.status.disabled": "Disabled",
1136
1143
  "workflows.triggers.title": "Event Triggers",
1137
1144
  "workflows.validation.descriptionMaxLength": "Description must be 5000 characters or less",
1145
+ "workflows.validation.invalidDuration": "Invalid duration. Use ISO 8601 (e.g., PT5M, PT1H, P1D) or simple format (5m, 1h, 3d).",
1138
1146
  "workflows.validation.invalidFormat": "Invalid format",
1139
1147
  "workflows.validation.invalidTransition": "Invalid transition configuration",
1140
1148
  "workflows.validation.invalidWorkflowId": "Workflow ID can only contain lowercase letters, numbers, hyphens, and underscores",
@@ -1144,6 +1152,7 @@
1144
1152
  "workflows.validation.noOrphanSteps": "All steps must be connected",
1145
1153
  "workflows.validation.required": "This field is required",
1146
1154
  "workflows.validation.uniqueId": "ID must be unique",
1155
+ "workflows.validation.untilMustBeFuture": "Wait Until must be a date and time in the future.",
1147
1156
  "workflows.validation.workflowIdFormat": "Workflow ID must contain only lowercase letters, numbers, hyphens, and underscores",
1148
1157
  "workflows.validation.workflowIdMaxLength": "Workflow ID must be 50 characters or less",
1149
1158
  "workflows.validation.workflowIdRequired": "Workflow ID is required",
@@ -93,7 +93,13 @@
93
93
  "workflows.activities.types.EXECUTE_FUNCTION": "Ejecutar funcion",
94
94
  "workflows.activities.types.SEND_EMAIL": "Enviar correo electronico",
95
95
  "workflows.activities.types.UPDATE_ENTITY": "Actualizar entidad",
96
- "workflows.activities.types.WAIT": "Esperar",
96
+ "workflows.activities.types.WAIT": "Esperar / Retraso",
97
+ "workflows.activities.waitDuration": "Duración",
98
+ "workflows.activities.waitDurationDescription": "Duración ISO 8601 (ej. PT5M para 5 minutos, PT1H para 1 hora, P1D para 1 día) o formato simple (5m, 1h, 3d)",
99
+ "workflows.activities.waitDurationPlaceholder": "PT5M",
100
+ "workflows.activities.waitOr": "o",
101
+ "workflows.activities.waitUntil": "Esperar hasta",
102
+ "workflows.activities.waitUntilDescription": "Fecha y hora específica hasta la cual esperar",
97
103
  "workflows.backToList": "Volver a flujos de trabajo",
98
104
  "workflows.backend.definitions.visual_editor.title": "Editor visual de flujos de trabajo",
99
105
  "workflows.checkoutDemo.cart.empty.hint": "Añade productos desde el menú desplegable de arriba",
@@ -910,6 +916,7 @@
910
916
  "workflows.nodeTypes.subWorkflow": "SUBFLUJO DE TRABAJO",
911
917
  "workflows.nodeTypes.userTask": "TAREA DE USUARIO",
912
918
  "workflows.nodeTypes.waitForSignal": "ESPERAR SENAL",
919
+ "workflows.nodeTypes.waitForTimer": "ESPERAR TEMPORIZADOR",
913
920
  "workflows.notifications.task.assigned.body": "Se te ha asignado la tarea \"{taskName}\" en el flujo de trabajo \"{workflowName}\"{dueDate, select, other { (vence: {dueDate})}}",
914
921
  "workflows.notifications.task.assigned.title": "Tarea asignada",
915
922
  "workflows.orderApproval.approveButton": "Aprobar",
@@ -1135,6 +1142,7 @@
1135
1142
  "workflows.triggers.status.disabled": "Deshabilitado",
1136
1143
  "workflows.triggers.title": "Disparadores de eventos",
1137
1144
  "workflows.validation.descriptionMaxLength": "La descripcion debe tener 5000 caracteres o menos",
1145
+ "workflows.validation.invalidDuration": "Duracion no valida. Use ISO 8601 (p. ej. PT5M, PT1H, P1D) o formato simple (5m, 1h, 3d).",
1138
1146
  "workflows.validation.invalidFormat": "Formato no valido",
1139
1147
  "workflows.validation.invalidTransition": "Configuracion de transicion no valida",
1140
1148
  "workflows.validation.invalidWorkflowId": "El ID del flujo de trabajo solo puede contener letras minusculas, numeros, guiones y guiones bajos",
@@ -1144,6 +1152,7 @@
1144
1152
  "workflows.validation.noOrphanSteps": "Todos los pasos deben estar conectados",
1145
1153
  "workflows.validation.required": "Este campo es obligatorio",
1146
1154
  "workflows.validation.uniqueId": "El ID debe ser unico",
1155
+ "workflows.validation.untilMustBeFuture": "Esperar hasta debe ser una fecha y hora futura.",
1147
1156
  "workflows.validation.workflowIdFormat": "El ID del flujo de trabajo solo puede contener letras minusculas, numeros, guiones y guiones bajos",
1148
1157
  "workflows.validation.workflowIdMaxLength": "El ID del flujo de trabajo debe tener 50 caracteres o menos",
1149
1158
  "workflows.validation.workflowIdRequired": "El ID del flujo de trabajo es obligatorio",
@@ -93,7 +93,13 @@
93
93
  "workflows.activities.types.EXECUTE_FUNCTION": "Wykonaj Funkcję",
94
94
  "workflows.activities.types.SEND_EMAIL": "Wyślij Email",
95
95
  "workflows.activities.types.UPDATE_ENTITY": "Zaktualizuj Encję",
96
- "workflows.activities.types.WAIT": "Czekaj",
96
+ "workflows.activities.types.WAIT": "Czekaj / Opóźnienie",
97
+ "workflows.activities.waitDuration": "Czas trwania",
98
+ "workflows.activities.waitDurationDescription": "Format ISO 8601 (np. PT5M dla 5 minut, PT1H dla 1 godziny, P1D dla 1 dnia) lub prosty format (5m, 1h, 3d)",
99
+ "workflows.activities.waitDurationPlaceholder": "PT5M",
100
+ "workflows.activities.waitOr": "lub",
101
+ "workflows.activities.waitUntil": "Czekaj do",
102
+ "workflows.activities.waitUntilDescription": "Konkretna data i godzina, do której czekać",
97
103
  "workflows.backToList": "Powrót do przepływów",
98
104
  "workflows.backend.definitions.visual_editor.title": "Wizualny Edytor Przepływów Pracy",
99
105
  "workflows.checkoutDemo.cart.empty.hint": "Dodaj produkty z listy powyżej",
@@ -910,6 +916,7 @@
910
916
  "workflows.nodeTypes.subWorkflow": "PODPRZEPŁYW",
911
917
  "workflows.nodeTypes.userTask": "ZADANIE UŻYTKOWNIKA",
912
918
  "workflows.nodeTypes.waitForSignal": "CZEKAJ NA SYGNAŁ",
919
+ "workflows.nodeTypes.waitForTimer": "CZEKAJ NA TIMER",
913
920
  "workflows.notifications.task.assigned.body": "Przypisano Ci zadanie \"{taskName}\" w przepływie \"{workflowName}\"{dueDate, select, other { (termin: {dueDate})}}",
914
921
  "workflows.notifications.task.assigned.title": "Przypisano Zadanie",
915
922
  "workflows.orderApproval.approveButton": "Zatwierdź",
@@ -1135,6 +1142,7 @@
1135
1142
  "workflows.triggers.status.disabled": "Wyłączony",
1136
1143
  "workflows.triggers.title": "Wyzwalacze Zdarzeń",
1137
1144
  "workflows.validation.descriptionMaxLength": "Opis musi mieć 5000 znaków lub mniej",
1145
+ "workflows.validation.invalidDuration": "Nieprawidłowy czas trwania. Użyj formatu ISO 8601 (np. PT5M, PT1H, P1D) lub uproszczonego (5m, 1h, 3d).",
1138
1146
  "workflows.validation.invalidFormat": "Nieprawidłowy format",
1139
1147
  "workflows.validation.invalidTransition": "Nieprawidłowa konfiguracja przejścia",
1140
1148
  "workflows.validation.invalidWorkflowId": "ID przepływu może zawierać tylko małe litery, cyfry, myślniki i podkreślenia",
@@ -1144,6 +1152,7 @@
1144
1152
  "workflows.validation.noOrphanSteps": "Wszystkie kroki muszą być połączone",
1145
1153
  "workflows.validation.required": "To pole jest wymagane",
1146
1154
  "workflows.validation.uniqueId": "ID musi być unikalne",
1155
+ "workflows.validation.untilMustBeFuture": "„Czekaj do” musi być datą i godziną w przyszłości.",
1147
1156
  "workflows.validation.workflowIdFormat": "ID przepływu może zawierać tylko małe litery, cyfry, myślniki i podkreślenia",
1148
1157
  "workflows.validation.workflowIdMaxLength": "ID przepływu musi mieć 50 znaków lub mniej",
1149
1158
  "workflows.validation.workflowIdRequired": "ID przepływu jest wymagane",
@@ -25,6 +25,7 @@ import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
25
25
  import { callWebhookConfigSchema } from '../data/validators'
26
26
  import { WorkflowActivityJob, WORKFLOW_ACTIVITIES_QUEUE_NAME } from './activity-queue-types'
27
27
  import { logWorkflowEvent } from './event-logger'
28
+ import { parseDuration } from './duration'
28
29
 
29
30
  export { isPrivateUrl } from '@open-mercato/shared/lib/network'
30
31
 
@@ -54,6 +55,7 @@ export type ActivityType =
54
55
  | 'UPDATE_ENTITY'
55
56
  | 'CALL_WEBHOOK'
56
57
  | 'EXECUTE_FUNCTION'
58
+ | 'WAIT'
57
59
 
58
60
  export interface ActivityDefinition {
59
61
  activityId: string // Unique identifier for activity
@@ -164,9 +166,12 @@ export async function enqueueActivity(
164
166
  userId: context.userId,
165
167
  }
166
168
 
167
- // Enqueue to queue
169
+ // Enqueue to queue (WAIT activities use delayMs for the actual delay)
168
170
  const queue = getActivityQueue()
169
- const jobId = await queue.enqueue(job)
171
+ const enqueueOptions = activity.activityType === 'WAIT' && (interpolatedConfig.duration || interpolatedConfig.until)
172
+ ? { delayMs: calculateWaitDelayMs(interpolatedConfig) }
173
+ : undefined
174
+ const jobId = await queue.enqueue(job, enqueueOptions)
170
175
 
171
176
  // Log event
172
177
  await logWorkflowEvent(em, {
@@ -187,6 +192,41 @@ export async function enqueueActivity(
187
192
  return jobId
188
193
  }
189
194
 
195
+ /**
196
+ * Enqueue a delayed timer job for a WAIT_FOR_TIMER step.
197
+ *
198
+ * The activity worker handles `kind: 'timer'` jobs by calling
199
+ * `timerHandler.fireTimer`, which resumes the paused workflow instance.
200
+ */
201
+ export async function enqueueTimerJob(params: {
202
+ workflowInstanceId: string
203
+ stepInstanceId: string
204
+ tenantId: string
205
+ organizationId: string
206
+ userId?: string
207
+ fireAt: string
208
+ delayMs: number
209
+ }): Promise<string> {
210
+ const { workflowInstanceId, stepInstanceId, tenantId, organizationId, userId, fireAt, delayMs } =
211
+ params
212
+
213
+ const queue = getActivityQueue()
214
+ const jobId = await queue.enqueue(
215
+ {
216
+ kind: 'timer',
217
+ workflowInstanceId,
218
+ stepInstanceId,
219
+ tenantId,
220
+ organizationId,
221
+ userId,
222
+ fireAt,
223
+ },
224
+ { delayMs: delayMs > 0 ? delayMs : undefined }
225
+ )
226
+
227
+ return jobId
228
+ }
229
+
190
230
  // ============================================================================
191
231
  // Main Activity Execution Functions
192
232
  // ============================================================================
@@ -377,6 +417,9 @@ async function executeActivityByType(
377
417
  case 'EXECUTE_FUNCTION':
378
418
  return await executeFunction(interpolatedConfig, context, container)
379
419
 
420
+ case 'WAIT':
421
+ return await executeWait(interpolatedConfig)
422
+
380
423
  default:
381
424
  throw new ActivityExecutionError(
382
425
  `Unknown activity type: ${activity.activityType}`,
@@ -750,6 +793,47 @@ export async function executeFunction(
750
793
  }
751
794
  }
752
795
 
796
+ /**
797
+ * Calculate delay in milliseconds from a WAIT activity config.
798
+ * Supports either `duration` (relative, e.g. "PT5M") or `until` (absolute ISO 8601 datetime).
799
+ */
800
+ function calculateWaitDelayMs(config: { duration?: string; until?: string }): number {
801
+ if (config.until) {
802
+ const targetDate = new Date(config.until)
803
+ if (isNaN(targetDate.getTime())) {
804
+ throw new Error(`WAIT activity: invalid "until" datetime: ${config.until}`)
805
+ }
806
+ const delayMs = targetDate.getTime() - Date.now()
807
+ return Math.max(0, delayMs)
808
+ }
809
+
810
+ if (config.duration) {
811
+ return parseDuration(config.duration)
812
+ }
813
+
814
+ throw new Error('WAIT activity requires "duration" (e.g., "PT5M", "1h") or "until" (ISO 8601 datetime)')
815
+ }
816
+
817
+ /**
818
+ * WAIT activity handler
819
+ *
820
+ * Delays workflow execution for a configured duration or until a specific datetime.
821
+ * - `duration`: relative delay (e.g. "PT5M", "1h", "30s")
822
+ * - `until`: absolute datetime (e.g. "2026-04-15T10:00:00Z")
823
+ * - Sync mode: blocks via sleep (suitable for short delays)
824
+ * - Async mode: delay is handled by the queue's delayMs option;
825
+ * this handler returns immediately when called from the worker
826
+ */
827
+ async function executeWait(config: any): Promise<any> {
828
+ const durationMs = calculateWaitDelayMs(config)
829
+
830
+ // In sync mode, actually sleep for the duration
831
+ // In async mode (called from worker), the delay already happened via queue scheduling
832
+ await sleep(durationMs)
833
+
834
+ return { waited: true, durationMs }
835
+ }
836
+
753
837
  /**
754
838
  * CALL_API activity handler
755
839
  *
@@ -2,25 +2,31 @@
2
2
  * Workflow Activity Queue Types
3
3
  *
4
4
  * Type definitions for async activity execution via queue system.
5
+ * Jobs are discriminated by the optional `kind` field:
6
+ * - `'activity'` (default, back-compat): background execution of a workflow activity
7
+ * - `'timer'`: delayed fire-timer job for a WAIT_FOR_TIMER step
5
8
  */
6
9
 
7
- export interface WorkflowActivityJob {
8
- // Execution context
10
+ export interface WorkflowActivityJobBase {
9
11
  workflowInstanceId: string
10
12
  stepInstanceId?: string
13
+ tenantId: string
14
+ organizationId: string
15
+ userId?: string
16
+ }
17
+
18
+ export interface WorkflowActivityJobActivity extends WorkflowActivityJobBase {
19
+ kind?: 'activity'
11
20
  transitionId?: string
12
21
 
13
- // Activity definition
14
22
  activityId: string
15
23
  activityName: string
16
24
  activityType: string
17
25
  activityConfig: any
18
26
 
19
- // Workflow context (for execution)
20
27
  workflowContext: Record<string, any>
21
28
  stepContext?: Record<string, any>
22
29
 
23
- // Retry & timeout config
24
30
  retryPolicy?: {
25
31
  maxAttempts: number
26
32
  initialIntervalMs: number
@@ -28,13 +34,14 @@ export interface WorkflowActivityJob {
28
34
  maxIntervalMs: number
29
35
  }
30
36
  timeoutMs?: number
37
+ }
31
38
 
32
- // Multi-tenant
33
- tenantId: string
34
- organizationId: string
35
-
36
- // Metadata
37
- userId?: string
39
+ export interface WorkflowActivityJobTimer extends WorkflowActivityJobBase {
40
+ kind: 'timer'
41
+ stepInstanceId: string
42
+ fireAt: string // ISO 8601 timestamp for when the timer should fire
38
43
  }
39
44
 
45
+ export type WorkflowActivityJob = WorkflowActivityJobActivity | WorkflowActivityJobTimer
46
+
40
47
  export const WORKFLOW_ACTIVITIES_QUEUE_NAME = 'workflow-activities'
@@ -34,6 +34,31 @@ export function createActivityWorkerHandler(
34
34
  const { payload } = job
35
35
  const startTime = Date.now()
36
36
 
37
+ // Timer jobs (kind: 'timer') are a distinct flow — they resume a paused
38
+ // workflow instance rather than executing an activity. Handle them first.
39
+ if (payload.kind === 'timer') {
40
+ console.log(
41
+ `[ActivityWorker] Firing timer for instance ${payload.workflowInstanceId} (job ${ctx.jobId})`
42
+ )
43
+ try {
44
+ const { fireTimer } = await import('./timer-handler')
45
+ await fireTimer(em, container, {
46
+ instanceId: payload.workflowInstanceId,
47
+ stepInstanceId: payload.stepInstanceId,
48
+ tenantId: payload.tenantId,
49
+ organizationId: payload.organizationId,
50
+ userId: payload.userId,
51
+ })
52
+ } catch (error: any) {
53
+ console.error(
54
+ `[ActivityWorker] Failed to fire timer for instance ${payload.workflowInstanceId}:`,
55
+ error.message
56
+ )
57
+ throw error
58
+ }
59
+ return
60
+ }
61
+
37
62
  console.log(
38
63
  `[ActivityWorker] Processing activity ${payload.activityId} (job ${ctx.jobId})`
39
64
  )
@@ -79,6 +104,10 @@ export function createActivityWorkerHandler(
79
104
  return await executeCallWebhook(payload.activityConfig, activityContext)
80
105
  case 'EXECUTE_FUNCTION':
81
106
  return await executeFunction(payload.activityConfig, activityContext, container)
107
+ case 'WAIT':
108
+ // Delay already applied by the queue via delayMs; the worker
109
+ // only needs to record completion so the workflow can resume.
110
+ return { waited: true, async: true }
82
111
  default:
83
112
  throw new Error(`Unsupported activity type: ${payload.activityType}`)
84
113
  }