@open-mercato/core 0.6.2-develop.3400.2.d948268f50 → 0.6.2-develop.3406.1.2b403f40da
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/workflow_definition/index.js +2 -0
- package/dist/generated/entities/workflow_definition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/sales/workflows.js +180 -0
- package/dist/modules/sales/workflows.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js +172 -0
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +205 -0
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/route.js +91 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/route.js +32 -9
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/serialize.js +36 -1
- package/dist/modules/workflows/api/definitions/serialize.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +161 -52
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +37 -34
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +134 -41
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/cli.js +19 -121
- package/dist/modules/workflows/cli.js.map +2 -2
- package/dist/modules/workflows/components/formConfig.js +1 -1
- package/dist/modules/workflows/components/formConfig.js.map +2 -2
- package/dist/modules/workflows/data/entities.js +3 -0
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +2 -2
- package/dist/modules/workflows/data/validators.js.map +2 -2
- package/dist/modules/workflows/events.js +2 -0
- package/dist/modules/workflows/events.js.map +2 -2
- package/dist/modules/workflows/examples/guard-rules-example.json +6 -6
- package/dist/modules/workflows/examples/order-approval-guard-rules.json +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +4 -4
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/dist/modules/workflows/lib/code-registry.js +40 -0
- package/dist/modules/workflows/lib/code-registry.js.map +7 -0
- package/dist/modules/workflows/lib/find-definition.js +58 -0
- package/dist/modules/workflows/lib/find-definition.js.map +7 -0
- package/dist/modules/workflows/lib/seeds.js +0 -3
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/lib/start-validator.js +1 -20
- package/dist/modules/workflows/lib/start-validator.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +1 -19
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260414120000.js +13 -0
- package/dist/modules/workflows/migrations/Migration20260414120000.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260428102318.js +32 -0
- package/dist/modules/workflows/migrations/Migration20260428102318.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +1 -1
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +1 -1
- package/dist/modules/workflows/workflows.js +260 -0
- package/dist/modules/workflows/workflows.js.map +7 -0
- package/generated/entities/workflow_definition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/sales/workflows.ts +184 -0
- package/src/modules/workflows/api/definitions/[id]/customize/route.ts +197 -0
- package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +252 -0
- package/src/modules/workflows/api/definitions/[id]/route.ts +108 -4
- package/src/modules/workflows/api/definitions/route.ts +54 -10
- package/src/modules/workflows/api/definitions/serialize.ts +42 -0
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +135 -11
- package/src/modules/workflows/backend/definitions/page.tsx +45 -33
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +222 -97
- package/src/modules/workflows/cli.ts +32 -155
- package/src/modules/workflows/components/formConfig.tsx +2 -2
- package/src/modules/workflows/data/entities.ts +4 -1
- package/src/modules/workflows/data/validators.ts +4 -3
- package/src/modules/workflows/events.ts +2 -0
- package/src/modules/workflows/examples/guard-rules-example.json +6 -6
- package/src/modules/workflows/examples/order-approval-guard-rules.json +2 -2
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +4 -4
- package/src/modules/workflows/i18n/de.json +16 -0
- package/src/modules/workflows/i18n/en.json +16 -0
- package/src/modules/workflows/i18n/es.json +16 -0
- package/src/modules/workflows/i18n/pl.json +16 -0
- package/src/modules/workflows/lib/code-registry.ts +65 -0
- package/src/modules/workflows/lib/find-definition.ts +100 -0
- package/src/modules/workflows/lib/seeds.ts +4 -3
- package/src/modules/workflows/lib/start-validator.ts +2 -35
- package/src/modules/workflows/lib/workflow-executor.ts +2 -39
- package/src/modules/workflows/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/workflows/migrations/Migration20260414120000.ts +13 -0
- package/src/modules/workflows/migrations/Migration20260428102318.ts +57 -0
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +1 -1
- package/src/modules/workflows/workflows.ts +278 -0
- package/dist/modules/workflows/examples/checkout-demo-definition.json +0 -486
- package/dist/modules/workflows/examples/order-approval-definition.json +0 -257
- package/dist/modules/workflows/examples/simple-approval-definition.json +0 -134
- package/src/modules/workflows/examples/checkout-demo-definition.json +0 -486
- package/src/modules/workflows/examples/order-approval-definition.json +0 -257
- package/src/modules/workflows/examples/simple-approval-definition.json +0 -134
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Code Workflow Registry
|
|
3
|
+
*
|
|
4
|
+
* Stores code-based workflow definitions populated at bootstrap from
|
|
5
|
+
* the generated workflows.generated.ts. Definitions live purely in memory —
|
|
6
|
+
* no DB row is created until a user customizes the definition.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CodeWorkflowDefinition } from '@open-mercato/shared/modules/workflows'
|
|
10
|
+
import { workflowDefinitionDataSchema } from '../data/validators'
|
|
11
|
+
|
|
12
|
+
const codeWorkflowRegistry = new Map<string, CodeWorkflowDefinition>()
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register code-based workflow definitions at bootstrap time.
|
|
16
|
+
* Validates each definition against the Zod schema and warns on failures.
|
|
17
|
+
*/
|
|
18
|
+
export function registerCodeWorkflows(workflows: CodeWorkflowDefinition[]): void {
|
|
19
|
+
for (const wf of workflows) {
|
|
20
|
+
const validation = workflowDefinitionDataSchema.safeParse(wf.definition)
|
|
21
|
+
if (!validation.success) {
|
|
22
|
+
console.warn(
|
|
23
|
+
`[workflows] Code workflow "${wf.workflowId}" failed validation:`,
|
|
24
|
+
validation.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; '),
|
|
25
|
+
)
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (codeWorkflowRegistry.has(wf.workflowId)) {
|
|
30
|
+
console.warn(
|
|
31
|
+
`[workflows] Duplicate code workflow ID "${wf.workflowId}" from module "${wf.moduleId}" — overwriting`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
codeWorkflowRegistry.set(wf.workflowId, wf)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a single code workflow definition by workflowId.
|
|
41
|
+
*/
|
|
42
|
+
export function getCodeWorkflow(workflowId: string): CodeWorkflowDefinition | undefined {
|
|
43
|
+
return codeWorkflowRegistry.get(workflowId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get all registered code workflow definitions.
|
|
48
|
+
*/
|
|
49
|
+
export function getAllCodeWorkflows(): CodeWorkflowDefinition[] {
|
|
50
|
+
return Array.from(codeWorkflowRegistry.values())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a workflowId is a code-based definition.
|
|
55
|
+
*/
|
|
56
|
+
export function isCodeWorkflow(workflowId: string): boolean {
|
|
57
|
+
return codeWorkflowRegistry.has(workflowId)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clear the registry (for testing).
|
|
62
|
+
*/
|
|
63
|
+
export function clearCodeWorkflowRegistry(): void {
|
|
64
|
+
codeWorkflowRegistry.clear()
|
|
65
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Workflow Definition Lookup
|
|
3
|
+
*
|
|
4
|
+
* Finds workflow definitions from the database first, falling back to the
|
|
5
|
+
* in-memory code workflow registry. This enables code-based definitions to
|
|
6
|
+
* be used seamlessly by the executor and validators.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash } from 'node:crypto'
|
|
10
|
+
import { EntityManager } from '@mikro-orm/core'
|
|
11
|
+
import { WorkflowDefinition, type WorkflowDefinitionData } from '../data/entities'
|
|
12
|
+
import { getCodeWorkflow } from './code-registry'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a deterministic UUID for a code workflow definition.
|
|
16
|
+
* Uses SHA-256 truncated to UUID v4 format so the same workflowId always
|
|
17
|
+
* produces the same UUID across restarts and horizontal replicas.
|
|
18
|
+
*/
|
|
19
|
+
export function codeWorkflowUuid(workflowId: string): string {
|
|
20
|
+
const hash = createHash('sha256').update(`code-workflow:${workflowId}`).digest('hex')
|
|
21
|
+
// Format as UUID v4 (set version nibble to 4, variant bits to 10xx)
|
|
22
|
+
return [
|
|
23
|
+
hash.slice(0, 8),
|
|
24
|
+
hash.slice(8, 12),
|
|
25
|
+
`4${hash.slice(13, 16)}`,
|
|
26
|
+
`${(parseInt(hash[16], 16) & 0x3 | 0x8).toString(16)}${hash.slice(17, 20)}`,
|
|
27
|
+
hash.slice(20, 32),
|
|
28
|
+
].join('-')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find a workflow definition by workflowId, checking the database first
|
|
33
|
+
* and falling back to the code registry.
|
|
34
|
+
*
|
|
35
|
+
* When a DB row exists with `codeWorkflowId` set, it means the user has
|
|
36
|
+
* customized the code definition — the DB version takes precedence.
|
|
37
|
+
*
|
|
38
|
+
* When no DB row exists but a code definition is registered, a virtual
|
|
39
|
+
* WorkflowDefinition-like object is constructed with a deterministic UUID.
|
|
40
|
+
*/
|
|
41
|
+
export async function findWorkflowDefinition(
|
|
42
|
+
em: EntityManager,
|
|
43
|
+
options: {
|
|
44
|
+
workflowId: string
|
|
45
|
+
version?: number
|
|
46
|
+
tenantId: string
|
|
47
|
+
organizationId: string
|
|
48
|
+
},
|
|
49
|
+
): Promise<WorkflowDefinition | null> {
|
|
50
|
+
const { workflowId, version, tenantId, organizationId } = options
|
|
51
|
+
|
|
52
|
+
// 1. Try database first
|
|
53
|
+
const where: Record<string, unknown> = {
|
|
54
|
+
workflowId,
|
|
55
|
+
tenantId,
|
|
56
|
+
organizationId,
|
|
57
|
+
deletedAt: null,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (version !== undefined) {
|
|
61
|
+
where.version = version
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (version === undefined) {
|
|
65
|
+
where.enabled = true
|
|
66
|
+
const dbDef = await em.findOne(WorkflowDefinition, where, {
|
|
67
|
+
orderBy: { version: 'DESC' },
|
|
68
|
+
})
|
|
69
|
+
if (dbDef) return dbDef
|
|
70
|
+
} else {
|
|
71
|
+
const dbDef = await em.findOne(WorkflowDefinition, where)
|
|
72
|
+
if (dbDef) return dbDef
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Fall back to code registry (version filter not applicable to code defs)
|
|
76
|
+
const codeDef = getCodeWorkflow(workflowId)
|
|
77
|
+
if (!codeDef) return null
|
|
78
|
+
|
|
79
|
+
// When no version was requested, mirror the DB branch's `enabled = true` filter
|
|
80
|
+
// so disabled code workflows aren't silently executable.
|
|
81
|
+
if (version === undefined && !codeDef.enabled) return null
|
|
82
|
+
|
|
83
|
+
// Construct a virtual WorkflowDefinition object (not persisted)
|
|
84
|
+
const virtual = new WorkflowDefinition()
|
|
85
|
+
virtual.id = codeWorkflowUuid(workflowId)
|
|
86
|
+
virtual.workflowId = codeDef.workflowId
|
|
87
|
+
virtual.workflowName = codeDef.workflowName
|
|
88
|
+
virtual.description = codeDef.description ?? null
|
|
89
|
+
virtual.version = codeDef.version
|
|
90
|
+
virtual.definition = codeDef.definition as WorkflowDefinitionData
|
|
91
|
+
virtual.metadata = codeDef.metadata ?? null
|
|
92
|
+
virtual.enabled = codeDef.enabled
|
|
93
|
+
virtual.codeWorkflowId = codeDef.workflowId
|
|
94
|
+
virtual.tenantId = tenantId
|
|
95
|
+
virtual.organizationId = organizationId
|
|
96
|
+
virtual.createdAt = new Date(0) // code defs have no creation date
|
|
97
|
+
virtual.updatedAt = new Date(0)
|
|
98
|
+
|
|
99
|
+
return virtual
|
|
100
|
+
}
|
|
@@ -190,10 +190,11 @@ async function seedGuardRules(
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
export async function seedExampleWorkflows(em: EntityManager, scope: WorkflowSeedScope): Promise<void> {
|
|
193
|
-
|
|
193
|
+
// workflows.checkout-demo and workflows.simple-approval are now code-defined
|
|
194
|
+
// (see packages/core/src/modules/workflows/workflows.ts). Seeding DB rows for
|
|
195
|
+
// them would shadow the code definitions in the merge layer, so they are no
|
|
196
|
+
// longer seeded here. Existing tenants are migrated via Migration20260428102318.
|
|
194
197
|
await seedGuardRules(em, scope, 'guard-rules-example.json')
|
|
195
198
|
await seedWorkflowDefinition(em, scope, 'sales-pipeline-definition.json')
|
|
196
|
-
await seedWorkflowDefinition(em, scope, 'simple-approval-definition.json')
|
|
197
199
|
await seedGuardRules(em, scope, 'order-approval-guard-rules.json')
|
|
198
|
-
await seedWorkflowDefinition(em, scope, 'order-approval-definition.json')
|
|
199
200
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { EntityManager } from '@mikro-orm/core'
|
|
10
10
|
import { WorkflowDefinition } from '../data/entities'
|
|
11
11
|
import * as ruleEngine from '../../business_rules/lib/rule-engine'
|
|
12
|
+
import { findWorkflowDefinition } from './find-definition'
|
|
12
13
|
import type { StartPreCondition } from '../data/validators'
|
|
13
14
|
|
|
14
15
|
// ============================================================================
|
|
@@ -258,38 +259,4 @@ function getLocalizedMessage(
|
|
|
258
259
|
return defaultMessage
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
* Find workflow definition by ID and optional version
|
|
263
|
-
*/
|
|
264
|
-
async function findWorkflowDefinition(
|
|
265
|
-
em: EntityManager,
|
|
266
|
-
options: {
|
|
267
|
-
workflowId: string
|
|
268
|
-
version?: number
|
|
269
|
-
tenantId: string
|
|
270
|
-
organizationId: string
|
|
271
|
-
}
|
|
272
|
-
): Promise<WorkflowDefinition | null> {
|
|
273
|
-
const { workflowId, version, tenantId, organizationId } = options
|
|
274
|
-
|
|
275
|
-
const where: any = {
|
|
276
|
-
workflowId,
|
|
277
|
-
tenantId,
|
|
278
|
-
organizationId,
|
|
279
|
-
deletedAt: null,
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (version !== undefined) {
|
|
283
|
-
where.version = version
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// If no version specified, get latest enabled version
|
|
287
|
-
if (version === undefined) {
|
|
288
|
-
where.enabled = true
|
|
289
|
-
return em.findOne(WorkflowDefinition, where, {
|
|
290
|
-
orderBy: { version: 'DESC' },
|
|
291
|
-
})
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return em.findOne(WorkflowDefinition, where)
|
|
295
|
-
}
|
|
262
|
+
// findWorkflowDefinition is imported from ./find-definition
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type WorkflowInstanceStatus,
|
|
20
20
|
} from '../data/entities'
|
|
21
21
|
import { compensateWorkflow } from './compensation-handler'
|
|
22
|
+
import { findWorkflowDefinition } from './find-definition'
|
|
22
23
|
|
|
23
24
|
// ============================================================================
|
|
24
25
|
// Types and Interfaces
|
|
@@ -903,45 +904,7 @@ export async function updateWorkflowContext(
|
|
|
903
904
|
await em.flush()
|
|
904
905
|
}
|
|
905
906
|
|
|
906
|
-
|
|
907
|
-
* Find workflow definition by ID and optional version
|
|
908
|
-
*
|
|
909
|
-
* @param em - Entity manager
|
|
910
|
-
* @param options - Search options
|
|
911
|
-
* @returns Workflow definition or null
|
|
912
|
-
*/
|
|
913
|
-
async function findWorkflowDefinition(
|
|
914
|
-
em: EntityManager,
|
|
915
|
-
options: {
|
|
916
|
-
workflowId: string
|
|
917
|
-
version?: number
|
|
918
|
-
tenantId: string
|
|
919
|
-
organizationId: string
|
|
920
|
-
}
|
|
921
|
-
): Promise<WorkflowDefinition | null> {
|
|
922
|
-
const { workflowId, version, tenantId, organizationId } = options
|
|
923
|
-
|
|
924
|
-
const where: any = {
|
|
925
|
-
workflowId,
|
|
926
|
-
tenantId,
|
|
927
|
-
organizationId,
|
|
928
|
-
deletedAt: null,
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
if (version !== undefined) {
|
|
932
|
-
where.version = version
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// If no version specified, get latest enabled version
|
|
936
|
-
if (version === undefined) {
|
|
937
|
-
where.enabled = true
|
|
938
|
-
return em.findOne(WorkflowDefinition, where, {
|
|
939
|
-
orderBy: { version: 'DESC' },
|
|
940
|
-
})
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
return em.findOne(WorkflowDefinition, where)
|
|
944
|
-
}
|
|
907
|
+
// findWorkflowDefinition is imported from ./find-definition
|
|
945
908
|
|
|
946
909
|
/**
|
|
947
910
|
* Log workflow event to event sourcing table
|
|
@@ -518,6 +518,16 @@
|
|
|
518
518
|
"length": 100,
|
|
519
519
|
"mappedType": "string"
|
|
520
520
|
},
|
|
521
|
+
"code_workflow_id": {
|
|
522
|
+
"name": "code_workflow_id",
|
|
523
|
+
"type": "varchar(100)",
|
|
524
|
+
"unsigned": false,
|
|
525
|
+
"autoincrement": false,
|
|
526
|
+
"primary": false,
|
|
527
|
+
"nullable": true,
|
|
528
|
+
"length": 100,
|
|
529
|
+
"mappedType": "string"
|
|
530
|
+
},
|
|
521
531
|
"workflow_name": {
|
|
522
532
|
"name": "workflow_name",
|
|
523
533
|
"type": "varchar(255)",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260414120000 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`alter table "workflow_definitions" add column if not exists "code_workflow_id" varchar(100) null;`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
override async down(): Promise<void> {
|
|
10
|
+
this.addSql(`alter table "workflow_definitions" drop column if exists "code_workflow_id";`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
const RENAMES: Array<{ from: string; to: string }> = [
|
|
4
|
+
{ from: 'checkout_simple_v1', to: 'workflows.checkout-demo' },
|
|
5
|
+
{ from: 'sales_order_approval_v1', to: 'sales.order-approval' },
|
|
6
|
+
{ from: 'simple_approval_v1', to: 'workflows.simple-approval' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export class Migration20260428102318 extends Migration {
|
|
10
|
+
|
|
11
|
+
override async up(): Promise<void> {
|
|
12
|
+
for (const { from, to } of RENAMES) {
|
|
13
|
+
this.addSql(
|
|
14
|
+
`update "workflow_definitions" ` +
|
|
15
|
+
`set "workflow_id" = '${to}' ` +
|
|
16
|
+
`where "workflow_id" = '${from}' ` +
|
|
17
|
+
`and not exists (` +
|
|
18
|
+
`select 1 from "workflow_definitions" d2 ` +
|
|
19
|
+
`where d2."workflow_id" = '${to}' and d2."tenant_id" = "workflow_definitions"."tenant_id"` +
|
|
20
|
+
`);`
|
|
21
|
+
);
|
|
22
|
+
this.addSql(
|
|
23
|
+
`update "workflow_instances" ` +
|
|
24
|
+
`set "workflow_id" = '${to}' ` +
|
|
25
|
+
`where "workflow_id" = '${from}' ` +
|
|
26
|
+
`and exists (` +
|
|
27
|
+
`select 1 from "workflow_definitions" d ` +
|
|
28
|
+
`where d."id" = "workflow_instances"."definition_id" and d."workflow_id" = '${to}'` +
|
|
29
|
+
`);`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override async down(): Promise<void> {
|
|
35
|
+
for (const { from, to } of RENAMES) {
|
|
36
|
+
this.addSql(
|
|
37
|
+
`update "workflow_definitions" ` +
|
|
38
|
+
`set "workflow_id" = '${from}' ` +
|
|
39
|
+
`where "workflow_id" = '${to}' ` +
|
|
40
|
+
`and not exists (` +
|
|
41
|
+
`select 1 from "workflow_definitions" d2 ` +
|
|
42
|
+
`where d2."workflow_id" = '${from}' and d2."tenant_id" = "workflow_definitions"."tenant_id"` +
|
|
43
|
+
`);`
|
|
44
|
+
);
|
|
45
|
+
this.addSql(
|
|
46
|
+
`update "workflow_instances" ` +
|
|
47
|
+
`set "workflow_id" = '${from}' ` +
|
|
48
|
+
`where "workflow_id" = '${to}' ` +
|
|
49
|
+
`and exists (` +
|
|
50
|
+
`select 1 from "workflow_definitions" d ` +
|
|
51
|
+
`where d."id" = "workflow_instances"."definition_id" and d."workflow_id" = '${from}'` +
|
|
52
|
+
`);`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
@@ -42,7 +42,7 @@ type DictionaryEntry = {
|
|
|
42
42
|
icon?: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const WORKFLOW_ID = '
|
|
45
|
+
const WORKFLOW_ID = 'sales.order-approval'
|
|
46
46
|
|
|
47
47
|
export default function OrderApprovalWidget({ data }: InjectionWidgetComponentProps<unknown, OrderRecord>) {
|
|
48
48
|
const t = useT()
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflows Module — Code-Based Workflow Definitions
|
|
3
|
+
*
|
|
4
|
+
* Demo workflows shipped with the workflows module.
|
|
5
|
+
* These are auto-discovered by the generator and loaded into the in-memory registry.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineWorkflow, createWorkflowsModuleConfig } from '@open-mercato/shared/modules/workflows'
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Simple Approval Workflow
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
const simpleApproval = defineWorkflow({
|
|
15
|
+
workflowId: 'workflows.simple-approval',
|
|
16
|
+
workflowName: 'Simple Approval Workflow',
|
|
17
|
+
description: 'Basic three-step approval workflow: Start → Approval → End',
|
|
18
|
+
metadata: { category: 'Testing', tags: ['test', 'approval', 'simple'], icon: 'check-circle' },
|
|
19
|
+
steps: [
|
|
20
|
+
{ stepId: 'start', stepName: 'Start', stepType: 'START', description: 'Initialize workflow' },
|
|
21
|
+
{
|
|
22
|
+
stepId: 'approval',
|
|
23
|
+
stepName: 'Pending Approval',
|
|
24
|
+
stepType: 'USER_TASK',
|
|
25
|
+
description: 'Approve or reject request',
|
|
26
|
+
userTaskConfig: { assignedTo: 'approver', slaDuration: 'PT1H' },
|
|
27
|
+
},
|
|
28
|
+
{ stepId: 'end', stepName: 'Complete', stepType: 'END', description: 'Workflow complete' },
|
|
29
|
+
] as const,
|
|
30
|
+
transitions: [
|
|
31
|
+
{
|
|
32
|
+
transitionId: 'start_to_approval',
|
|
33
|
+
transitionName: 'Submit for Approval',
|
|
34
|
+
fromStepId: 'start',
|
|
35
|
+
toStepId: 'approval',
|
|
36
|
+
trigger: 'auto',
|
|
37
|
+
priority: 100,
|
|
38
|
+
activities: [
|
|
39
|
+
{
|
|
40
|
+
activityId: 'notify_approver',
|
|
41
|
+
activityName: 'Notify Approver',
|
|
42
|
+
activityType: 'SEND_EMAIL',
|
|
43
|
+
config: {
|
|
44
|
+
to: '{{context.approver.email}}',
|
|
45
|
+
subject: 'Approval Required: {{context.request.title}}',
|
|
46
|
+
template: 'approval_request',
|
|
47
|
+
data: {
|
|
48
|
+
requestId: '{{context.request.id}}',
|
|
49
|
+
title: '{{context.request.title}}',
|
|
50
|
+
requester: '{{context.requester.name}}',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
timeout: 'PT10S',
|
|
54
|
+
async: true,
|
|
55
|
+
retryPolicy: { maxAttempts: 3, initialIntervalMs: 2000, backoffCoefficient: 2, maxIntervalMs: 20000 },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
activityId: 'emit_approval_requested',
|
|
59
|
+
activityName: 'Emit Approval Requested Event',
|
|
60
|
+
activityType: 'EMIT_EVENT',
|
|
61
|
+
config: {
|
|
62
|
+
eventType: 'approval.requested',
|
|
63
|
+
payload: { requestId: '{{context.request.id}}', workflowInstanceId: '{{workflow.instanceId}}' },
|
|
64
|
+
},
|
|
65
|
+
async: true,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
transitionId: 'approval_to_end',
|
|
71
|
+
transitionName: 'Approval Decision',
|
|
72
|
+
fromStepId: 'approval',
|
|
73
|
+
toStepId: 'end',
|
|
74
|
+
trigger: 'manual',
|
|
75
|
+
priority: 100,
|
|
76
|
+
activities: [
|
|
77
|
+
{
|
|
78
|
+
activityId: 'notify_requester',
|
|
79
|
+
activityName: 'Notify Requester of Decision',
|
|
80
|
+
activityType: 'SEND_EMAIL',
|
|
81
|
+
config: {
|
|
82
|
+
to: '{{context.requester.email}}',
|
|
83
|
+
subject: 'Approval Decision: {{context.request.title}}',
|
|
84
|
+
template: 'approval_decision',
|
|
85
|
+
data: {
|
|
86
|
+
requestId: '{{context.request.id}}',
|
|
87
|
+
title: '{{context.request.title}}',
|
|
88
|
+
decision: '{{task.result.approved}}',
|
|
89
|
+
comments: '{{task.result.comments}}',
|
|
90
|
+
approver: '{{task.completedBy}}',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
timeout: 'PT10S',
|
|
94
|
+
async: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
activityId: 'emit_approval_completed',
|
|
98
|
+
activityName: 'Emit Approval Completed Event',
|
|
99
|
+
activityType: 'EMIT_EVENT',
|
|
100
|
+
config: {
|
|
101
|
+
eventType: 'approval.completed',
|
|
102
|
+
payload: {
|
|
103
|
+
requestId: '{{context.request.id}}',
|
|
104
|
+
approved: '{{task.result.approved}}',
|
|
105
|
+
approvedBy: '{{task.completedBy}}',
|
|
106
|
+
timestamp: '{{now}}',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
async: true,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Checkout Demo Workflow
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
const checkoutDemo = defineWorkflow({
|
|
121
|
+
workflowId: 'workflows.checkout-demo',
|
|
122
|
+
workflowName: 'Checkout with Payment Webhook',
|
|
123
|
+
description: 'Realistic checkout workflow with signal-based payment confirmation',
|
|
124
|
+
metadata: { category: 'E-commerce', tags: ['checkout', 'demo', 'payments', 'signals'], icon: 'ShoppingCart' },
|
|
125
|
+
steps: [
|
|
126
|
+
{
|
|
127
|
+
stepId: 'start',
|
|
128
|
+
stepName: 'Start',
|
|
129
|
+
stepType: 'START',
|
|
130
|
+
description: 'Checkout process initiated',
|
|
131
|
+
preConditions: [{
|
|
132
|
+
ruleId: 'workflow_checkout_cart_not_empty',
|
|
133
|
+
required: true,
|
|
134
|
+
validationMessage: {
|
|
135
|
+
en: 'Your shopping cart is empty. Please add items before starting checkout.',
|
|
136
|
+
pl: 'Twój koszyk jest pusty. Dodaj produkty przed rozpoczęciem zakupu.',
|
|
137
|
+
},
|
|
138
|
+
}],
|
|
139
|
+
},
|
|
140
|
+
{ stepId: 'cart_validation', stepName: 'Cart Validation', stepType: 'AUTOMATED', description: 'Validate cart has items and inventory is available', timeout: 'PT10S' },
|
|
141
|
+
{
|
|
142
|
+
stepId: 'customer_info',
|
|
143
|
+
stepName: 'Customer Information',
|
|
144
|
+
stepType: 'USER_TASK',
|
|
145
|
+
description: 'Collect customer shipping and contact information',
|
|
146
|
+
userTaskConfig: {
|
|
147
|
+
formSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
fullName: { type: 'string', title: 'Full Name', description: "Customer's full legal name" },
|
|
151
|
+
email: { type: 'string', format: 'email', title: 'Email Address', description: 'Email for order confirmation' },
|
|
152
|
+
phone: { type: 'string', title: 'Phone Number', description: 'Contact number for delivery' },
|
|
153
|
+
comment: { type: 'string', title: 'Order Comment', maxLength: 500, description: 'Special instructions or notes' },
|
|
154
|
+
},
|
|
155
|
+
required: ['fullName', 'email'],
|
|
156
|
+
},
|
|
157
|
+
slaDuration: 'PT24H',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{ stepId: 'payment_initiation', stepName: 'Initiate Payment', stepType: 'AUTOMATED', description: 'Send payment request to payment provider', timeout: 'PT10S' },
|
|
161
|
+
{
|
|
162
|
+
stepId: 'wait_payment_confirmation',
|
|
163
|
+
stepName: 'Wait for Payment Confirmation',
|
|
164
|
+
stepType: 'WAIT_FOR_SIGNAL',
|
|
165
|
+
description: 'Waiting for payment provider webhook confirmation',
|
|
166
|
+
signalConfig: { signalName: 'payment_confirmed', timeout: 'PT5M' },
|
|
167
|
+
},
|
|
168
|
+
{ stepId: 'order_confirmation', stepName: 'Order Confirmation', stepType: 'AUTOMATED', description: 'Create order record and send confirmation', timeout: 'PT15S' },
|
|
169
|
+
{ stepId: 'end', stepName: 'Complete', stepType: 'END', description: 'Checkout completed successfully' },
|
|
170
|
+
] as const,
|
|
171
|
+
transitions: [
|
|
172
|
+
{
|
|
173
|
+
transitionId: 'start_to_cart',
|
|
174
|
+
transitionName: 'Initialize Checkout',
|
|
175
|
+
fromStepId: 'start',
|
|
176
|
+
toStepId: 'cart_validation',
|
|
177
|
+
trigger: 'auto',
|
|
178
|
+
priority: 100,
|
|
179
|
+
activities: [{
|
|
180
|
+
activityId: 'log_checkout_start',
|
|
181
|
+
activityName: 'Log Checkout Start',
|
|
182
|
+
activityType: 'EMIT_EVENT',
|
|
183
|
+
config: {
|
|
184
|
+
eventName: 'checkout.started',
|
|
185
|
+
payload: { customerId: '{{context.customerId}}', cartId: '{{context.cartId}}', currency: '{{context.currency}}', timestamp: '{{now}}' },
|
|
186
|
+
},
|
|
187
|
+
async: false,
|
|
188
|
+
}],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
transitionId: 'cart_to_customer_info',
|
|
192
|
+
transitionName: 'Collect Customer Information',
|
|
193
|
+
fromStepId: 'cart_validation',
|
|
194
|
+
toStepId: 'customer_info',
|
|
195
|
+
trigger: 'auto',
|
|
196
|
+
priority: 100,
|
|
197
|
+
preConditions: [{ ruleId: 'workflow_checkout_cart_not_empty', required: true }],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
transitionId: 'customer_info_to_payment',
|
|
201
|
+
transitionName: 'Initiate Payment',
|
|
202
|
+
fromStepId: 'customer_info',
|
|
203
|
+
toStepId: 'payment_initiation',
|
|
204
|
+
trigger: 'auto',
|
|
205
|
+
priority: 100,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
transitionId: 'payment_to_wait_confirmation',
|
|
209
|
+
transitionName: 'Wait for Confirmation',
|
|
210
|
+
fromStepId: 'payment_initiation',
|
|
211
|
+
toStepId: 'wait_payment_confirmation',
|
|
212
|
+
trigger: 'auto',
|
|
213
|
+
priority: 100,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
transitionId: 'confirmation_to_order',
|
|
217
|
+
transitionName: 'Payment Confirmed',
|
|
218
|
+
fromStepId: 'wait_payment_confirmation',
|
|
219
|
+
toStepId: 'order_confirmation',
|
|
220
|
+
trigger: 'auto',
|
|
221
|
+
priority: 100,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
transitionId: 'confirmation_to_end',
|
|
225
|
+
transitionName: 'Finalize Order',
|
|
226
|
+
fromStepId: 'order_confirmation',
|
|
227
|
+
toStepId: 'end',
|
|
228
|
+
trigger: 'auto',
|
|
229
|
+
priority: 100,
|
|
230
|
+
activities: [
|
|
231
|
+
{
|
|
232
|
+
activityId: 'create_order',
|
|
233
|
+
activityName: 'Create Order Record',
|
|
234
|
+
activityType: 'CALL_API',
|
|
235
|
+
config: {
|
|
236
|
+
endpoint: '/api/sales/orders',
|
|
237
|
+
method: 'POST',
|
|
238
|
+
body: {
|
|
239
|
+
customerEntityId: '{{context.customer.id}}',
|
|
240
|
+
currencyCode: '{{context.cart.currency}}',
|
|
241
|
+
placedAt: '{{now}}',
|
|
242
|
+
grandTotalGrossAmount: '{{context.cart.total}}',
|
|
243
|
+
},
|
|
244
|
+
validateTenantMatch: true,
|
|
245
|
+
},
|
|
246
|
+
timeout: 'PT10S',
|
|
247
|
+
async: false,
|
|
248
|
+
retryPolicy: { maxAttempts: 3, initialIntervalMs: 2000, backoffCoefficient: 2, maxIntervalMs: 10000 },
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
activityId: 'send_confirmation_email',
|
|
252
|
+
activityName: 'Send Order Confirmation Email',
|
|
253
|
+
activityType: 'SEND_EMAIL',
|
|
254
|
+
config: {
|
|
255
|
+
to: '{{context.customer.email}}',
|
|
256
|
+
subject: 'Order Confirmation',
|
|
257
|
+
template: 'order_confirmation',
|
|
258
|
+
data: { customerName: '{{context.customer.name}}', total: '{{context.cart.total}}' },
|
|
259
|
+
},
|
|
260
|
+
timeout: 'PT15S',
|
|
261
|
+
async: true,
|
|
262
|
+
retryPolicy: { maxAttempts: 3, initialIntervalMs: 2000, backoffCoefficient: 2, maxIntervalMs: 20000 },
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Module Export
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
export const workflowsConfig = createWorkflowsModuleConfig({
|
|
274
|
+
moduleId: 'workflows',
|
|
275
|
+
workflows: [simpleApproval, checkoutDemo],
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
export default workflowsConfig
|