@open-mercato/core 0.4.11-develop.1347.c693e6dfee → 0.4.11-develop.1354.54d40d164a
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/package.json +3 -3
- package/src/modules/auth/agentic/standalone-guide.md +101 -0
- package/src/modules/catalog/agentic/standalone-guide.md +79 -0
- package/src/modules/currencies/agentic/standalone-guide.md +43 -0
- package/src/modules/customer_accounts/agentic/standalone-guide.md +124 -0
- package/src/modules/customers/agentic/standalone-guide.md +138 -0
- package/src/modules/data_sync/agentic/standalone-guide.md +107 -0
- package/src/modules/integrations/agentic/standalone-guide.md +113 -0
- package/src/modules/sales/agentic/standalone-guide.md +84 -0
- package/src/modules/workflows/agentic/standalone-guide.md +152 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.11-develop.
|
|
3
|
+
"version": "0.4.11-develop.1354.54d40d164a",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -230,10 +230,10 @@
|
|
|
230
230
|
"ts-pattern": "^5.0.0"
|
|
231
231
|
},
|
|
232
232
|
"peerDependencies": {
|
|
233
|
-
"@open-mercato/shared": "0.4.11-develop.
|
|
233
|
+
"@open-mercato/shared": "0.4.11-develop.1354.54d40d164a"
|
|
234
234
|
},
|
|
235
235
|
"devDependencies": {
|
|
236
|
-
"@open-mercato/shared": "0.4.11-develop.
|
|
236
|
+
"@open-mercato/shared": "0.4.11-develop.1354.54d40d164a",
|
|
237
237
|
"@testing-library/dom": "^10.4.1",
|
|
238
238
|
"@testing-library/jest-dom": "^6.9.1",
|
|
239
239
|
"@testing-library/react": "^16.3.1",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Auth Module — Standalone App Guide
|
|
2
|
+
|
|
3
|
+
The auth module handles staff authentication, authorization, users, roles, and RBAC. For customer portal authentication, see the `customer_accounts` module guide.
|
|
4
|
+
|
|
5
|
+
## RBAC Implementation
|
|
6
|
+
|
|
7
|
+
### Two-Layer Model
|
|
8
|
+
|
|
9
|
+
1. **Role ACLs** — features assigned to roles (admin, employee, etc.)
|
|
10
|
+
2. **User ACLs** — per-user overrides (additional features or restrictions)
|
|
11
|
+
|
|
12
|
+
Effective permissions = Role features + User-specific features.
|
|
13
|
+
|
|
14
|
+
### Declaring Features
|
|
15
|
+
|
|
16
|
+
Every module MUST declare features in `acl.ts` and wire them in `setup.ts`:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// src/modules/<your_module>/acl.ts
|
|
20
|
+
export const features = [
|
|
21
|
+
'your_module.view',
|
|
22
|
+
'your_module.create',
|
|
23
|
+
'your_module.update',
|
|
24
|
+
'your_module.delete',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
// src/modules/<your_module>/setup.ts
|
|
28
|
+
import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
|
|
29
|
+
|
|
30
|
+
export const setup: ModuleSetupConfig = {
|
|
31
|
+
defaultRoleFeatures: {
|
|
32
|
+
superadmin: ['your_module.*'],
|
|
33
|
+
admin: ['your_module.*'],
|
|
34
|
+
user: ['your_module.view'],
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Feature Naming Convention
|
|
40
|
+
|
|
41
|
+
Features follow the `<module>.<action>` pattern (e.g., `users.view`, `users.edit`).
|
|
42
|
+
|
|
43
|
+
### Declarative Guards
|
|
44
|
+
|
|
45
|
+
Prefer declarative guards in page and API metadata:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
export const metadata = {
|
|
49
|
+
requireAuth: true,
|
|
50
|
+
requireRoles: ['admin'],
|
|
51
|
+
requireFeatures: ['users.manage'],
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Server-Side Checks
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const rbacService = container.resolve('rbacService')
|
|
59
|
+
const hasAccess = await rbacService.userHasAllFeatures(
|
|
60
|
+
userId,
|
|
61
|
+
['your_module.view'],
|
|
62
|
+
{ tenantId, organizationId }
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Wildcards
|
|
67
|
+
|
|
68
|
+
Wildcards are first-class ACL grants: `module.*` and `*` satisfy matching concrete features. When your code inspects raw granted feature arrays (instead of calling `rbacService`), use the shared wildcard-aware matchers (`matchFeature`, `hasFeature`, `hasAllFeatures`) — never use `includes(...)`.
|
|
69
|
+
|
|
70
|
+
### Special Flags
|
|
71
|
+
|
|
72
|
+
- `isSuperAdmin` — bypasses all feature checks
|
|
73
|
+
- Organization visibility list — restricts which organizations a user can access
|
|
74
|
+
|
|
75
|
+
## Security Rules
|
|
76
|
+
|
|
77
|
+
- Hash passwords with `bcryptjs` (cost >= 10)
|
|
78
|
+
- Never log credentials
|
|
79
|
+
- Return minimal auth error messages — never reveal whether an email exists
|
|
80
|
+
- Use `findWithDecryption` / `findOneWithDecryption` for user queries
|
|
81
|
+
|
|
82
|
+
## Authentication Flow
|
|
83
|
+
|
|
84
|
+
1. User submits credentials via `POST /api/auth/session`
|
|
85
|
+
2. Password verified with bcryptjs
|
|
86
|
+
3. JWT session token issued
|
|
87
|
+
4. Session attached to requests via middleware
|
|
88
|
+
|
|
89
|
+
## Subscribing to Auth Events
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
export const metadata = {
|
|
93
|
+
event: 'auth.user.created',
|
|
94
|
+
persistent: true,
|
|
95
|
+
id: 'your-module-user-created',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default async function handler(payload, ctx) {
|
|
99
|
+
// React to new user registration
|
|
100
|
+
}
|
|
101
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Catalog Module — Standalone App Guide
|
|
2
|
+
|
|
3
|
+
Use the catalog module for products, categories, pricing, variants, and offers.
|
|
4
|
+
|
|
5
|
+
## Pricing System
|
|
6
|
+
|
|
7
|
+
Never reimplement pricing logic. Use the catalog pricing service via DI:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const pricingService = container.resolve('catalogPricingService')
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `selectBestPrice` — finds the best price for a given context (customer, channel, quantity)
|
|
14
|
+
- `resolvePriceVariantId` — resolves variant-level prices
|
|
15
|
+
- Register custom pricing resolvers with priority (higher = checked first):
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { registerCatalogPricingResolver } from '@open-mercato/core/modules/catalog/lib/pricing'
|
|
19
|
+
registerCatalogPricingResolver(myResolver, { priority: 10 })
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Price layers compose in order: base price → channel override → customer-specific → promotional.
|
|
23
|
+
|
|
24
|
+
The pipeline emits `catalog.pricing.resolve.before` and `catalog.pricing.resolve.after` events that your module can subscribe to.
|
|
25
|
+
|
|
26
|
+
## Data Model
|
|
27
|
+
|
|
28
|
+
| Entity | Purpose | Key Constraints |
|
|
29
|
+
|--------|---------|----------------|
|
|
30
|
+
| **Products** | Core items with media and descriptions | MUST have at least a name |
|
|
31
|
+
| **Categories** | Hierarchical product grouping | No circular parent-child references |
|
|
32
|
+
| **Variants** | Product variations (size, color) | MUST reference valid option schemas |
|
|
33
|
+
| **Prices** | Multi-tier with channel scoping | Use `selectBestPrice` for resolution |
|
|
34
|
+
| **Offers** | Time-limited promotions | MUST have valid date ranges |
|
|
35
|
+
| **Option Schemas** | Variant option type definitions | Cannot delete while variants reference them |
|
|
36
|
+
|
|
37
|
+
## Subscribing to Catalog Events
|
|
38
|
+
|
|
39
|
+
React to product lifecycle events in your module:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// src/modules/<your_module>/subscribers/product-updated.ts
|
|
43
|
+
export const metadata = {
|
|
44
|
+
event: 'catalog.product.updated',
|
|
45
|
+
persistent: true,
|
|
46
|
+
id: 'your-module-product-updated',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default async function handler(payload, ctx) {
|
|
50
|
+
// payload.resourceId = product ID
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Key events:
|
|
55
|
+
- `catalog.product.created` / `updated` / `deleted`
|
|
56
|
+
- `catalog.pricing.resolve.before` / `after` (excluded from workflow triggers)
|
|
57
|
+
|
|
58
|
+
## Extending Catalog UI
|
|
59
|
+
|
|
60
|
+
Use widget injection to add your module's UI into catalog pages:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// src/modules/<your_module>/widgets/injection-table.ts
|
|
64
|
+
export const widgetInjections = {
|
|
65
|
+
'crud-form:catalog.catalog_product:fields': {
|
|
66
|
+
widgetId: 'your-module-product-fields',
|
|
67
|
+
priority: 100,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Common injection spots:
|
|
73
|
+
- `crud-form:catalog.catalog_product:fields` — product edit form
|
|
74
|
+
- `data-table:catalog.products:columns` — product list columns
|
|
75
|
+
- `data-table:catalog.products:row-actions` — product row actions
|
|
76
|
+
|
|
77
|
+
## Using Catalog in Sales
|
|
78
|
+
|
|
79
|
+
When building sales-related features, use the catalog pricing service to resolve prices rather than reading price entities directly. This ensures channel scoping, customer-specific pricing, and promotional offers are applied correctly.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Currencies Module — Standalone App Guide
|
|
2
|
+
|
|
3
|
+
Use the currencies module for multi-currency support, exchange rates, and currency conversion.
|
|
4
|
+
|
|
5
|
+
## Key Rules
|
|
6
|
+
|
|
7
|
+
1. **Store amounts with 4 decimal precision** — never truncate to 2 decimals internally
|
|
8
|
+
2. **Use date-based exchange rates** — always resolve rates for the transaction date, not the "current" rate
|
|
9
|
+
3. **Record both currencies** — dual recording (transaction currency + base currency) is mandatory for reporting
|
|
10
|
+
4. **Calculate realized gains/losses** on payment: `(payment rate - invoice rate) × foreign amount`
|
|
11
|
+
5. **Never hard-delete exchange rates** — they are historical reference data
|
|
12
|
+
|
|
13
|
+
## Multi-Currency Transaction Pattern
|
|
14
|
+
|
|
15
|
+
When processing multi-currency transactions (e.g., sales invoice in EUR with USD base):
|
|
16
|
+
|
|
17
|
+
1. Retrieve the exchange rate for the transaction date
|
|
18
|
+
2. Generate the document in the transaction currency
|
|
19
|
+
3. Calculate the base currency equivalent: `foreign amount × rate`
|
|
20
|
+
4. Store both amounts on the document
|
|
21
|
+
5. On payment: calculate realized gain/loss from rate difference
|
|
22
|
+
6. Report in both transaction and base currencies
|
|
23
|
+
|
|
24
|
+
## Data Model
|
|
25
|
+
|
|
26
|
+
| Entity | Table | Purpose |
|
|
27
|
+
|--------|-------|---------|
|
|
28
|
+
| **Currency** | `currency` | Currency master data (code, name, symbol) |
|
|
29
|
+
| **Exchange Rate** | `exchange_rate` | Daily exchange rates per currency pair |
|
|
30
|
+
|
|
31
|
+
## Adding a New Currency
|
|
32
|
+
|
|
33
|
+
1. Add the currency record via the admin UI or `seedDefaults` hook in your `setup.ts`
|
|
34
|
+
2. Ensure exchange rates exist for the currency pair at required dates
|
|
35
|
+
3. Verify all sales/pricing logic resolves the new currency correctly
|
|
36
|
+
|
|
37
|
+
## Using Currencies in Your Module
|
|
38
|
+
|
|
39
|
+
When your module deals with monetary amounts:
|
|
40
|
+
- Store the currency code alongside the amount
|
|
41
|
+
- Reference the currencies module for exchange rate lookups
|
|
42
|
+
- Use the transaction date for rate resolution, not the current date
|
|
43
|
+
- Store both foreign and base amounts for reporting
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Customer Accounts Module — Standalone App Guide
|
|
2
|
+
|
|
3
|
+
Customer-facing identity and portal authentication. This module manages customer user accounts, sessions, roles, and the authentication flow for the customer portal. It is separate from the staff `auth` module.
|
|
4
|
+
|
|
5
|
+
## Portal Authentication
|
|
6
|
+
|
|
7
|
+
### Login Flow
|
|
8
|
+
1. Customer submits credentials via `POST /api/login`
|
|
9
|
+
2. Password verified with bcryptjs, lockout checked (5 attempts → 15 min lock)
|
|
10
|
+
3. JWT issued with customer claims (`type: 'customer'`, features, CRM links)
|
|
11
|
+
4. Two cookies set: `customer_auth_token` (JWT, 8h) + `customer_session_token` (raw, 30d)
|
|
12
|
+
|
|
13
|
+
### Other Auth Methods
|
|
14
|
+
- **Signup**: `POST /api/signup` — self-registration with email verification
|
|
15
|
+
- **Magic Link**: `POST /api/magic-link/request` + `/verify` — passwordless login (15 min TTL)
|
|
16
|
+
- **Password Reset**: `POST /api/password/reset-request` + `/reset-confirm` (60 min TTL)
|
|
17
|
+
- **Invitation**: Admin invites user → `POST /api/invitations/accept` (72h TTL)
|
|
18
|
+
|
|
19
|
+
## Customer RBAC
|
|
20
|
+
|
|
21
|
+
### Two-Layer Model (mirrors staff RBAC)
|
|
22
|
+
1. **Role ACLs** — features assigned to roles
|
|
23
|
+
2. **User ACLs** — per-user overrides (takes precedence if present)
|
|
24
|
+
|
|
25
|
+
### Default Roles (seeded on tenant creation)
|
|
26
|
+
| Role | Features | Portal Admin |
|
|
27
|
+
|------|----------|-------------|
|
|
28
|
+
| Portal Admin | `portal.*` | Yes |
|
|
29
|
+
| Buyer | Orders, quotes, catalog, account | No |
|
|
30
|
+
| Viewer | Read-only orders, invoices, catalog | No |
|
|
31
|
+
|
|
32
|
+
### Feature Convention
|
|
33
|
+
Portal features use `portal.<area>.<action>` naming (e.g., `portal.orders.view`, `portal.catalog.view`).
|
|
34
|
+
|
|
35
|
+
### Cross-Module Feature Merging
|
|
36
|
+
Your module can declare `defaultCustomerRoleFeatures` in `setup.ts`. During tenant setup, these are merged into the corresponding customer role ACLs:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// src/modules/<your_module>/setup.ts
|
|
40
|
+
export const setup: ModuleSetupConfig = {
|
|
41
|
+
defaultCustomerRoleFeatures: {
|
|
42
|
+
portal_admin: ['portal.your_feature.*'],
|
|
43
|
+
buyer: ['portal.your_feature.view'],
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Using Customer Auth in Your Module
|
|
49
|
+
|
|
50
|
+
### Server Components (pages)
|
|
51
|
+
```typescript
|
|
52
|
+
import { getCustomerAuthFromCookies } from '@open-mercato/core/modules/customer_accounts/lib/customerAuthServer'
|
|
53
|
+
|
|
54
|
+
const auth = await getCustomerAuthFromCookies()
|
|
55
|
+
if (!auth) redirect('/login')
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### API Routes
|
|
59
|
+
```typescript
|
|
60
|
+
import { requireCustomerAuth, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'
|
|
61
|
+
|
|
62
|
+
// In your API handler:
|
|
63
|
+
const auth = requireCustomerAuth(request) // throws 401 if not authenticated
|
|
64
|
+
requireCustomerFeature(auth, ['portal.orders.view']) // throws 403 if missing
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### RBAC Service
|
|
68
|
+
```typescript
|
|
69
|
+
const rbacService = container.resolve('customerRbacService')
|
|
70
|
+
const hasAccess = await rbacService.userHasAllFeatures(
|
|
71
|
+
userId, ['portal.orders.view'], { tenantId, organizationId }
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Portal Page Guards
|
|
76
|
+
|
|
77
|
+
Use declarative metadata for portal pages:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export const metadata = {
|
|
81
|
+
requireCustomerAuth: true,
|
|
82
|
+
requireCustomerFeatures: ['portal.orders.view'],
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Subscribing to Customer Events
|
|
87
|
+
|
|
88
|
+
| Event | When |
|
|
89
|
+
|-------|------|
|
|
90
|
+
| `customer_accounts.user.created` | New customer signup |
|
|
91
|
+
| `customer_accounts.user.updated` | Profile updated |
|
|
92
|
+
| `customer_accounts.user.locked` | Account locked after failed logins |
|
|
93
|
+
| `customer_accounts.login.success` | Successful login |
|
|
94
|
+
| `customer_accounts.invitation.accepted` | Invitation accepted |
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
export const metadata = {
|
|
98
|
+
event: 'customer_accounts.user.created',
|
|
99
|
+
persistent: true,
|
|
100
|
+
id: 'your-module-customer-signup',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default async function handler(payload, ctx) {
|
|
104
|
+
// React to customer signup — e.g., create default preferences
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## CRM Auto-Linking
|
|
109
|
+
|
|
110
|
+
When a customer signs up, the module automatically searches for a matching CRM person by email and links them (`personEntityId`). The reverse also works — creating a CRM person auto-links to an existing customer user.
|
|
111
|
+
|
|
112
|
+
## Widget Injection Spots
|
|
113
|
+
|
|
114
|
+
| Spot | Widget | Purpose |
|
|
115
|
+
|------|--------|---------|
|
|
116
|
+
| `crud-form:customers:customer_person_profile:fields` | Account status | Shows portal account status on CRM person detail |
|
|
117
|
+
| `crud-form:customers:customer_company_profile:fields` | Company users | Shows portal users linked to a CRM company |
|
|
118
|
+
|
|
119
|
+
## Security Notes
|
|
120
|
+
|
|
121
|
+
- All public endpoints are rate-limited (per-email + per-IP)
|
|
122
|
+
- Tokens stored as SHA-256 hashes — raw tokens never persisted
|
|
123
|
+
- Emails use deterministic hash for lookups (`hashForLookup`)
|
|
124
|
+
- Error messages never confirm whether an email is registered
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Customers Module — Reference CRUD Patterns
|
|
2
|
+
|
|
3
|
+
This is the **reference CRUD module**. When building new modules in your standalone app, follow these patterns.
|
|
4
|
+
|
|
5
|
+
## CRUD API Pattern
|
|
6
|
+
|
|
7
|
+
Use `makeCrudRoute` with `indexer: { entityType }` for query index coverage:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// src/modules/<your_module>/api/get/<entities>.ts
|
|
11
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/make-crud-route'
|
|
12
|
+
import { YourEntity } from '../../entities/YourEntity'
|
|
13
|
+
|
|
14
|
+
const handler = makeCrudRoute({
|
|
15
|
+
entity: YourEntity,
|
|
16
|
+
entityId: 'your_module.your_entity',
|
|
17
|
+
operations: ['list', 'detail'],
|
|
18
|
+
indexer: { entityType: 'your_module.your_entity' },
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export default handler
|
|
22
|
+
export const openApi = { summary: 'List and retrieve entities', tags: ['Your Module'] }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Key points:
|
|
26
|
+
- Always set `indexer: { entityType }` — keeps custom entities indexed
|
|
27
|
+
- Wire custom field helpers for create/update if your module supports custom fields
|
|
28
|
+
- Export `openApi` on every API route file
|
|
29
|
+
|
|
30
|
+
## Undoable Commands Pattern
|
|
31
|
+
|
|
32
|
+
All write operations should use the Command pattern with undo support:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
36
|
+
import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
37
|
+
|
|
38
|
+
registerCommand('your_module.entity.create', {
|
|
39
|
+
async execute(payload, ctx) {
|
|
40
|
+
// 1. Create entity
|
|
41
|
+
// 2. Capture snapshot for undo: extractUndoPayload(entity)
|
|
42
|
+
// 3. Side effects: emitCrudSideEffects({ indexer: { entityType, cacheAliases } })
|
|
43
|
+
},
|
|
44
|
+
async undo(payload, ctx) {
|
|
45
|
+
// 1. Restore from snapshot
|
|
46
|
+
// 2. Side effects: emitCrudUndoSideEffects({ indexer: { entityType, cacheAliases } })
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Key points:
|
|
52
|
+
- Include `indexer: { entityType, cacheAliases }` in both `emitCrudSideEffects` and `emitCrudUndoSideEffects`
|
|
53
|
+
- Capture custom field snapshots in `before`/`after` payloads (`snapshot.custom`)
|
|
54
|
+
- Restore custom fields via `buildCustomFieldResetMap(before.custom, after.custom)` in undo
|
|
55
|
+
|
|
56
|
+
## Custom Field Integration
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- Pass `{ transform }` to normalize values (e.g., `normalizeCustomFieldSubmitValue`)
|
|
63
|
+
- Works for both `cf_` and `cf:` prefixed keys
|
|
64
|
+
- Pass `entityIds` to form helpers so correct custom-field sets are loaded
|
|
65
|
+
- If your module ships default custom fields, declare them in `ce.ts` via `entities[].fields`
|
|
66
|
+
|
|
67
|
+
## Search Configuration
|
|
68
|
+
|
|
69
|
+
Declare in `search.ts` with all three strategies:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import type { SearchModuleConfig } from '@open-mercato/shared/modules/search'
|
|
73
|
+
|
|
74
|
+
export const searchConfig: SearchModuleConfig = {
|
|
75
|
+
entities: {
|
|
76
|
+
'your_module.your_entity': {
|
|
77
|
+
fields: ['name', 'description'], // Fulltext indexing
|
|
78
|
+
// fieldPolicy for sensitive field handling
|
|
79
|
+
// buildSource for vector embeddings
|
|
80
|
+
// formatResult for search result display
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Key points:
|
|
87
|
+
- Use `fieldPolicy.excluded` for sensitive fields (passwords, tokens)
|
|
88
|
+
- Use `fieldPolicy.hashOnly` for PII needing exact-match only (email, phone)
|
|
89
|
+
- Always define `formatResult` for human-friendly search results
|
|
90
|
+
|
|
91
|
+
## Backend Page Structure
|
|
92
|
+
|
|
93
|
+
Follow this pattern for each page type:
|
|
94
|
+
|
|
95
|
+
| Page | Pattern | Key Features |
|
|
96
|
+
|------|---------|-------------|
|
|
97
|
+
| **List** | `DataTable` | Filters, search, export, row actions, pagination |
|
|
98
|
+
| **Create** | `CrudForm` mode=create | Fields, groups, custom fields, back link |
|
|
99
|
+
| **Detail/Edit** | `CrudForm` mode=edit or tabbed layout | Entity data, related entities, activities |
|
|
100
|
+
|
|
101
|
+
## Module Files Checklist
|
|
102
|
+
|
|
103
|
+
When scaffolding a new CRUD module, ensure all these files are present:
|
|
104
|
+
|
|
105
|
+
| File | Purpose |
|
|
106
|
+
|------|---------|
|
|
107
|
+
| `index.ts` | Module metadata |
|
|
108
|
+
| `acl.ts` | Feature-based permissions |
|
|
109
|
+
| `setup.ts` | Tenant init, default role features |
|
|
110
|
+
| `di.ts` | Awilix DI registrations |
|
|
111
|
+
| `events.ts` | Typed event declarations |
|
|
112
|
+
| `data/entities.ts` | MikroORM entity classes |
|
|
113
|
+
| `data/validators.ts` | Zod validation schemas |
|
|
114
|
+
| `search.ts` | Search indexing configuration |
|
|
115
|
+
| `ce.ts` | Custom entities / custom field sets |
|
|
116
|
+
|
|
117
|
+
Optional:
|
|
118
|
+
- `translations.ts` — translatable fields per entity
|
|
119
|
+
- `notifications.ts` — notification type definitions
|
|
120
|
+
- `cli.ts` — module CLI commands
|
|
121
|
+
|
|
122
|
+
## Entity Update Safety
|
|
123
|
+
|
|
124
|
+
When mutating entities across multiple phases that include queries:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
128
|
+
|
|
129
|
+
await withAtomicFlush(em, [
|
|
130
|
+
() => { record.name = 'New'; record.status = 'active' },
|
|
131
|
+
() => syncEntityTags(em, record, tags),
|
|
132
|
+
], { transaction: true })
|
|
133
|
+
|
|
134
|
+
// Side effects AFTER the atomic flush
|
|
135
|
+
await emitCrudSideEffects({ ... })
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Never run `em.find`/`em.findOne` between scalar mutations and `em.flush()` without `withAtomicFlush` — changes will be silently lost.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Data Sync Module — Standalone App Guide
|
|
2
|
+
|
|
3
|
+
The data sync module provides a streaming synchronization hub for import/export operations with external systems. Provider modules register `DataSyncAdapter` implementations.
|
|
4
|
+
|
|
5
|
+
## Creating a Sync Adapter
|
|
6
|
+
|
|
7
|
+
Implement the `DataSyncAdapter` interface in your provider module:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import type { DataSyncAdapter } from '@open-mercato/core/modules/data_sync/lib/adapter'
|
|
11
|
+
|
|
12
|
+
const myAdapter: DataSyncAdapter = {
|
|
13
|
+
providerKey: 'my_provider',
|
|
14
|
+
direction: 'import', // 'import' | 'export' | 'bidirectional'
|
|
15
|
+
supportedEntities: ['catalog.product', 'customers.person'],
|
|
16
|
+
|
|
17
|
+
async *streamImport(entityType, cursor, config) {
|
|
18
|
+
// Yield ImportBatch objects with records
|
|
19
|
+
yield { records: [...], cursor: 'next-page-token' }
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async validateConnection(credentials) {
|
|
23
|
+
// Verify external system is reachable
|
|
24
|
+
return { valid: true }
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async getInitialCursor(entityType) {
|
|
28
|
+
return null // Start from beginning
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Register in your module's `di.ts`:
|
|
34
|
+
```typescript
|
|
35
|
+
import { registerDataSyncAdapter } from '@open-mercato/core/modules/data_sync/lib/adapter-registry'
|
|
36
|
+
registerDataSyncAdapter(myAdapter)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Run Lifecycle
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pending → running → completed | failed | cancelled
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **Cursor persistence**: After each batch, cursor is saved — enables resume on failure
|
|
46
|
+
- **Progress**: Linked to `ProgressJob` for live progress display via `ProgressTopBar`
|
|
47
|
+
- **Cancellation**: Via `progressService.isCancellationRequested()`
|
|
48
|
+
- **Overlap protection**: Only one sync per integration + entityType + direction at a time
|
|
49
|
+
|
|
50
|
+
## Key Services (DI)
|
|
51
|
+
|
|
52
|
+
| Service | Purpose |
|
|
53
|
+
|---------|---------|
|
|
54
|
+
| `dataSyncRunService` | CRUD for sync runs, cursor management, overlap detection |
|
|
55
|
+
| `dataSyncEngine` | Orchestrates streaming import/export with batch processing and progress |
|
|
56
|
+
| `externalIdMappingService` | Maps local entity IDs to/from external system IDs |
|
|
57
|
+
|
|
58
|
+
## Starting a Sync
|
|
59
|
+
|
|
60
|
+
Via API:
|
|
61
|
+
```
|
|
62
|
+
POST /api/data_sync/run
|
|
63
|
+
{ "integrationId": "my_provider", "entityType": "catalog.product", "direction": "import" }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Syncs run asynchronously via the queue system — never run inline in API handlers.
|
|
67
|
+
|
|
68
|
+
## Queue Workers
|
|
69
|
+
|
|
70
|
+
| Queue | Worker | Concurrency |
|
|
71
|
+
|-------|--------|-------------|
|
|
72
|
+
| `data-sync-import` | Import handler | 5 |
|
|
73
|
+
| `data-sync-export` | Export handler | 5 |
|
|
74
|
+
| `data-sync-scheduled` | Scheduled sync dispatch | 3 |
|
|
75
|
+
|
|
76
|
+
## Events
|
|
77
|
+
|
|
78
|
+
| Event | When |
|
|
79
|
+
|-------|------|
|
|
80
|
+
| `data_sync.run.started` | Sync begins processing |
|
|
81
|
+
| `data_sync.run.completed` | Sync finishes successfully |
|
|
82
|
+
| `data_sync.run.failed` | Sync fails |
|
|
83
|
+
| `data_sync.run.cancelled` | Sync is cancelled |
|
|
84
|
+
|
|
85
|
+
Subscribe to these events to trigger post-sync side effects in your module.
|
|
86
|
+
|
|
87
|
+
## UMES Extension Points
|
|
88
|
+
|
|
89
|
+
Sync providers can extend the platform UI:
|
|
90
|
+
|
|
91
|
+
| Extension | Use Case |
|
|
92
|
+
|-----------|----------|
|
|
93
|
+
| **Widget Injection** | Sync status badges, mapping previews on entity pages |
|
|
94
|
+
| **Event Subscribers** | React to sync lifecycle events |
|
|
95
|
+
| **Entity Extensions** | Link sync metadata to core entities |
|
|
96
|
+
| **Response Enrichers** | Attach external ID data to API responses |
|
|
97
|
+
| **Notifications** | Alerts on sync completion/failure |
|
|
98
|
+
| **DOM Event Bridge** | Real-time sync progress via SSE |
|
|
99
|
+
| **Menu Injection** | Provider-specific sync dashboards in sidebar |
|
|
100
|
+
|
|
101
|
+
## Key Rules
|
|
102
|
+
|
|
103
|
+
- Always scope queries by `organizationId` + `tenantId`
|
|
104
|
+
- Use the queue system — never run syncs inline
|
|
105
|
+
- Persist cursor after each batch — enables resume on failure
|
|
106
|
+
- Log item-level errors — don't stop the sync for individual failures
|
|
107
|
+
- Check for overlap before starting a new run
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Integrations Module — Standalone App Guide
|
|
2
|
+
|
|
3
|
+
The integrations module provides the foundation for all external connectors (payment gateways, shipping carriers, data sync providers, etc.). It offers three shared mechanisms: **Integration Registry**, **Credentials API**, and **Operation Logs**.
|
|
4
|
+
|
|
5
|
+
## Creating an Integration Provider
|
|
6
|
+
|
|
7
|
+
Create a new module in your app for each provider:
|
|
8
|
+
|
|
9
|
+
1. Create `src/modules/<provider_id>/` with standard module files
|
|
10
|
+
2. Add `integration.ts` at the module root exporting an `IntegrationDefinition`:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import type { IntegrationDefinition } from '@open-mercato/shared/modules/integrations/types'
|
|
14
|
+
|
|
15
|
+
export const integration: IntegrationDefinition = {
|
|
16
|
+
id: 'my_provider',
|
|
17
|
+
name: 'My Provider',
|
|
18
|
+
description: 'Integration with My Provider',
|
|
19
|
+
category: 'payment',
|
|
20
|
+
credentials: {
|
|
21
|
+
fields: [
|
|
22
|
+
{ key: 'apiKey', label: 'API Key', type: 'password', required: true },
|
|
23
|
+
{ key: 'environment', label: 'Environment', type: 'select', options: ['sandbox', 'production'] },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
healthCheck: { service: 'myProviderHealthCheck' }, // optional
|
|
27
|
+
apiVersions: ['v1', 'v2'], // optional
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
3. Register the health check service in `di.ts` (if declared)
|
|
32
|
+
4. Run `yarn generate` to auto-discover the integration
|
|
33
|
+
|
|
34
|
+
## Key Services (DI)
|
|
35
|
+
|
|
36
|
+
| Service | Purpose |
|
|
37
|
+
|---------|---------|
|
|
38
|
+
| `integrationCredentialsService` | Encrypted credential CRUD with bundle fallthrough |
|
|
39
|
+
| `integrationStateService` | Enable/disable, API version, reauth, health state |
|
|
40
|
+
| `integrationLogService` | Structured logging with scoped loggers |
|
|
41
|
+
| `integrationHealthService` | Resolves and runs provider health checks |
|
|
42
|
+
|
|
43
|
+
## Credential Resolution
|
|
44
|
+
|
|
45
|
+
1. Direct credentials for the integration ID
|
|
46
|
+
2. If `bundleId` is set, fallback to bundle's credentials
|
|
47
|
+
3. Returns `null` if neither exists
|
|
48
|
+
|
|
49
|
+
## Bundle Integrations
|
|
50
|
+
|
|
51
|
+
For platform connectors with multiple integrations (e.g., an ERP with products + orders sync):
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
export const bundle: IntegrationBundle = {
|
|
55
|
+
id: 'my_erp',
|
|
56
|
+
name: 'My ERP',
|
|
57
|
+
integrations: ['my_erp_products', 'my_erp_orders'],
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Set `bundleId` on each child integration
|
|
62
|
+
- Bundle credentials are shared via fallthrough
|
|
63
|
+
|
|
64
|
+
## Events
|
|
65
|
+
|
|
66
|
+
| Event | When |
|
|
67
|
+
|-------|------|
|
|
68
|
+
| `integrations.credentials.updated` | Credentials saved |
|
|
69
|
+
| `integrations.state.updated` | Integration enabled/disabled |
|
|
70
|
+
| `integrations.version.changed` | API version changed |
|
|
71
|
+
| `integrations.log.created` | Log entry written |
|
|
72
|
+
|
|
73
|
+
## Extending the Integration Detail Page
|
|
74
|
+
|
|
75
|
+
Provider modules can add tabs, cards, or sections to the integration detail page:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// integration.ts
|
|
79
|
+
import { buildIntegrationDetailWidgetSpotId } from '@open-mercato/shared/modules/integrations/types'
|
|
80
|
+
|
|
81
|
+
export const integration = {
|
|
82
|
+
id: 'my_provider',
|
|
83
|
+
detailPage: {
|
|
84
|
+
widgetSpotId: buildIntegrationDetailWidgetSpotId('my_provider'),
|
|
85
|
+
},
|
|
86
|
+
} satisfies IntegrationDefinition
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Register widgets for that spot in `widgets/injection-table.ts`. Use `placement.kind: 'tab'` for additional tabs, `'group'` for card panels, `'stack'` for inline sections.
|
|
90
|
+
|
|
91
|
+
## UMES Extension Points
|
|
92
|
+
|
|
93
|
+
Integration providers can leverage the full extension system:
|
|
94
|
+
|
|
95
|
+
| Extension | Use Case |
|
|
96
|
+
|-----------|----------|
|
|
97
|
+
| **Widget Injection** | Inject status badges, config panels into other modules |
|
|
98
|
+
| **Event Subscribers** | React to integration events for side-effects |
|
|
99
|
+
| **Entity Extensions** | Link provider data to core entities (e.g., external IDs) |
|
|
100
|
+
| **Response Enrichers** | Attach provider data to API responses |
|
|
101
|
+
| **API Interceptors** | Intercept routes with before/after hooks |
|
|
102
|
+
| **Notifications** | In-app alerts on integration events |
|
|
103
|
+
| **DOM Event Bridge** | Real-time updates via SSE (`clientBroadcast: true`) |
|
|
104
|
+
|
|
105
|
+
## Provider-Owned Env Preconfiguration
|
|
106
|
+
|
|
107
|
+
If your provider needs credentials or settings after a fresh install:
|
|
108
|
+
|
|
109
|
+
- Read env vars in a provider-local helper (e.g., `lib/preset.ts`)
|
|
110
|
+
- Apply from your module's `setup.ts` for automatic tenant bootstrap
|
|
111
|
+
- Expose a CLI command for rerunning the bootstrap
|
|
112
|
+
- Use provider-prefixed env names (e.g., `OM_INTEGRATION_MYPROVIDER_*`)
|
|
113
|
+
- Persist through normal integration services — never special-case in core
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Sales Module — Standalone App Guide
|
|
2
|
+
|
|
3
|
+
Use the sales module for orders, quotes, invoices, shipments, and payments. This module has the most complex business logic in the system.
|
|
4
|
+
|
|
5
|
+
## Document Flow
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Quote → Order → Invoice
|
|
9
|
+
↓
|
|
10
|
+
Shipments + Payments
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Quotes convert to orders — do not create orders without a source quote (unless configured)
|
|
14
|
+
- Orders track shipments and payments independently
|
|
15
|
+
- Each entity has its own status workflow — do not skip states
|
|
16
|
+
- Returns create line-level adjustments and update `returned_quantity`
|
|
17
|
+
|
|
18
|
+
## Pricing Calculations
|
|
19
|
+
|
|
20
|
+
Always use the sales calculation service — never inline price math:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
const calcService = container.resolve('salesCalculationService')
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Dispatches `sales.line.calculate.*` and `sales.document.calculate.*` events
|
|
27
|
+
- For catalog pricing: use `selectBestPrice` from the catalog module
|
|
28
|
+
- Register custom line/totals calculators or override via DI
|
|
29
|
+
|
|
30
|
+
## Channel Scoping
|
|
31
|
+
|
|
32
|
+
All sales documents are scoped to channels. Channel selection affects:
|
|
33
|
+
- Available pricing tiers
|
|
34
|
+
- Document numbering sequences
|
|
35
|
+
- Visibility in admin UI
|
|
36
|
+
|
|
37
|
+
## Data Model
|
|
38
|
+
|
|
39
|
+
### Core Entities
|
|
40
|
+
| Entity | Purpose | Key Constraint |
|
|
41
|
+
|--------|---------|---------------|
|
|
42
|
+
| **Sales Orders** | Confirmed customer orders | MUST have a channel and at least one line |
|
|
43
|
+
| **Sales Quotes** | Proposed orders | MUST track conversion status |
|
|
44
|
+
| **Order/Quote Lines** | Individual items | MUST reference valid products |
|
|
45
|
+
| **Adjustments** | Discounts/surcharges | MUST use registered `AdjustmentKind` |
|
|
46
|
+
|
|
47
|
+
### Fulfillment
|
|
48
|
+
| Entity | Purpose |
|
|
49
|
+
|--------|---------|
|
|
50
|
+
| **Shipments** | Delivery tracking with status workflow |
|
|
51
|
+
| **Payments** | Payment recording with status workflow |
|
|
52
|
+
| **Returns** | Order returns with line selection and automatic adjustments |
|
|
53
|
+
|
|
54
|
+
### Configuration (do not modify directly)
|
|
55
|
+
Channels, statuses, payment/shipping methods, price kinds, adjustment kinds, and document numbers — configure via admin UI or `setup.ts` hooks.
|
|
56
|
+
|
|
57
|
+
## Subscribing to Sales Events
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// src/modules/<your_module>/subscribers/order-created.ts
|
|
61
|
+
export const metadata = {
|
|
62
|
+
event: 'sales.order.created',
|
|
63
|
+
persistent: true,
|
|
64
|
+
id: 'your-module-order-created',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default async function handler(payload, ctx) {
|
|
68
|
+
// React to new orders
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Key events: `sales.order.created` / `updated` / `deleted`, `sales.quote.created` / `updated`, `sales.payment.created`, `sales.shipment.created`
|
|
73
|
+
|
|
74
|
+
## Extending Sales UI
|
|
75
|
+
|
|
76
|
+
Common widget injection spots:
|
|
77
|
+
- `crud-form:sales.sales_order:fields` — order detail form
|
|
78
|
+
- `data-table:sales.orders:columns` — order list columns
|
|
79
|
+
- `data-table:sales.orders:row-actions` — order row actions
|
|
80
|
+
- `sales.document.detail.order:details` — order detail page sections
|
|
81
|
+
|
|
82
|
+
## Frontend Pages
|
|
83
|
+
|
|
84
|
+
- `frontend/quote/` — public-facing quote view for customer acceptance
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Workflows Module — Standalone App Guide
|
|
2
|
+
|
|
3
|
+
Use the workflows module for business process automation: defining step-based workflows, executing instances, handling user tasks, and triggering workflows from domain events.
|
|
4
|
+
|
|
5
|
+
## Using Workflows in Your App
|
|
6
|
+
|
|
7
|
+
The workflow engine is provided by `@open-mercato/core`. Your standalone app can:
|
|
8
|
+
|
|
9
|
+
1. **Create workflow definitions** via the visual editor at `/backend/workflows`
|
|
10
|
+
2. **Trigger workflows** from domain events emitted by your modules
|
|
11
|
+
3. **Subscribe to workflow events** for side effects in your modules
|
|
12
|
+
4. **Inject UI widgets** into workflow pages or inject workflow widgets into your pages
|
|
13
|
+
5. **Define user tasks** that require human approval or data entry
|
|
14
|
+
|
|
15
|
+
## Starting Workflows Programmatically
|
|
16
|
+
|
|
17
|
+
Resolve the workflow executor via DI — never import lib functions directly:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// In your module's DI-aware context (API route, subscriber, worker)
|
|
21
|
+
const executor = container.resolve('workflowExecutor')
|
|
22
|
+
|
|
23
|
+
await executor.startWorkflow({
|
|
24
|
+
workflowId: 'order-approval', // matches a WorkflowDefinition.workflowId
|
|
25
|
+
context: {
|
|
26
|
+
orderId: order.id,
|
|
27
|
+
orderTotal: order.totalGross,
|
|
28
|
+
customerName: order.customerName,
|
|
29
|
+
},
|
|
30
|
+
organizationId,
|
|
31
|
+
tenantId,
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Event Triggers
|
|
36
|
+
|
|
37
|
+
Configure automatic workflow starts from your module's domain events:
|
|
38
|
+
|
|
39
|
+
1. Create a workflow definition with a `triggers[]` entry in the visual editor or via API
|
|
40
|
+
2. The workflow engine's wildcard subscriber evaluates all non-internal events
|
|
41
|
+
3. Use `filterConditions` to narrow which events match (e.g., only orders above a threshold)
|
|
42
|
+
4. Use `contextMapping` to extract event payload fields into workflow context variables
|
|
43
|
+
5. Use `debounceMs` and `maxConcurrentInstances` to prevent trigger storms
|
|
44
|
+
|
|
45
|
+
Excluded event prefixes (never trigger workflows): `query_index`, `search`, `workflows`, `cache`, `queue`.
|
|
46
|
+
|
|
47
|
+
## Subscribing to Workflow Events
|
|
48
|
+
|
|
49
|
+
React to workflow lifecycle events in your module:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// src/modules/<your_module>/subscribers/workflow-completed.ts
|
|
53
|
+
export const metadata = {
|
|
54
|
+
event: 'workflows.instance.completed',
|
|
55
|
+
persistent: true,
|
|
56
|
+
id: 'your-module-workflow-completed',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default async function handler(payload, ctx) {
|
|
60
|
+
// payload.resourceId = instance ID
|
|
61
|
+
// payload.workflowId = definition ID
|
|
62
|
+
// payload.context = workflow context variables
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Key workflow events your module can subscribe to:
|
|
67
|
+
|
|
68
|
+
| Event | When it fires |
|
|
69
|
+
|-------|--------------|
|
|
70
|
+
| `workflows.instance.created` | New workflow instance started |
|
|
71
|
+
| `workflows.instance.completed` | Workflow finished successfully |
|
|
72
|
+
| `workflows.instance.failed` | Workflow failed |
|
|
73
|
+
| `workflows.instance.cancelled` | Workflow was cancelled |
|
|
74
|
+
| `workflows.task.created` | User task assigned |
|
|
75
|
+
| `workflows.task.completed` | User task completed |
|
|
76
|
+
| `workflows.step.completed` | Individual step finished |
|
|
77
|
+
|
|
78
|
+
## Step Types
|
|
79
|
+
|
|
80
|
+
| Step type | Use case |
|
|
81
|
+
|-----------|----------|
|
|
82
|
+
| `START` | Entry point — every definition has exactly one |
|
|
83
|
+
| `END` | Terminal step — marks workflow as COMPLETED |
|
|
84
|
+
| `USER_TASK` | Human approval or data entry — pauses until task completion |
|
|
85
|
+
| `AUTOMATED` | Executes transition activities immediately and advances |
|
|
86
|
+
| `SUB_WORKFLOW` | Invokes a nested workflow definition |
|
|
87
|
+
| `WAIT_FOR_SIGNAL` | Pauses for an external signal (e.g., payment confirmed) |
|
|
88
|
+
| `WAIT_FOR_TIMER` | Pauses for a configured duration |
|
|
89
|
+
| `PARALLEL_FORK` / `PARALLEL_JOIN` | Splits/merges parallel execution paths |
|
|
90
|
+
|
|
91
|
+
## Activity Types
|
|
92
|
+
|
|
93
|
+
Activities execute on transitions between steps:
|
|
94
|
+
|
|
95
|
+
| Activity type | Use case |
|
|
96
|
+
|---------------|----------|
|
|
97
|
+
| `SEND_EMAIL` | Send templated email |
|
|
98
|
+
| `CALL_API` | Call an internal API endpoint |
|
|
99
|
+
| `CALL_WEBHOOK` | Call an external HTTP endpoint |
|
|
100
|
+
| `UPDATE_ENTITY` | Mutate an entity via the command bus |
|
|
101
|
+
| `EMIT_EVENT` | Emit a domain event |
|
|
102
|
+
| `EXECUTE_FUNCTION` | Run a registered custom function |
|
|
103
|
+
| `WAIT` | Delay execution for a configured duration |
|
|
104
|
+
|
|
105
|
+
Use `{{context.*}}`, `{{workflow.*}}`, `{{env.*}}`, `{{now}}` for variable interpolation in activity config — never hardcode values.
|
|
106
|
+
|
|
107
|
+
## Sending Signals
|
|
108
|
+
|
|
109
|
+
Resume a workflow waiting for an external signal:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const executor = container.resolve('workflowExecutor')
|
|
113
|
+
|
|
114
|
+
await executor.sendSignal({
|
|
115
|
+
instanceId: workflowInstanceId,
|
|
116
|
+
signalName: 'payment_confirmed',
|
|
117
|
+
payload: { transactionId: '...' },
|
|
118
|
+
organizationId,
|
|
119
|
+
tenantId,
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Widget Injection
|
|
124
|
+
|
|
125
|
+
Inject workflow-related UI into your module's pages, or inject your module's widgets into workflow pages:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// src/modules/<your_module>/widgets/injection-table.ts
|
|
129
|
+
export const widgetInjections = {
|
|
130
|
+
// Inject into workflow task detail page
|
|
131
|
+
'workflows.task.detail:after': {
|
|
132
|
+
widgetId: 'your-module-task-context',
|
|
133
|
+
priority: 50,
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Compensation (Saga Pattern)
|
|
139
|
+
|
|
140
|
+
When a workflow step fails, compensation activities execute in reverse order to undo previous steps. This follows the saga pattern:
|
|
141
|
+
|
|
142
|
+
- **Sync activities** execute inline and advance immediately
|
|
143
|
+
- **Async activities** enqueue to the `workflow-activities` queue; workflow pauses until completion
|
|
144
|
+
- On failure, compensation runs in reverse — keep activity handlers **idempotent** (check state before mutating)
|
|
145
|
+
|
|
146
|
+
## Key Rules
|
|
147
|
+
|
|
148
|
+
- MUST resolve services via DI (`container.resolve('workflowExecutor')`) — never import lib functions directly
|
|
149
|
+
- MUST use `workflowExecutor.startWorkflow()` to create instances — never insert rows directly
|
|
150
|
+
- MUST keep activity handlers idempotent — they may be retried on failure
|
|
151
|
+
- MUST scope all queries by `organization_id` — workflow data is tenant-scoped
|
|
152
|
+
- MUST NOT couple your module to workflow internals — use event triggers and signals for integration
|