@open-mercato/core 0.4.2-canary-49d47ff90e → 0.4.2-canary-0ba39cdeb6
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/dist/modules/auth/backend/auth/profile/page.js.map +1 -1
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +4 -1
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +4 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/cli.js +13 -12
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/business_rules/api/execute/route.js +7 -1
- package/dist/modules/business_rules/api/execute/route.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +33 -3
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/configs/components/CachePanel.js +4 -4
- package/dist/modules/configs/components/CachePanel.js.map +2 -2
- package/dist/modules/configs/lib/system-status.js +48 -1
- package/dist/modules/configs/lib/system-status.js.map +2 -2
- package/dist/modules/dashboards/cli.js +12 -4
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +16 -11
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +3 -3
- package/dist/modules/dashboards/services/widgetDataService.js +110 -3
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +5 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +2 -1
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/notifications/lib/deliveryConfig.js +4 -2
- package/dist/modules/notifications/lib/deliveryConfig.js.map +2 -2
- package/dist/modules/notifications/lib/deliveryStrategies.js +14 -0
- package/dist/modules/notifications/lib/deliveryStrategies.js.map +7 -0
- package/dist/modules/notifications/subscribers/deliver-notification.js +33 -7
- package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +14 -6
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/auth/README.md +1 -1
- package/src/modules/auth/__tests__/cli-setup-acl.test.ts +1 -1
- package/src/modules/auth/backend/auth/profile/page.tsx +2 -2
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +4 -1
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +4 -1
- package/src/modules/auth/cli.ts +25 -12
- package/src/modules/business_rules/api/execute/route.ts +8 -1
- package/src/modules/business_rules/lib/__tests__/rule-engine.test.ts +51 -0
- package/src/modules/business_rules/lib/rule-engine.ts +57 -3
- package/src/modules/configs/components/CachePanel.tsx +4 -4
- package/src/modules/configs/i18n/en.json +12 -2
- package/src/modules/configs/i18n/pl.json +12 -2
- package/src/modules/configs/lib/system-status.ts +48 -1
- package/src/modules/configs/lib/system-status.types.ts +1 -0
- package/src/modules/dashboards/cli.ts +14 -4
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +22 -11
- package/src/modules/dashboards/services/widgetDataService.ts +132 -4
- package/src/modules/notifications/__tests__/deliver-notification.test.ts +195 -0
- package/src/modules/notifications/__tests__/deliveryStrategies.test.ts +19 -0
- package/src/modules/notifications/__tests__/notificationService.test.ts +208 -0
- package/src/modules/notifications/data/validators.ts +5 -0
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +2 -0
- package/src/modules/notifications/lib/deliveryConfig.ts +8 -0
- package/src/modules/notifications/lib/deliveryStrategies.ts +50 -0
- package/src/modules/notifications/subscribers/deliver-notification.ts +39 -10
- package/src/modules/workflows/lib/transition-handler.ts +18 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/notifications/frontend/NotificationSettingsPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\n\ntype NotificationDeliveryConfig = {\n appUrl?: string\n panelPath: string\n strategies: {\n database: { enabled: boolean }\n email: { enabled: boolean; from?: string; replyTo?: string; subjectPrefix?: string }\n }\n}\n\ntype SettingsResponse = {\n settings?: NotificationDeliveryConfig\n error?: string\n}\n\nconst emptySettings: NotificationDeliveryConfig = {\n panelPath: '/backend/notifications',\n strategies: {\n database: { enabled: true },\n email: { enabled: true },\n },\n}\n\nexport function NotificationSettingsPageClient() {\n const t = useT()\n const [settings, setSettings] = React.useState<NotificationDeliveryConfig | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchSettings = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const body = await readApiResultOrThrow<SettingsResponse>(\n '/api/notifications/settings',\n undefined,\n { errorMessage: t('notifications.settings.loadError', 'Failed to load notification settings'), allowNullResult: true },\n )\n if (body?.settings) {\n setSettings(body.settings)\n } else {\n setSettings(emptySettings)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.settings.loadError', 'Failed to load notification settings')\n setError(message)\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchSettings()\n }, [fetchSettings])\n\n const updateSettings = (patch: Partial<NotificationDeliveryConfig>) => {\n setSettings((prev) => (prev ? { ...prev, ...patch } : prev))\n }\n\n const updateStrategy = (\n strategy: keyof NotificationDeliveryConfig['strategies'],\n patch: Partial<NotificationDeliveryConfig['strategies'][keyof NotificationDeliveryConfig['strategies']]>,\n ) => {\n setSettings((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n strategies: {\n ...prev.strategies,\n [strategy]: {\n ...prev.strategies[strategy],\n ...patch,\n },\n },\n }\n })\n }\n\n const handleSave = async () => {\n if (!settings) return\n setSaving(true)\n try {\n const response = await apiCall<SettingsResponse>('/api/notifications/settings', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(settings),\n })\n if (!response.ok) {\n const message = response.result?.error || t('notifications.settings.saveError', 'Failed to save notification settings')\n throw new Error(message)\n }\n if (response.result?.settings) {\n setSettings(response.result.settings)\n }\n flash(t('notifications.settings.saveSuccess', 'Notification settings saved'), 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.settings.saveError', 'Failed to save notification settings')\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }\n\n if (loading || !settings) {\n return (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner size=\"sm\" />\n {t('notifications.settings.loading', 'Loading notification settings...')}\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{t('notifications.settings.pageTitle', 'Notification Delivery')}</h1>\n <p className=\"text-muted-foreground text-sm\">\n {t('notifications.settings.pageDescription', 'Configure delivery strategies for in-app notifications.')}\n </p>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('notifications.settings.core.title', 'Core delivery')}</CardTitle>\n <CardDescription>{t('notifications.settings.core.description', 'Control the default notification center and panel link used by external channels.')}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-app-url\">{t('notifications.settings.core.appUrl', 'Application URL')}</Label>\n <Input\n id=\"notifications-app-url\"\n value={settings.appUrl ?? ''}\n placeholder=\"https://app.open-mercato.com\"\n onChange={(event) => updateSettings({ appUrl: event.target.value || undefined })}\n />\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.appUrlHint', 'Used to build absolute links in email notifications.')}</p>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-panel-path\">{t('notifications.settings.core.panelPath', 'Notification panel path')}</Label>\n <Input\n id=\"notifications-panel-path\"\n value={settings.panelPath}\n onChange={(event) => updateSettings({ panelPath: event.target.value })}\n />\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.panelPathHint', 'Relative path for the read-only notification panel.')}</p>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <p className=\"text-sm font-medium\">{t('notifications.settings.core.databaseLabel', 'In-app notifications')}</p>\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.databaseHint', 'Store notifications in the database for the panel and bell.')}</p>\n </div>\n <Switch\n checked={settings.strategies.database.enabled}\n disabled\n onCheckedChange={(checked) => updateStrategy('database', { enabled: checked })}\n />\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('notifications.settings.email.title', 'Email strategy')}</CardTitle>\n <CardDescription>{t('notifications.settings.email.description', 'Send notification copies via Resend using React templates.')}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border p-3 md:col-span-2\">\n <div>\n <p className=\"text-sm font-medium\">{t('notifications.settings.email.enabledLabel', 'Enable email delivery')}</p>\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.email.enabledHint', 'Email actions are read-only and link back to the notification center.')}</p>\n </div>\n <Switch\n checked={settings.strategies.email.enabled}\n onCheckedChange={(checked) => updateStrategy('email', { enabled: checked })}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-email-from\">{t('notifications.settings.email.from', 'From address')}</Label>\n <Input\n id=\"notifications-email-from\"\n value={settings.strategies.email.from ?? ''}\n placeholder=\"notifications@open-mercato.com\"\n onChange={(event) => updateStrategy('email', { from: event.target.value || undefined })}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-email-reply\">{t('notifications.settings.email.replyTo', 'Reply-to')}</Label>\n <Input\n id=\"notifications-email-reply\"\n value={settings.strategies.email.replyTo ?? ''}\n placeholder=\"support@open-mercato.com\"\n onChange={(event) => updateStrategy('email', { replyTo: event.target.value || undefined })}\n />\n </div>\n <div className=\"space-y-2 md:col-span-2\">\n <Label htmlFor=\"notifications-email-subject-prefix\">{t('notifications.settings.email.subjectPrefix', 'Subject prefix')}</Label>\n <Input\n id=\"notifications-email-subject-prefix\"\n value={settings.strategies.email.subjectPrefix ?? ''}\n placeholder=\"[Open Mercato]\"\n onChange={(event) => updateStrategy('email', { subjectPrefix: event.target.value || undefined })}\n />\n </div>\n </CardContent>\n </Card>\n\n <div className=\"flex items-center gap-3\">\n <Button type=\"button\" onClick={handleSave} disabled={saving}>\n {saving ? t('notifications.settings.saving', 'Saving...') : t('notifications.settings.save', 'Save settings')}\n </Button>\n {error && <span className=\"text-sm text-destructive\">{error}</span>}\n </div>\n </div>\n )\n}\n\nexport default NotificationSettingsPageClient\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\n\ntype NotificationDeliveryConfig = {\n appUrl?: string\n panelPath: string\n strategies: {\n database: { enabled: boolean }\n email: { enabled: boolean; from?: string; replyTo?: string; subjectPrefix?: string }\n custom?: Record<string, { enabled?: boolean; config?: unknown }>\n }\n}\n\ntype SettingsResponse = {\n settings?: NotificationDeliveryConfig\n error?: string\n}\n\nconst emptySettings: NotificationDeliveryConfig = {\n panelPath: '/backend/notifications',\n strategies: {\n database: { enabled: true },\n email: { enabled: true },\n custom: {},\n },\n}\n\nexport function NotificationSettingsPageClient() {\n const t = useT()\n const [settings, setSettings] = React.useState<NotificationDeliveryConfig | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchSettings = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const body = await readApiResultOrThrow<SettingsResponse>(\n '/api/notifications/settings',\n undefined,\n { errorMessage: t('notifications.settings.loadError', 'Failed to load notification settings'), allowNullResult: true },\n )\n if (body?.settings) {\n setSettings(body.settings)\n } else {\n setSettings(emptySettings)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.settings.loadError', 'Failed to load notification settings')\n setError(message)\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchSettings()\n }, [fetchSettings])\n\n const updateSettings = (patch: Partial<NotificationDeliveryConfig>) => {\n setSettings((prev) => (prev ? { ...prev, ...patch } : prev))\n }\n\n const updateStrategy = (\n strategy: keyof NotificationDeliveryConfig['strategies'],\n patch: Partial<NotificationDeliveryConfig['strategies'][keyof NotificationDeliveryConfig['strategies']]>,\n ) => {\n setSettings((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n strategies: {\n ...prev.strategies,\n [strategy]: {\n ...prev.strategies[strategy],\n ...patch,\n },\n },\n }\n })\n }\n\n const handleSave = async () => {\n if (!settings) return\n setSaving(true)\n try {\n const response = await apiCall<SettingsResponse>('/api/notifications/settings', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(settings),\n })\n if (!response.ok) {\n const message = response.result?.error || t('notifications.settings.saveError', 'Failed to save notification settings')\n throw new Error(message)\n }\n if (response.result?.settings) {\n setSettings(response.result.settings)\n }\n flash(t('notifications.settings.saveSuccess', 'Notification settings saved'), 'success')\n } catch (err) {\n const message = err instanceof Error ? err.message : t('notifications.settings.saveError', 'Failed to save notification settings')\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }\n\n if (loading || !settings) {\n return (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner size=\"sm\" />\n {t('notifications.settings.loading', 'Loading notification settings...')}\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col gap-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{t('notifications.settings.pageTitle', 'Notification Delivery')}</h1>\n <p className=\"text-muted-foreground text-sm\">\n {t('notifications.settings.pageDescription', 'Configure delivery strategies for in-app notifications.')}\n </p>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('notifications.settings.core.title', 'Core delivery')}</CardTitle>\n <CardDescription>{t('notifications.settings.core.description', 'Control the default notification center and panel link used by external channels.')}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-app-url\">{t('notifications.settings.core.appUrl', 'Application URL')}</Label>\n <Input\n id=\"notifications-app-url\"\n value={settings.appUrl ?? ''}\n placeholder=\"https://app.open-mercato.com\"\n onChange={(event) => updateSettings({ appUrl: event.target.value || undefined })}\n />\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.appUrlHint', 'Used to build absolute links in email notifications.')}</p>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-panel-path\">{t('notifications.settings.core.panelPath', 'Notification panel path')}</Label>\n <Input\n id=\"notifications-panel-path\"\n value={settings.panelPath}\n onChange={(event) => updateSettings({ panelPath: event.target.value })}\n />\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.panelPathHint', 'Relative path for the read-only notification panel.')}</p>\n </div>\n <div className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <p className=\"text-sm font-medium\">{t('notifications.settings.core.databaseLabel', 'In-app notifications')}</p>\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.core.databaseHint', 'Store notifications in the database for the panel and bell.')}</p>\n </div>\n <Switch\n checked={settings.strategies.database.enabled}\n disabled\n onCheckedChange={(checked) => updateStrategy('database', { enabled: checked })}\n />\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('notifications.settings.email.title', 'Email strategy')}</CardTitle>\n <CardDescription>{t('notifications.settings.email.description', 'Send notification copies via Resend using React templates.')}</CardDescription>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"flex items-center justify-between rounded-lg border p-3 md:col-span-2\">\n <div>\n <p className=\"text-sm font-medium\">{t('notifications.settings.email.enabledLabel', 'Enable email delivery')}</p>\n <p className=\"text-xs text-muted-foreground\">{t('notifications.settings.email.enabledHint', 'Email actions are read-only and link back to the notification center.')}</p>\n </div>\n <Switch\n checked={settings.strategies.email.enabled}\n onCheckedChange={(checked) => updateStrategy('email', { enabled: checked })}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-email-from\">{t('notifications.settings.email.from', 'From address')}</Label>\n <Input\n id=\"notifications-email-from\"\n value={settings.strategies.email.from ?? ''}\n placeholder=\"notifications@open-mercato.com\"\n onChange={(event) => updateStrategy('email', { from: event.target.value || undefined })}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"notifications-email-reply\">{t('notifications.settings.email.replyTo', 'Reply-to')}</Label>\n <Input\n id=\"notifications-email-reply\"\n value={settings.strategies.email.replyTo ?? ''}\n placeholder=\"support@open-mercato.com\"\n onChange={(event) => updateStrategy('email', { replyTo: event.target.value || undefined })}\n />\n </div>\n <div className=\"space-y-2 md:col-span-2\">\n <Label htmlFor=\"notifications-email-subject-prefix\">{t('notifications.settings.email.subjectPrefix', 'Subject prefix')}</Label>\n <Input\n id=\"notifications-email-subject-prefix\"\n value={settings.strategies.email.subjectPrefix ?? ''}\n placeholder=\"[Open Mercato]\"\n onChange={(event) => updateStrategy('email', { subjectPrefix: event.target.value || undefined })}\n />\n </div>\n </CardContent>\n </Card>\n\n <div className=\"flex items-center gap-3\">\n <Button type=\"button\" onClick={handleSave} disabled={saving}>\n {saving ? t('notifications.settings.saving', 'Saving...') : t('notifications.settings.save', 'Save settings')}\n </Button>\n {error && <span className=\"text-sm text-destructive\">{error}</span>}\n </div>\n </div>\n )\n}\n\nexport default NotificationSettingsPageClient\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -36,7 +36,8 @@ const DEFAULT_NOTIFICATION_DELIVERY_CONFIG = (() => {
|
|
|
36
36
|
from: env.emailFrom,
|
|
37
37
|
replyTo: env.emailReplyTo,
|
|
38
38
|
subjectPrefix: env.emailSubjectPrefix
|
|
39
|
-
}
|
|
39
|
+
},
|
|
40
|
+
custom: {}
|
|
40
41
|
}
|
|
41
42
|
};
|
|
42
43
|
})();
|
|
@@ -59,7 +60,8 @@ const normalizeDeliveryConfig = (input) => {
|
|
|
59
60
|
from: strategies.email?.from,
|
|
60
61
|
replyTo: strategies.email?.replyTo,
|
|
61
62
|
subjectPrefix: strategies.email?.subjectPrefix
|
|
62
|
-
}
|
|
63
|
+
},
|
|
64
|
+
custom: strategies.custom ?? {}
|
|
63
65
|
}
|
|
64
66
|
};
|
|
65
67
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/notifications/lib/deliveryConfig.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { notificationDeliveryConfigSchema, type NotificationDeliveryConfigInput } from '../data/validators'\n\nexport const NOTIFICATIONS_DELIVERY_CONFIG_KEY = 'delivery_strategies'\n\nexport type NotificationDeliveryStrategyState = {\n enabled: boolean\n}\n\nexport type NotificationEmailDeliveryConfig = NotificationDeliveryStrategyState & {\n from?: string\n replyTo?: string\n subjectPrefix?: string\n}\n\nexport type NotificationDeliveryConfig = {\n appUrl?: string\n panelPath: string\n strategies: {\n database: NotificationDeliveryStrategyState\n email: NotificationEmailDeliveryConfig\n }\n}\n\nconst envString = (value: string | undefined | null) => {\n if (!value) return undefined\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n}\n\nconst resolveEnvDefaults = () => {\n const appUrl = envString(\n process.env.NOTIFICATIONS_APP_URL ||\n process.env.APPLICATION_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL\n )\n const panelPath = envString(process.env.NOTIFICATIONS_PANEL_PATH)\n const emailEnabled = parseBooleanWithDefault(process.env.NOTIFICATIONS_EMAIL_ENABLED, true)\n const emailFrom = envString(process.env.NOTIFICATIONS_EMAIL_FROM || process.env.EMAIL_FROM)\n const emailReplyTo = envString(process.env.NOTIFICATIONS_EMAIL_REPLY_TO || process.env.ADMIN_EMAIL)\n const emailSubjectPrefix = envString(process.env.NOTIFICATIONS_EMAIL_SUBJECT_PREFIX)\n\n return {\n appUrl,\n panelPath,\n emailEnabled,\n emailFrom,\n emailReplyTo,\n emailSubjectPrefix,\n }\n}\n\nexport const DEFAULT_NOTIFICATION_DELIVERY_CONFIG: NotificationDeliveryConfig = (() => {\n const env = resolveEnvDefaults()\n return {\n appUrl: env.appUrl,\n panelPath: env.panelPath ?? '/backend/notifications',\n strategies: {\n database: { enabled: true },\n email: {\n enabled: env.emailEnabled,\n from: env.emailFrom,\n replyTo: env.emailReplyTo,\n subjectPrefix: env.emailSubjectPrefix,\n },\n },\n }\n})()\n\nconst normalizeDeliveryConfig = (input?: unknown | null): NotificationDeliveryConfig => {\n const parsed = notificationDeliveryConfigSchema.safeParse(input ?? {})\n if (!parsed.success) {\n return { ...DEFAULT_NOTIFICATION_DELIVERY_CONFIG }\n }\n\n const value = parsed.data ?? {}\n const strategies = value.strategies ?? {}\n\n return {\n appUrl: value.appUrl,\n panelPath: value.panelPath ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG.panelPath,\n strategies: {\n database: {\n enabled: DEFAULT_NOTIFICATION_DELIVERY_CONFIG.strategies.database.enabled,\n },\n email: {\n enabled: strategies.email?.enabled ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG.strategies.email.enabled,\n from: strategies.email?.from,\n replyTo: strategies.email?.replyTo,\n subjectPrefix: strategies.email?.subjectPrefix,\n },\n },\n }\n}\n\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveNotificationDeliveryConfig(\n resolver: Resolver,\n options?: { defaultValue?: NotificationDeliveryConfig }\n): Promise<NotificationDeliveryConfig> {\n const fallback = options?.defaultValue ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return { ...fallback }\n }\n try {\n const value = await service.getValue('notifications', NOTIFICATIONS_DELIVERY_CONFIG_KEY, { defaultValue: fallback })\n return normalizeDeliveryConfig(value)\n } catch {\n return { ...fallback }\n }\n}\n\nexport async function saveNotificationDeliveryConfig(\n resolver: Resolver,\n config: NotificationDeliveryConfigInput\n): Promise<void> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n\n const normalized = normalizeDeliveryConfig(config)\n await service.setValue('notifications', NOTIFICATIONS_DELIVERY_CONFIG_KEY, normalized)\n}\n\nexport function resolveNotificationPanelUrl(config: NotificationDeliveryConfig): string | null {\n const base = config.appUrl\n || process.env.APPLICATION_URL\n || process.env.NEXT_PUBLIC_APP_URL\n || process.env.APP_URL\n if (!base || !base.trim()) {\n return config.panelPath\n }\n return `${base.replace(/\\/$/, '')}${config.panelPath}`\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,+BAA+B;AACxC,SAAS,wCAA8E;AAEhF,MAAM,oCAAoC;
|
|
4
|
+
"sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { notificationDeliveryConfigSchema, type NotificationDeliveryConfigInput } from '../data/validators'\n\nexport const NOTIFICATIONS_DELIVERY_CONFIG_KEY = 'delivery_strategies'\n\nexport type NotificationDeliveryStrategyState = {\n enabled: boolean\n}\n\nexport type NotificationCustomDeliveryConfig = {\n enabled?: boolean\n config?: unknown\n}\n\nexport type NotificationEmailDeliveryConfig = NotificationDeliveryStrategyState & {\n from?: string\n replyTo?: string\n subjectPrefix?: string\n}\n\nexport type NotificationDeliveryConfig = {\n appUrl?: string\n panelPath: string\n strategies: {\n database: NotificationDeliveryStrategyState\n email: NotificationEmailDeliveryConfig\n custom?: Record<string, NotificationCustomDeliveryConfig>\n }\n}\n\nconst envString = (value: string | undefined | null) => {\n if (!value) return undefined\n const trimmed = value.trim()\n return trimmed.length ? trimmed : undefined\n}\n\nconst resolveEnvDefaults = () => {\n const appUrl = envString(\n process.env.NOTIFICATIONS_APP_URL ||\n process.env.APPLICATION_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL\n )\n const panelPath = envString(process.env.NOTIFICATIONS_PANEL_PATH)\n const emailEnabled = parseBooleanWithDefault(process.env.NOTIFICATIONS_EMAIL_ENABLED, true)\n const emailFrom = envString(process.env.NOTIFICATIONS_EMAIL_FROM || process.env.EMAIL_FROM)\n const emailReplyTo = envString(process.env.NOTIFICATIONS_EMAIL_REPLY_TO || process.env.ADMIN_EMAIL)\n const emailSubjectPrefix = envString(process.env.NOTIFICATIONS_EMAIL_SUBJECT_PREFIX)\n\n return {\n appUrl,\n panelPath,\n emailEnabled,\n emailFrom,\n emailReplyTo,\n emailSubjectPrefix,\n }\n}\n\nexport const DEFAULT_NOTIFICATION_DELIVERY_CONFIG: NotificationDeliveryConfig = (() => {\n const env = resolveEnvDefaults()\n return {\n appUrl: env.appUrl,\n panelPath: env.panelPath ?? '/backend/notifications',\n strategies: {\n database: { enabled: true },\n email: {\n enabled: env.emailEnabled,\n from: env.emailFrom,\n replyTo: env.emailReplyTo,\n subjectPrefix: env.emailSubjectPrefix,\n },\n custom: {},\n },\n }\n})()\n\nconst normalizeDeliveryConfig = (input?: unknown | null): NotificationDeliveryConfig => {\n const parsed = notificationDeliveryConfigSchema.safeParse(input ?? {})\n if (!parsed.success) {\n return { ...DEFAULT_NOTIFICATION_DELIVERY_CONFIG }\n }\n\n const value = parsed.data ?? {}\n const strategies = value.strategies ?? {}\n\n return {\n appUrl: value.appUrl,\n panelPath: value.panelPath ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG.panelPath,\n strategies: {\n database: {\n enabled: DEFAULT_NOTIFICATION_DELIVERY_CONFIG.strategies.database.enabled,\n },\n email: {\n enabled: strategies.email?.enabled ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG.strategies.email.enabled,\n from: strategies.email?.from,\n replyTo: strategies.email?.replyTo,\n subjectPrefix: strategies.email?.subjectPrefix,\n },\n custom: strategies.custom ?? {},\n },\n }\n}\n\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveNotificationDeliveryConfig(\n resolver: Resolver,\n options?: { defaultValue?: NotificationDeliveryConfig }\n): Promise<NotificationDeliveryConfig> {\n const fallback = options?.defaultValue ?? DEFAULT_NOTIFICATION_DELIVERY_CONFIG\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return { ...fallback }\n }\n try {\n const value = await service.getValue('notifications', NOTIFICATIONS_DELIVERY_CONFIG_KEY, { defaultValue: fallback })\n return normalizeDeliveryConfig(value)\n } catch {\n return { ...fallback }\n }\n}\n\nexport async function saveNotificationDeliveryConfig(\n resolver: Resolver,\n config: NotificationDeliveryConfigInput\n): Promise<void> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n\n const normalized = normalizeDeliveryConfig(config)\n await service.setValue('notifications', NOTIFICATIONS_DELIVERY_CONFIG_KEY, normalized)\n}\n\nexport function resolveNotificationPanelUrl(config: NotificationDeliveryConfig): string | null {\n const base = config.appUrl\n || process.env.APPLICATION_URL\n || process.env.NEXT_PUBLIC_APP_URL\n || process.env.APP_URL\n if (!base || !base.trim()) {\n return config.panelPath\n }\n return `${base.replace(/\\/$/, '')}${config.panelPath}`\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const registry = [];
|
|
2
|
+
function registerNotificationDeliveryStrategy(strategy, options) {
|
|
3
|
+
const priority = options?.priority ?? 0;
|
|
4
|
+
registry.push({ ...strategy, priority });
|
|
5
|
+
registry.sort((a, b) => b.priority - a.priority);
|
|
6
|
+
}
|
|
7
|
+
function getNotificationDeliveryStrategies() {
|
|
8
|
+
return registry;
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
getNotificationDeliveryStrategies,
|
|
12
|
+
registerNotificationDeliveryStrategy
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=deliveryStrategies.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/notifications/lib/deliveryStrategies.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Notification } from '../data/entities'\nimport type { NotificationDeliveryConfig } from './deliveryConfig'\n\nexport type NotificationDeliveryStrategyConfig = {\n enabled?: boolean\n config?: unknown\n}\n\nexport type NotificationDeliveryRecipient = {\n email?: string | null\n name?: string | null\n}\n\nexport type NotificationDeliveryContext = {\n notification: Notification\n recipient: NotificationDeliveryRecipient\n title: string\n body: string | null\n panelUrl: string | null\n panelLink: string | null\n actionLinks: Array<{ id: string; label: string; href: string }>\n deliveryConfig: NotificationDeliveryConfig\n config: NotificationDeliveryStrategyConfig\n resolve: <T = unknown>(name: string) => T\n t: (key: string, fallback?: string, variables?: Record<string, string>) => string\n}\n\nexport type NotificationDeliveryStrategy = {\n id: string\n label?: string\n defaultEnabled?: boolean\n deliver: (ctx: NotificationDeliveryContext) => Promise<void> | void\n}\n\ntype RegisteredStrategy = NotificationDeliveryStrategy & { priority: number }\n\nconst registry: RegisteredStrategy[] = []\n\nexport function registerNotificationDeliveryStrategy(\n strategy: NotificationDeliveryStrategy,\n options?: { priority?: number }\n): void {\n const priority = options?.priority ?? 0\n registry.push({ ...strategy, priority })\n registry.sort((a, b) => b.priority - a.priority)\n}\n\nexport function getNotificationDeliveryStrategies(): NotificationDeliveryStrategy[] {\n return registry\n}\n"],
|
|
5
|
+
"mappings": "AAoCA,MAAM,WAAiC,CAAC;AAEjC,SAAS,qCACd,UACA,SACM;AACN,QAAM,WAAW,SAAS,YAAY;AACtC,WAAS,KAAK,EAAE,GAAG,UAAU,SAAS,CAAC;AACvC,WAAS,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AACjD;AAEO,SAAS,oCAAoE;AAClF,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Notification } from "../data/entities.js";
|
|
2
2
|
import { NOTIFICATION_EVENTS } from "../lib/events.js";
|
|
3
3
|
import { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig, resolveNotificationPanelUrl } from "../lib/deliveryConfig.js";
|
|
4
|
+
import { getNotificationDeliveryStrategies } from "../lib/deliveryStrategies.js";
|
|
4
5
|
import { sendEmail } from "@open-mercato/shared/lib/email/send";
|
|
5
6
|
import NotificationEmail from "../emails/NotificationEmail.js";
|
|
6
7
|
import { loadDictionary } from "@open-mercato/shared/lib/i18n/server";
|
|
@@ -66,7 +67,6 @@ async function handle(payload, ctx) {
|
|
|
66
67
|
const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, { defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG });
|
|
67
68
|
if (!deliveryConfig.strategies.email.enabled) {
|
|
68
69
|
debug("email delivery disabled");
|
|
69
|
-
return;
|
|
70
70
|
}
|
|
71
71
|
const em = ctx.resolve("em");
|
|
72
72
|
const notification = await em.findOne(Notification, {
|
|
@@ -84,7 +84,7 @@ async function handle(payload, ctx) {
|
|
|
84
84
|
} catch {
|
|
85
85
|
encryptionService = null;
|
|
86
86
|
}
|
|
87
|
-
const recipient = await resolveRecipient(em, notification, encryptionService);
|
|
87
|
+
const recipient = await resolveRecipient(em, notification, encryptionService) ?? { email: null, name: null };
|
|
88
88
|
if (!recipient?.email) {
|
|
89
89
|
debug("recipient has no email", notification.recipientUserId);
|
|
90
90
|
}
|
|
@@ -92,15 +92,14 @@ async function handle(payload, ctx) {
|
|
|
92
92
|
const panelUrl = resolveNotificationPanelUrl(deliveryConfig);
|
|
93
93
|
if (!panelUrl) {
|
|
94
94
|
debug("missing panelUrl; check appUrl/panelPath settings");
|
|
95
|
-
return;
|
|
96
95
|
}
|
|
97
|
-
const panelLink = buildPanelLink(panelUrl, notification.id);
|
|
98
|
-
const actionLinks = (notification.actionData?.actions ?? []).map((action) => ({
|
|
96
|
+
const panelLink = panelUrl ? buildPanelLink(panelUrl, notification.id) : null;
|
|
97
|
+
const actionLinks = panelLink ? (notification.actionData?.actions ?? []).map((action) => ({
|
|
99
98
|
id: action.id,
|
|
100
99
|
label: action.labelKey ? t(action.labelKey, action.label) : action.label,
|
|
101
100
|
href: panelLink
|
|
102
|
-
}));
|
|
103
|
-
if (deliveryConfig.strategies.email.enabled && recipient?.email) {
|
|
101
|
+
})) : [];
|
|
102
|
+
if (deliveryConfig.strategies.email.enabled && recipient?.email && panelLink) {
|
|
104
103
|
const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim();
|
|
105
104
|
const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title;
|
|
106
105
|
const copy = {
|
|
@@ -130,6 +129,33 @@ async function handle(payload, ctx) {
|
|
|
130
129
|
console.error("[notifications] email delivery failed", error);
|
|
131
130
|
}
|
|
132
131
|
}
|
|
132
|
+
const strategyConfigs = deliveryConfig.strategies.custom ?? {};
|
|
133
|
+
const strategies = getNotificationDeliveryStrategies();
|
|
134
|
+
for (const strategy of strategies) {
|
|
135
|
+
const strategyConfig = strategyConfigs[strategy.id];
|
|
136
|
+
const enabled = strategyConfig?.enabled ?? strategy.defaultEnabled ?? false;
|
|
137
|
+
if (!enabled) {
|
|
138
|
+
debug("custom delivery disabled", strategy.id);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await strategy.deliver({
|
|
143
|
+
notification,
|
|
144
|
+
recipient,
|
|
145
|
+
title,
|
|
146
|
+
body,
|
|
147
|
+
panelUrl,
|
|
148
|
+
panelLink,
|
|
149
|
+
actionLinks,
|
|
150
|
+
deliveryConfig,
|
|
151
|
+
config: strategyConfig ?? {},
|
|
152
|
+
resolve: ctx.resolve,
|
|
153
|
+
t
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`[notifications] delivery strategy failed (${strategy.id})`, error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
133
159
|
return;
|
|
134
160
|
}
|
|
135
161
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/notifications/subscribers/deliver-notification.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Notification } from '../data/entities'\nimport { NOTIFICATION_EVENTS } from '../lib/events'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig, resolveNotificationPanelUrl } from '../lib/deliveryConfig'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport NotificationEmail from '../emails/NotificationEmail'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { createFallbackTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\nexport const metadata = {\n event: NOTIFICATION_EVENTS.CREATED,\n persistent: true,\n id: 'notifications:deliver',\n}\n\nconst DEBUG = process.env.NOTIFICATIONS_DEBUG === 'true'\n\nfunction debug(...args: unknown[]): void {\n if (DEBUG) {\n console.log('[notifications]', ...args)\n }\n}\n\ntype NotificationCreatedPayload = {\n notificationId: string\n recipientUserId: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nconst buildPanelLink = (panelUrl: string, notificationId: string) => {\n if (panelUrl.startsWith('http://') || panelUrl.startsWith('https://')) {\n const url = new URL(panelUrl)\n url.searchParams.set('notificationId', notificationId)\n return url.toString()\n }\n const separator = panelUrl.includes('?') ? '&' : '?'\n return `${panelUrl}${separator}notificationId=${encodeURIComponent(notificationId)}`\n}\n\nconst resolveNotificationCopy = async (\n notification: Notification\n) => {\n const dict = await loadDictionary(defaultLocale)\n const t = createFallbackTranslator(dict)\n\n const title = notification.titleKey\n ? t(notification.titleKey, notification.title ?? notification.titleKey, notification.titleVariables ?? undefined)\n : notification.title\n\n const body = notification.bodyKey\n ? t(notification.bodyKey, notification.body ?? notification.bodyKey ?? '', notification.bodyVariables ?? undefined)\n : notification.body ?? null\n\n return { title, body, t }\n}\n\nconst resolveRecipient = async (\n em: EntityManager,\n notification: Notification,\n encryptionService?: TenantDataEncryptionService | null,\n) => {\n const where: Partial<User> & { deletedAt?: null } = {\n id: notification.recipientUserId,\n tenantId: notification.tenantId,\n deletedAt: null,\n }\n if (notification.organizationId) {\n where.organizationId = notification.organizationId\n }\n const record = await findOneWithDecryption(\n em,\n User,\n where,\n undefined,\n {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n encryptionService: encryptionService ?? null,\n },\n )\n if (!record) return null\n return {\n email: typeof record.email === 'string' ? record.email : null,\n name: typeof record.name === 'string' ? record.name : null,\n }\n}\n\n\nexport default async function handle(payload: NotificationCreatedPayload, ctx: ResolverContext) {\n debug('deliver notification event', payload)\n const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, { defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG })\n if (!deliveryConfig.strategies.email.enabled) {\n debug('email delivery disabled')\n
|
|
5
|
-
"mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,sCAAsC,mCAAmC,mCAAmC;AACrH,SAAS,iBAAiB;AAC1B,OAAO,uBAAuB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,gCAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AACtC,SAAS,YAAY;AAGd,MAAM,WAAW;AAAA,EACtB,OAAO,oBAAoB;AAAA,EAC3B,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,MAAM,QAAQ,QAAQ,IAAI,wBAAwB;AAElD,SAAS,SAAS,MAAuB;AACvC,MAAI,OAAO;AACT,YAAQ,IAAI,mBAAmB,GAAG,IAAI;AAAA,EACxC;AACF;AAaA,MAAM,iBAAiB,CAAC,UAAkB,mBAA2B;AACnE,MAAI,SAAS,WAAW,SAAS,KAAK,SAAS,WAAW,UAAU,GAAG;AACrE,UAAM,MAAM,IAAI,IAAI,QAAQ;AAC5B,QAAI,aAAa,IAAI,kBAAkB,cAAc;AACrD,WAAO,IAAI,SAAS;AAAA,EACtB;AACA,QAAM,YAAY,SAAS,SAAS,GAAG,IAAI,MAAM;AACjD,SAAO,GAAG,QAAQ,GAAG,SAAS,kBAAkB,mBAAmB,cAAc,CAAC;AACpF;AAEA,MAAM,0BAA0B,OAC9B,iBACG;AACH,QAAM,OAAO,MAAM,eAAe,aAAa;AAC/C,QAAM,IAAI,yBAAyB,IAAI;AAEvC,QAAM,QAAQ,aAAa,WACvB,EAAE,aAAa,UAAU,aAAa,SAAS,aAAa,UAAU,aAAa,kBAAkB,MAAS,IAC9G,aAAa;AAEjB,QAAM,OAAO,aAAa,UACtB,EAAE,aAAa,SAAS,aAAa,QAAQ,aAAa,WAAW,IAAI,aAAa,iBAAiB,MAAS,IAChH,aAAa,QAAQ;AAEzB,SAAO,EAAE,OAAO,MAAM,EAAE;AAC1B;AAEA,MAAM,mBAAmB,OACvB,IACA,cACA,sBACG;AACH,QAAM,QAA8C;AAAA,IAClD,IAAI,aAAa;AAAA,IACjB,UAAU,aAAa;AAAA,IACvB,WAAW;AAAA,EACb;AACA,MAAI,aAAa,gBAAgB;AAC/B,UAAM,iBAAiB,aAAa;AAAA,EACtC;AACA,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,aAAa;AAAA,MACvB,gBAAgB,aAAa,kBAAkB;AAAA,MAC/C,mBAAmB,qBAAqB;AAAA,IAC1C;AAAA,EACF;AACA,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO;AAAA,IACL,OAAO,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,IACzD,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAAA,EACxD;AACF;AAGA,eAAO,OAA8B,SAAqC,KAAsB;AAC9F,QAAM,8BAA8B,OAAO;AAC3C,QAAM,iBAAiB,MAAM,kCAAkC,KAAK,EAAE,cAAc,qCAAqC,CAAC;AAC1H,MAAI,CAAC,eAAe,WAAW,MAAM,SAAS;AAC5C,UAAM,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Notification } from '../data/entities'\nimport { NOTIFICATION_EVENTS } from '../lib/events'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, resolveNotificationDeliveryConfig, resolveNotificationPanelUrl } from '../lib/deliveryConfig'\nimport { getNotificationDeliveryStrategies } from '../lib/deliveryStrategies'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport NotificationEmail from '../emails/NotificationEmail'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { createFallbackTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\n\nexport const metadata = {\n event: NOTIFICATION_EVENTS.CREATED,\n persistent: true,\n id: 'notifications:deliver',\n}\n\nconst DEBUG = process.env.NOTIFICATIONS_DEBUG === 'true'\n\nfunction debug(...args: unknown[]): void {\n if (DEBUG) {\n console.log('[notifications]', ...args)\n }\n}\n\ntype NotificationCreatedPayload = {\n notificationId: string\n recipientUserId: string\n tenantId: string\n organizationId?: string | null\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\nconst buildPanelLink = (panelUrl: string, notificationId: string) => {\n if (panelUrl.startsWith('http://') || panelUrl.startsWith('https://')) {\n const url = new URL(panelUrl)\n url.searchParams.set('notificationId', notificationId)\n return url.toString()\n }\n const separator = panelUrl.includes('?') ? '&' : '?'\n return `${panelUrl}${separator}notificationId=${encodeURIComponent(notificationId)}`\n}\n\nconst resolveNotificationCopy = async (\n notification: Notification\n) => {\n const dict = await loadDictionary(defaultLocale)\n const t = createFallbackTranslator(dict)\n\n const title = notification.titleKey\n ? t(notification.titleKey, notification.title ?? notification.titleKey, notification.titleVariables ?? undefined)\n : notification.title\n\n const body = notification.bodyKey\n ? t(notification.bodyKey, notification.body ?? notification.bodyKey ?? '', notification.bodyVariables ?? undefined)\n : notification.body ?? null\n\n return { title, body, t }\n}\n\nconst resolveRecipient = async (\n em: EntityManager,\n notification: Notification,\n encryptionService?: TenantDataEncryptionService | null,\n) => {\n const where: Partial<User> & { deletedAt?: null } = {\n id: notification.recipientUserId,\n tenantId: notification.tenantId,\n deletedAt: null,\n }\n if (notification.organizationId) {\n where.organizationId = notification.organizationId\n }\n const record = await findOneWithDecryption(\n em,\n User,\n where,\n undefined,\n {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n encryptionService: encryptionService ?? null,\n },\n )\n if (!record) return null\n return {\n email: typeof record.email === 'string' ? record.email : null,\n name: typeof record.name === 'string' ? record.name : null,\n }\n}\n\n\nexport default async function handle(payload: NotificationCreatedPayload, ctx: ResolverContext) {\n debug('deliver notification event', payload)\n const deliveryConfig = await resolveNotificationDeliveryConfig(ctx, { defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG })\n if (!deliveryConfig.strategies.email.enabled) {\n debug('email delivery disabled')\n }\n\n const em = ctx.resolve('em') as EntityManager\n const notification = await em.findOne(Notification, {\n id: payload.notificationId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId ?? null,\n })\n if (!notification) {\n debug('notification not found', payload.notificationId)\n return\n }\n\n let encryptionService: TenantDataEncryptionService | null = null\n try {\n encryptionService = ctx.resolve<TenantDataEncryptionService>('tenantEncryptionService')\n } catch {\n encryptionService = null\n }\n\n const recipient = (await resolveRecipient(em, notification, encryptionService)) ?? { email: null, name: null }\n if (!recipient?.email) {\n debug('recipient has no email', notification.recipientUserId)\n }\n const { title, body, t } = await resolveNotificationCopy(notification)\n const panelUrl = resolveNotificationPanelUrl(deliveryConfig)\n if (!panelUrl) {\n debug('missing panelUrl; check appUrl/panelPath settings')\n }\n\n const panelLink = panelUrl ? buildPanelLink(panelUrl, notification.id) : null\n const actionLinks = panelLink\n ? (notification.actionData?.actions ?? []).map((action) => ({\n id: action.id,\n label: action.labelKey ? t(action.labelKey, action.label) : action.label,\n href: panelLink,\n }))\n : []\n\n if (deliveryConfig.strategies.email.enabled && recipient?.email && panelLink) {\n const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim()\n const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title\n const copy = {\n preview: t('notifications.delivery.email.preview', 'New notification'),\n heading: t('notifications.delivery.email.heading', 'You have a new notification'),\n bodyIntro: t('notifications.delivery.email.bodyIntro', 'Review the notification details and take any required actions.'),\n actionNotice: t('notifications.delivery.email.actionNotice', 'Actions are available in Open Mercato and are read-only in this email.'),\n openCta: t('notifications.delivery.email.openCta', 'Open notification center'),\n footer: t('notifications.delivery.email.footer', 'Open Mercato notifications'),\n }\n\n try {\n debug('sending email', { to: recipient.email, from: deliveryConfig.strategies.email.from, subject })\n await sendEmail({\n to: recipient.email,\n subject,\n from: deliveryConfig.strategies.email.from,\n replyTo: deliveryConfig.strategies.email.replyTo,\n react: NotificationEmail({\n title,\n body,\n actions: actionLinks,\n panelUrl: panelLink,\n copy,\n }),\n })\n } catch (error) {\n console.error('[notifications] email delivery failed', error)\n }\n }\n\n const strategyConfigs = deliveryConfig.strategies.custom ?? {}\n const strategies = getNotificationDeliveryStrategies()\n for (const strategy of strategies) {\n const strategyConfig = strategyConfigs[strategy.id]\n const enabled = strategyConfig?.enabled ?? strategy.defaultEnabled ?? false\n if (!enabled) {\n debug('custom delivery disabled', strategy.id)\n continue\n }\n try {\n await strategy.deliver({\n notification,\n recipient,\n title,\n body,\n panelUrl,\n panelLink,\n actionLinks,\n deliveryConfig,\n config: strategyConfig ?? {},\n resolve: ctx.resolve,\n t,\n })\n } catch (error) {\n console.error(`[notifications] delivery strategy failed (${strategy.id})`, error)\n }\n }\n\n return\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -121,6 +121,12 @@ async function findValidTransitions(em, instance, fromStepId, context) {
|
|
|
121
121
|
}
|
|
122
122
|
async function executeTransition(em, container, instance, fromStepId, toStepId, context) {
|
|
123
123
|
try {
|
|
124
|
+
let eventBus = null;
|
|
125
|
+
try {
|
|
126
|
+
eventBus = container.resolve("eventBus");
|
|
127
|
+
} catch {
|
|
128
|
+
eventBus = null;
|
|
129
|
+
}
|
|
124
130
|
const evaluation = await evaluateTransition(
|
|
125
131
|
em,
|
|
126
132
|
instance,
|
|
@@ -139,7 +145,8 @@ async function executeTransition(em, container, instance, fromStepId, toStepId,
|
|
|
139
145
|
em,
|
|
140
146
|
instance,
|
|
141
147
|
transition,
|
|
142
|
-
context
|
|
148
|
+
context,
|
|
149
|
+
eventBus
|
|
143
150
|
);
|
|
144
151
|
if (!preConditionsResult.allowed) {
|
|
145
152
|
const failedRules = preConditionsResult.executedRules.filter((r) => !r.conditionResult).map((r) => ({
|
|
@@ -303,7 +310,8 @@ async function executeTransition(em, container, instance, fromStepId, toStepId,
|
|
|
303
310
|
em,
|
|
304
311
|
instance,
|
|
305
312
|
transition,
|
|
306
|
-
context
|
|
313
|
+
context,
|
|
314
|
+
eventBus
|
|
307
315
|
);
|
|
308
316
|
if (!postConditionsResult.allowed) {
|
|
309
317
|
const failedRules = postConditionsResult.errors?.join(", ") || "Unknown post-condition failure";
|
|
@@ -403,7 +411,7 @@ async function evaluateTransitionConditions(em, instance, transition, context) {
|
|
|
403
411
|
};
|
|
404
412
|
}
|
|
405
413
|
}
|
|
406
|
-
async function evaluatePreConditions(em, instance, transition, context) {
|
|
414
|
+
async function evaluatePreConditions(em, instance, transition, context, eventBus) {
|
|
407
415
|
try {
|
|
408
416
|
const definition = await em.findOne(WorkflowDefinition, {
|
|
409
417
|
id: instance.definitionId
|
|
@@ -435,7 +443,7 @@ async function evaluatePreConditions(em, instance, transition, context) {
|
|
|
435
443
|
organizationId: instance.organizationId,
|
|
436
444
|
executedBy: context.userId
|
|
437
445
|
};
|
|
438
|
-
const result = await ruleEngine.executeRules(em, ruleContext);
|
|
446
|
+
const result = await ruleEngine.executeRules(em, ruleContext, { eventBus });
|
|
439
447
|
return result;
|
|
440
448
|
} catch (error) {
|
|
441
449
|
console.error("Error evaluating pre-conditions:", error);
|
|
@@ -447,7 +455,7 @@ async function evaluatePreConditions(em, instance, transition, context) {
|
|
|
447
455
|
};
|
|
448
456
|
}
|
|
449
457
|
}
|
|
450
|
-
async function evaluatePostConditions(em, instance, transition, context) {
|
|
458
|
+
async function evaluatePostConditions(em, instance, transition, context, eventBus) {
|
|
451
459
|
try {
|
|
452
460
|
const definition = await em.findOne(WorkflowDefinition, {
|
|
453
461
|
id: instance.definitionId
|
|
@@ -479,7 +487,7 @@ async function evaluatePostConditions(em, instance, transition, context) {
|
|
|
479
487
|
organizationId: instance.organizationId,
|
|
480
488
|
executedBy: context.userId
|
|
481
489
|
};
|
|
482
|
-
const result = await ruleEngine.executeRules(em, ruleContext);
|
|
490
|
+
const result = await ruleEngine.executeRules(em, ruleContext, { eventBus });
|
|
483
491
|
return result;
|
|
484
492
|
} catch (error) {
|
|
485
493
|
console.error("Error evaluating post-conditions:", error);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/transition-handler.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Workflows Module - Transition Handler Service\n *\n * Handles workflow transitions between steps:\n * - Evaluating if a transition is valid (checking conditions)\n * - Executing transitions (moving from one step to another)\n * - Integrating with business rules engine for pre/post conditions\n * - Executing activities on transition\n *\n * Functional API (no classes) following Open Mercato conventions.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport {\n WorkflowInstance,\n WorkflowDefinition,\n WorkflowEvent,\n} from '../data/entities'\nimport * as ruleEvaluator from '../../business_rules/lib/rule-evaluator'\nimport * as ruleEngine from '../../business_rules/lib/rule-engine'\nimport type { RuleEngineContext } from '../../business_rules/lib/rule-engine'\nimport * as activityExecutor from './activity-executor'\nimport type { ActivityDefinition } from './activity-executor'\nimport * as stepHandler from './step-handler'\n\n// ============================================================================\n// Types and Interfaces\n// ============================================================================\n\nexport interface TransitionEvaluationContext {\n workflowContext: Record<string, any>\n userId?: string\n triggerData?: any\n}\n\nexport interface TransitionEvaluationResult {\n isValid: boolean\n transition?: any\n reason?: string\n failedConditions?: string[]\n evaluationTime?: number\n}\n\nexport interface TransitionExecutionContext {\n workflowContext: Record<string, any>\n userId?: string\n triggerData?: any\n}\n\nexport interface TransitionExecutionResult {\n success: boolean\n nextStepId?: string\n pausedForActivities?: boolean\n conditionsEvaluated?: {\n preConditions: boolean\n postConditions: boolean\n }\n activitiesExecuted?: activityExecutor.ActivityExecutionResult[]\n error?: string\n}\n\nexport class TransitionError extends Error {\n constructor(\n message: string,\n public code: string,\n public details?: any\n ) {\n super(message)\n this.name = 'TransitionError'\n }\n}\n\n// ============================================================================\n// Main Transition Functions\n// ============================================================================\n\n/**\n * Evaluate if a transition from current step to target step is valid\n *\n * Checks:\n * - Transition exists in workflow definition\n * - Pre-conditions pass (if any business rules defined)\n * - Transition condition evaluates to true (if specified)\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param fromStepId - Current step ID\n * @param toStepId - Target step ID (optional - will auto-select if not provided)\n * @param context - Evaluation context\n * @returns Evaluation result with validity and reason\n */\nexport async function evaluateTransition(\n em: EntityManager,\n instance: WorkflowInstance,\n fromStepId: string,\n toStepId: string | undefined,\n context: TransitionEvaluationContext\n): Promise<TransitionEvaluationResult> {\n const startTime = Date.now()\n\n try {\n // Load workflow definition\n const definition = await em.findOne(WorkflowDefinition, {\n id: instance.definitionId,\n })\n\n if (!definition) {\n return {\n isValid: false,\n reason: `Workflow definition not found: ${instance.definitionId}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n\n // Find transition\n const transitions = definition.definition.transitions || []\n let transition: any\n\n if (toStepId) {\n // Find specific transition\n transition = transitions.find(\n (t: any) => t.fromStepId === fromStepId && t.toStepId === toStepId\n )\n\n if (!transition) {\n return {\n isValid: false,\n reason: `No transition found from ${fromStepId} to ${toStepId}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n } else {\n // Auto-select first valid transition\n const availableTransitions = transitions.filter(\n (t: any) => t.fromStepId === fromStepId\n )\n\n if (availableTransitions.length === 0) {\n return {\n isValid: false,\n reason: `No transitions available from step ${fromStepId}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n\n // Evaluate each transition to find first valid one\n for (const t of availableTransitions) {\n const result = await evaluateTransitionConditions(\n em,\n instance,\n t,\n context\n )\n\n if (result.isValid) {\n transition = t\n break\n }\n }\n\n if (!transition) {\n return {\n isValid: false,\n reason: `No valid transitions found from step ${fromStepId}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n }\n\n // Evaluate transition conditions (inline condition + business rules pre-conditions)\n const conditionResult = await evaluateTransitionConditions(\n em,\n instance,\n transition,\n context\n )\n\n return {\n ...conditionResult,\n transition,\n evaluationTime: Date.now() - startTime,\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n return {\n isValid: false,\n reason: `Transition evaluation error: ${errorMessage}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n}\n\n/**\n * Find all valid transitions from current step\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param fromStepId - Current step ID\n * @param context - Evaluation context\n * @returns Array of evaluation results for all transitions\n */\nexport async function findValidTransitions(\n em: EntityManager,\n instance: WorkflowInstance,\n fromStepId: string,\n context: TransitionEvaluationContext\n): Promise<TransitionEvaluationResult[]> {\n try {\n // Load workflow definition\n const definition = await em.findOne(WorkflowDefinition, {\n id: instance.definitionId,\n })\n\n if (!definition) {\n return []\n }\n\n // Find all transitions from current step\n const transitions = (definition.definition.transitions || []).filter(\n (t: any) => t.fromStepId === fromStepId\n )\n\n // Evaluate each transition\n const results: TransitionEvaluationResult[] = []\n\n for (const transition of transitions) {\n const result = await evaluateTransition(\n em,\n instance,\n fromStepId,\n transition.toStepId,\n context\n )\n\n results.push(result)\n }\n\n return results\n } catch (error) {\n console.error('Error finding valid transitions:', error)\n return []\n }\n}\n\n/**\n * Execute a transition from one step to another\n *\n * This is the main entry point for transition execution. It:\n * 1. Validates the transition\n * 2. Evaluates pre-conditions\n * 3. Executes activities (if any)\n * 4. Updates workflow instance state (atomically with activity outputs)\n * 5. Evaluates post-conditions\n * 6. Logs transition event\n *\n * @param em - Entity manager\n * @param container - DI container (for activity execution)\n * @param instance - Workflow instance\n * @param fromStepId - Current step ID\n * @param toStepId - Target step ID\n * @param context - Execution context\n * @returns Execution result\n */\nexport async function executeTransition(\n em: EntityManager,\n container: AwilixContainer,\n instance: WorkflowInstance,\n fromStepId: string,\n toStepId: string,\n context: TransitionExecutionContext\n): Promise<TransitionExecutionResult> {\n try {\n // First, evaluate if transition is valid\n const evaluation = await evaluateTransition(\n em,\n instance,\n fromStepId,\n toStepId,\n context\n )\n\n if (!evaluation.isValid) {\n return {\n success: false,\n error: evaluation.reason || 'Transition validation failed',\n }\n }\n\n const transition = evaluation.transition!\n\n // Evaluate pre-conditions (business rules)\n const preConditionsResult = await evaluatePreConditions(\n em,\n instance,\n transition,\n context\n )\n\n if (!preConditionsResult.allowed) {\n // Build detailed failure information\n const failedRules = preConditionsResult.executedRules\n .filter((r) => !r.conditionResult)\n .map((r) => ({\n ruleId: r.rule.ruleId,\n ruleName: r.rule.ruleName,\n error: r.error,\n }))\n\n const failedRulesDetails = failedRules.length > 0\n ? failedRules.map(r => `${r.ruleId}: ${r.error || 'condition failed'}`).join('; ')\n : preConditionsResult.errors?.join(', ') || 'Unknown pre-condition failure'\n\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_REJECTED',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId || `${fromStepId}->${toStepId}`,\n reason: 'Pre-conditions failed',\n failedRules: failedRulesDetails,\n failedRulesDetail: failedRules,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n return {\n success: false,\n error: `Pre-conditions failed: ${failedRulesDetails}`,\n conditionsEvaluated: {\n preConditions: false,\n postConditions: false,\n },\n }\n }\n\n // Execute activities (if any)\n let activityOutputs: Record<string, any> = {}\n const activityResults: activityExecutor.ActivityExecutionResult[] = []\n\n if (transition.activities && transition.activities.length > 0) {\n const activityContext: activityExecutor.ActivityContext = {\n workflowInstance: instance,\n workflowContext: {\n ...instance.context,\n ...context.workflowContext,\n },\n userId: context.userId,\n }\n\n // Execute all activities\n const results = await activityExecutor.executeActivities(\n em,\n container,\n transition.activities as ActivityDefinition[],\n activityContext\n )\n\n activityResults.push(...results)\n\n // Check for failures\n const failedActivities = results.filter(r => !r.success)\n\n if (failedActivities.length > 0) {\n const continueOnFailure = transition.continueOnActivityFailure ?? true\n\n // Log activity failures\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'ACTIVITY_FAILED',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId || `${fromStepId}->${toStepId}`,\n failedActivities: failedActivities.map(f => ({\n activityType: f.activityType,\n activityName: f.activityName,\n error: f.error,\n retryCount: f.retryCount,\n })),\n continueOnFailure,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n if (!continueOnFailure) {\n return {\n success: false,\n error: `Activities failed: ${failedActivities.map(f => f.error).join(', ')}`,\n conditionsEvaluated: {\n preConditions: true,\n postConditions: false,\n },\n }\n }\n }\n\n // Collect activity outputs for context update\n results.forEach(result => {\n if (result.success && result.output) {\n const key = result.activityName || result.activityType\n activityOutputs[key] = result.output\n }\n })\n }\n\n // Check if any activities are async - if so, pause before executing step\n const hasAsyncActivities = activityResults.some(r => r.async)\n\n if (hasAsyncActivities) {\n const pendingJobIds = activityResults\n .filter(a => a.async && a.jobId)\n .map(a => ({ activityId: a.activityId, jobId: a.jobId }))\n\n // Store pending transition state\n instance.pendingTransition = {\n toStepId,\n activityResults,\n timestamp: new Date(),\n }\n\n // Store pending activities in context for tracking\n instance.context = {\n ...instance.context,\n ...context.workflowContext,\n ...activityOutputs,\n _pendingAsyncActivities: pendingJobIds,\n }\n\n // Set status to waiting\n instance.status = 'WAITING_FOR_ACTIVITIES'\n instance.updatedAt = new Date()\n await em.flush()\n\n // Log event\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_PAUSED_FOR_ACTIVITIES',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId,\n pendingActivities: pendingJobIds,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n // Return WITHOUT executing step\n return {\n success: true,\n pausedForActivities: true,\n nextStepId: toStepId,\n conditionsEvaluated: {\n preConditions: true,\n postConditions: false, // Not evaluated yet\n },\n activitiesExecuted: activityResults,\n }\n }\n\n // Update workflow instance - set current step and update context atomically\n instance.currentStepId = toStepId\n instance.context = {\n ...instance.context,\n ...context.workflowContext,\n ...activityOutputs, // Include activity outputs\n }\n instance.updatedAt = new Date()\n\n await em.flush()\n\n // Execute the new step (this will create USER_TASK, handle END steps, etc.)\n const stepExecutionResult = await stepHandler.executeStep(\n em,\n instance,\n toStepId,\n {\n workflowContext: instance.context || {},\n userId: context.userId,\n triggerData: context.triggerData,\n },\n container\n )\n\n // Flush to database after step execution completes to make state visible to UI\n await em.flush()\n\n // Handle step execution failure\n if (stepExecutionResult.status === 'FAILED') {\n return {\n success: false,\n error: stepExecutionResult.error || 'Step execution failed',\n }\n }\n\n // Evaluate post-conditions (business rules)\n const postConditionsResult = await evaluatePostConditions(\n em,\n instance,\n transition,\n context\n )\n\n if (!postConditionsResult.allowed) {\n const failedRules = postConditionsResult.errors?.join(', ') || 'Unknown post-condition failure'\n\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_POST_CONDITION_FAILED',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId || `${fromStepId}->${toStepId}`,\n reason: 'Post-conditions failed',\n failedRules,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n // Note: We don't roll back the transition on post-condition failure\n // Post-conditions are warnings, not blockers\n }\n\n // Log successful transition\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_EXECUTED',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId || `${fromStepId}->${toStepId}`,\n transitionName: transition.transitionName,\n preConditionsPassed: true,\n postConditionsPassed: postConditionsResult.allowed,\n activitiesExecuted: activityResults.length,\n activitiesSucceeded: activityResults.filter(r => r.success).length,\n activitiesFailed: activityResults.filter(r => !r.success).length,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n return {\n success: true,\n nextStepId: toStepId,\n conditionsEvaluated: {\n preConditions: true,\n postConditions: postConditionsResult.allowed,\n },\n activitiesExecuted: activityResults,\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_FAILED',\n eventData: {\n fromStepId,\n toStepId,\n error: errorMessage,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n return {\n success: false,\n error: `Transition execution failed: ${errorMessage}`,\n }\n }\n}\n\n// ============================================================================\n// Condition Evaluation\n// ============================================================================\n\n/**\n * Evaluate transition conditions (inline condition expression)\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param transition - Transition definition\n * @param context - Evaluation context\n * @returns Evaluation result\n */\nasync function evaluateTransitionConditions(\n em: EntityManager,\n instance: WorkflowInstance,\n transition: any,\n context: TransitionEvaluationContext\n): Promise<TransitionEvaluationResult> {\n try {\n // If no condition specified, transition is always valid\n if (!transition.condition) {\n return {\n isValid: true,\n }\n }\n\n // Build data context for rule evaluation\n const data = {\n ...instance.context,\n ...context.workflowContext,\n triggerData: context.triggerData,\n }\n\n // Build evaluation context\n const evalContext: ruleEvaluator.RuleEvaluationContext = {\n entityType: 'workflow:transition',\n entityId: instance.id,\n user: context.userId ? { id: context.userId } : undefined,\n }\n\n // Evaluate condition using expression evaluator\n const result = await ruleEvaluator.evaluateConditions(\n transition.condition,\n data,\n evalContext\n )\n\n return {\n isValid: result,\n reason: result ? undefined : 'Transition condition evaluated to false',\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n return {\n isValid: false,\n reason: `Condition evaluation error: ${errorMessage}`,\n }\n }\n}\n\n/**\n * Evaluate pre-conditions using business rules engine\n *\n * Pre-conditions are GUARD rules that must pass before transition can execute.\n * If any GUARD rule fails, the transition is blocked.\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param transition - Transition definition\n * @param context - Execution context\n * @returns Rule engine result\n */\nasync function evaluatePreConditions(\n em: EntityManager,\n instance: WorkflowInstance,\n transition: any,\n context: TransitionExecutionContext\n): Promise<ruleEngine.RuleEngineResult> {\n try {\n // Load workflow definition to get workflow ID\n const definition = await em.findOne(WorkflowDefinition, {\n id: instance.definitionId,\n })\n\n if (!definition) {\n return {\n allowed: true,\n executedRules: [],\n totalExecutionTime: 0,\n }\n }\n\n // Build rule engine context\n const ruleContext: RuleEngineContext = {\n entityType: `workflow:${definition.workflowId}:transition`,\n entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,\n eventType: 'pre_transition',\n data: {\n workflowInstanceId: instance.id,\n workflowId: definition.workflowId,\n fromStepId: transition.fromStepId,\n toStepId: transition.toStepId,\n workflowContext: {\n ...instance.context,\n ...context.workflowContext,\n },\n triggerData: context.triggerData,\n },\n user: context.userId ? { id: context.userId } : undefined,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n executedBy: context.userId,\n }\n\n // Execute rules - only GUARD rules will affect the 'allowed' status\n const result = await ruleEngine.executeRules(em, ruleContext)\n\n return result\n } catch (error) {\n console.error('Error evaluating pre-conditions:', error)\n return {\n allowed: false,\n executedRules: [],\n totalExecutionTime: 0,\n errors: [error instanceof Error ? error.message : String(error)],\n }\n }\n}\n\n/**\n * Evaluate post-conditions using business rules engine\n *\n * Post-conditions are GUARD rules that should pass after transition executes.\n * Unlike pre-conditions, post-condition failures are logged but don't block the transition.\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param transition - Transition definition\n * @param context - Execution context\n * @returns Rule engine result\n */\nasync function evaluatePostConditions(\n em: EntityManager,\n instance: WorkflowInstance,\n transition: any,\n context: TransitionExecutionContext\n): Promise<ruleEngine.RuleEngineResult> {\n try {\n // Load workflow definition to get workflow ID\n const definition = await em.findOne(WorkflowDefinition, {\n id: instance.definitionId,\n })\n\n if (!definition) {\n return {\n allowed: true,\n executedRules: [],\n totalExecutionTime: 0,\n }\n }\n\n // Build rule engine context\n const ruleContext: RuleEngineContext = {\n entityType: `workflow:${definition.workflowId}:transition`,\n entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,\n eventType: 'post_transition',\n data: {\n workflowInstanceId: instance.id,\n workflowId: definition.workflowId,\n fromStepId: transition.fromStepId,\n toStepId: transition.toStepId,\n workflowContext: {\n ...instance.context,\n ...context.workflowContext,\n },\n triggerData: context.triggerData,\n },\n user: context.userId ? { id: context.userId } : undefined,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n executedBy: context.userId,\n }\n\n // Execute rules\n const result = await ruleEngine.executeRules(em, ruleContext)\n\n return result\n } catch (error) {\n console.error('Error evaluating post-conditions:', error)\n return {\n allowed: false,\n executedRules: [],\n totalExecutionTime: 0,\n errors: [error instanceof Error ? error.message : String(error)],\n }\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Log transition-related event to event sourcing table\n */\nasync function logTransitionEvent(\n em: EntityManager,\n event: {\n workflowInstanceId: string\n eventType: string\n eventData: any\n userId?: string\n tenantId: string\n organizationId: string\n }\n): Promise<WorkflowEvent> {\n const workflowEvent = em.create(WorkflowEvent, {\n ...event,\n occurredAt: new Date(),\n })\n\n await em.persistAndFlush(workflowEvent)\n return workflowEvent\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * Workflows Module - Transition Handler Service\n *\n * Handles workflow transitions between steps:\n * - Evaluating if a transition is valid (checking conditions)\n * - Executing transitions (moving from one step to another)\n * - Integrating with business rules engine for pre/post conditions\n * - Executing activities on transition\n *\n * Functional API (no classes) following Open Mercato conventions.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport type { EventBus } from '@open-mercato/events'\nimport {\n WorkflowInstance,\n WorkflowDefinition,\n WorkflowEvent,\n} from '../data/entities'\nimport * as ruleEvaluator from '../../business_rules/lib/rule-evaluator'\nimport * as ruleEngine from '../../business_rules/lib/rule-engine'\nimport type { RuleEngineContext } from '../../business_rules/lib/rule-engine'\nimport * as activityExecutor from './activity-executor'\nimport type { ActivityDefinition } from './activity-executor'\nimport * as stepHandler from './step-handler'\n\n// ============================================================================\n// Types and Interfaces\n// ============================================================================\n\nexport interface TransitionEvaluationContext {\n workflowContext: Record<string, any>\n userId?: string\n triggerData?: any\n}\n\nexport interface TransitionEvaluationResult {\n isValid: boolean\n transition?: any\n reason?: string\n failedConditions?: string[]\n evaluationTime?: number\n}\n\nexport interface TransitionExecutionContext {\n workflowContext: Record<string, any>\n userId?: string\n triggerData?: any\n}\n\nexport interface TransitionExecutionResult {\n success: boolean\n nextStepId?: string\n pausedForActivities?: boolean\n conditionsEvaluated?: {\n preConditions: boolean\n postConditions: boolean\n }\n activitiesExecuted?: activityExecutor.ActivityExecutionResult[]\n error?: string\n}\n\nexport class TransitionError extends Error {\n constructor(\n message: string,\n public code: string,\n public details?: any\n ) {\n super(message)\n this.name = 'TransitionError'\n }\n}\n\n// ============================================================================\n// Main Transition Functions\n// ============================================================================\n\n/**\n * Evaluate if a transition from current step to target step is valid\n *\n * Checks:\n * - Transition exists in workflow definition\n * - Pre-conditions pass (if any business rules defined)\n * - Transition condition evaluates to true (if specified)\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param fromStepId - Current step ID\n * @param toStepId - Target step ID (optional - will auto-select if not provided)\n * @param context - Evaluation context\n * @returns Evaluation result with validity and reason\n */\nexport async function evaluateTransition(\n em: EntityManager,\n instance: WorkflowInstance,\n fromStepId: string,\n toStepId: string | undefined,\n context: TransitionEvaluationContext\n): Promise<TransitionEvaluationResult> {\n const startTime = Date.now()\n\n try {\n // Load workflow definition\n const definition = await em.findOne(WorkflowDefinition, {\n id: instance.definitionId,\n })\n\n if (!definition) {\n return {\n isValid: false,\n reason: `Workflow definition not found: ${instance.definitionId}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n\n // Find transition\n const transitions = definition.definition.transitions || []\n let transition: any\n\n if (toStepId) {\n // Find specific transition\n transition = transitions.find(\n (t: any) => t.fromStepId === fromStepId && t.toStepId === toStepId\n )\n\n if (!transition) {\n return {\n isValid: false,\n reason: `No transition found from ${fromStepId} to ${toStepId}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n } else {\n // Auto-select first valid transition\n const availableTransitions = transitions.filter(\n (t: any) => t.fromStepId === fromStepId\n )\n\n if (availableTransitions.length === 0) {\n return {\n isValid: false,\n reason: `No transitions available from step ${fromStepId}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n\n // Evaluate each transition to find first valid one\n for (const t of availableTransitions) {\n const result = await evaluateTransitionConditions(\n em,\n instance,\n t,\n context\n )\n\n if (result.isValid) {\n transition = t\n break\n }\n }\n\n if (!transition) {\n return {\n isValid: false,\n reason: `No valid transitions found from step ${fromStepId}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n }\n\n // Evaluate transition conditions (inline condition + business rules pre-conditions)\n const conditionResult = await evaluateTransitionConditions(\n em,\n instance,\n transition,\n context\n )\n\n return {\n ...conditionResult,\n transition,\n evaluationTime: Date.now() - startTime,\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n return {\n isValid: false,\n reason: `Transition evaluation error: ${errorMessage}`,\n evaluationTime: Date.now() - startTime,\n }\n }\n}\n\n/**\n * Find all valid transitions from current step\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param fromStepId - Current step ID\n * @param context - Evaluation context\n * @returns Array of evaluation results for all transitions\n */\nexport async function findValidTransitions(\n em: EntityManager,\n instance: WorkflowInstance,\n fromStepId: string,\n context: TransitionEvaluationContext\n): Promise<TransitionEvaluationResult[]> {\n try {\n // Load workflow definition\n const definition = await em.findOne(WorkflowDefinition, {\n id: instance.definitionId,\n })\n\n if (!definition) {\n return []\n }\n\n // Find all transitions from current step\n const transitions = (definition.definition.transitions || []).filter(\n (t: any) => t.fromStepId === fromStepId\n )\n\n // Evaluate each transition\n const results: TransitionEvaluationResult[] = []\n\n for (const transition of transitions) {\n const result = await evaluateTransition(\n em,\n instance,\n fromStepId,\n transition.toStepId,\n context\n )\n\n results.push(result)\n }\n\n return results\n } catch (error) {\n console.error('Error finding valid transitions:', error)\n return []\n }\n}\n\n/**\n * Execute a transition from one step to another\n *\n * This is the main entry point for transition execution. It:\n * 1. Validates the transition\n * 2. Evaluates pre-conditions\n * 3. Executes activities (if any)\n * 4. Updates workflow instance state (atomically with activity outputs)\n * 5. Evaluates post-conditions\n * 6. Logs transition event\n *\n * @param em - Entity manager\n * @param container - DI container (for activity execution)\n * @param instance - Workflow instance\n * @param fromStepId - Current step ID\n * @param toStepId - Target step ID\n * @param context - Execution context\n * @returns Execution result\n */\nexport async function executeTransition(\n em: EntityManager,\n container: AwilixContainer,\n instance: WorkflowInstance,\n fromStepId: string,\n toStepId: string,\n context: TransitionExecutionContext\n): Promise<TransitionExecutionResult> {\n try {\n let eventBus: Pick<EventBus, 'emitEvent'> | null = null\n try {\n eventBus = container.resolve('eventBus') as EventBus\n } catch {\n eventBus = null\n }\n\n // First, evaluate if transition is valid\n const evaluation = await evaluateTransition(\n em,\n instance,\n fromStepId,\n toStepId,\n context\n )\n\n if (!evaluation.isValid) {\n return {\n success: false,\n error: evaluation.reason || 'Transition validation failed',\n }\n }\n\n const transition = evaluation.transition!\n\n // Evaluate pre-conditions (business rules)\n const preConditionsResult = await evaluatePreConditions(\n em,\n instance,\n transition,\n context,\n eventBus\n )\n\n if (!preConditionsResult.allowed) {\n // Build detailed failure information\n const failedRules = preConditionsResult.executedRules\n .filter((r) => !r.conditionResult)\n .map((r) => ({\n ruleId: r.rule.ruleId,\n ruleName: r.rule.ruleName,\n error: r.error,\n }))\n\n const failedRulesDetails = failedRules.length > 0\n ? failedRules.map(r => `${r.ruleId}: ${r.error || 'condition failed'}`).join('; ')\n : preConditionsResult.errors?.join(', ') || 'Unknown pre-condition failure'\n\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_REJECTED',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId || `${fromStepId}->${toStepId}`,\n reason: 'Pre-conditions failed',\n failedRules: failedRulesDetails,\n failedRulesDetail: failedRules,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n return {\n success: false,\n error: `Pre-conditions failed: ${failedRulesDetails}`,\n conditionsEvaluated: {\n preConditions: false,\n postConditions: false,\n },\n }\n }\n\n // Execute activities (if any)\n let activityOutputs: Record<string, any> = {}\n const activityResults: activityExecutor.ActivityExecutionResult[] = []\n\n if (transition.activities && transition.activities.length > 0) {\n const activityContext: activityExecutor.ActivityContext = {\n workflowInstance: instance,\n workflowContext: {\n ...instance.context,\n ...context.workflowContext,\n },\n userId: context.userId,\n }\n\n // Execute all activities\n const results = await activityExecutor.executeActivities(\n em,\n container,\n transition.activities as ActivityDefinition[],\n activityContext\n )\n\n activityResults.push(...results)\n\n // Check for failures\n const failedActivities = results.filter(r => !r.success)\n\n if (failedActivities.length > 0) {\n const continueOnFailure = transition.continueOnActivityFailure ?? true\n\n // Log activity failures\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'ACTIVITY_FAILED',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId || `${fromStepId}->${toStepId}`,\n failedActivities: failedActivities.map(f => ({\n activityType: f.activityType,\n activityName: f.activityName,\n error: f.error,\n retryCount: f.retryCount,\n })),\n continueOnFailure,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n if (!continueOnFailure) {\n return {\n success: false,\n error: `Activities failed: ${failedActivities.map(f => f.error).join(', ')}`,\n conditionsEvaluated: {\n preConditions: true,\n postConditions: false,\n },\n }\n }\n }\n\n // Collect activity outputs for context update\n results.forEach(result => {\n if (result.success && result.output) {\n const key = result.activityName || result.activityType\n activityOutputs[key] = result.output\n }\n })\n }\n\n // Check if any activities are async - if so, pause before executing step\n const hasAsyncActivities = activityResults.some(r => r.async)\n\n if (hasAsyncActivities) {\n const pendingJobIds = activityResults\n .filter(a => a.async && a.jobId)\n .map(a => ({ activityId: a.activityId, jobId: a.jobId }))\n\n // Store pending transition state\n instance.pendingTransition = {\n toStepId,\n activityResults,\n timestamp: new Date(),\n }\n\n // Store pending activities in context for tracking\n instance.context = {\n ...instance.context,\n ...context.workflowContext,\n ...activityOutputs,\n _pendingAsyncActivities: pendingJobIds,\n }\n\n // Set status to waiting\n instance.status = 'WAITING_FOR_ACTIVITIES'\n instance.updatedAt = new Date()\n await em.flush()\n\n // Log event\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_PAUSED_FOR_ACTIVITIES',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId,\n pendingActivities: pendingJobIds,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n // Return WITHOUT executing step\n return {\n success: true,\n pausedForActivities: true,\n nextStepId: toStepId,\n conditionsEvaluated: {\n preConditions: true,\n postConditions: false, // Not evaluated yet\n },\n activitiesExecuted: activityResults,\n }\n }\n\n // Update workflow instance - set current step and update context atomically\n instance.currentStepId = toStepId\n instance.context = {\n ...instance.context,\n ...context.workflowContext,\n ...activityOutputs, // Include activity outputs\n }\n instance.updatedAt = new Date()\n\n await em.flush()\n\n // Execute the new step (this will create USER_TASK, handle END steps, etc.)\n const stepExecutionResult = await stepHandler.executeStep(\n em,\n instance,\n toStepId,\n {\n workflowContext: instance.context || {},\n userId: context.userId,\n triggerData: context.triggerData,\n },\n container\n )\n\n // Flush to database after step execution completes to make state visible to UI\n await em.flush()\n\n // Handle step execution failure\n if (stepExecutionResult.status === 'FAILED') {\n return {\n success: false,\n error: stepExecutionResult.error || 'Step execution failed',\n }\n }\n\n // Evaluate post-conditions (business rules)\n const postConditionsResult = await evaluatePostConditions(\n em,\n instance,\n transition,\n context,\n eventBus\n )\n\n if (!postConditionsResult.allowed) {\n const failedRules = postConditionsResult.errors?.join(', ') || 'Unknown post-condition failure'\n\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_POST_CONDITION_FAILED',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId || `${fromStepId}->${toStepId}`,\n reason: 'Post-conditions failed',\n failedRules,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n // Note: We don't roll back the transition on post-condition failure\n // Post-conditions are warnings, not blockers\n }\n\n // Log successful transition\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_EXECUTED',\n eventData: {\n fromStepId,\n toStepId,\n transitionId: transition.transitionId || `${fromStepId}->${toStepId}`,\n transitionName: transition.transitionName,\n preConditionsPassed: true,\n postConditionsPassed: postConditionsResult.allowed,\n activitiesExecuted: activityResults.length,\n activitiesSucceeded: activityResults.filter(r => r.success).length,\n activitiesFailed: activityResults.filter(r => !r.success).length,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n return {\n success: true,\n nextStepId: toStepId,\n conditionsEvaluated: {\n preConditions: true,\n postConditions: postConditionsResult.allowed,\n },\n activitiesExecuted: activityResults,\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n await logTransitionEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'TRANSITION_FAILED',\n eventData: {\n fromStepId,\n toStepId,\n error: errorMessage,\n },\n userId: context.userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n return {\n success: false,\n error: `Transition execution failed: ${errorMessage}`,\n }\n }\n}\n\n// ============================================================================\n// Condition Evaluation\n// ============================================================================\n\n/**\n * Evaluate transition conditions (inline condition expression)\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param transition - Transition definition\n * @param context - Evaluation context\n * @returns Evaluation result\n */\nasync function evaluateTransitionConditions(\n em: EntityManager,\n instance: WorkflowInstance,\n transition: any,\n context: TransitionEvaluationContext\n): Promise<TransitionEvaluationResult> {\n try {\n // If no condition specified, transition is always valid\n if (!transition.condition) {\n return {\n isValid: true,\n }\n }\n\n // Build data context for rule evaluation\n const data = {\n ...instance.context,\n ...context.workflowContext,\n triggerData: context.triggerData,\n }\n\n // Build evaluation context\n const evalContext: ruleEvaluator.RuleEvaluationContext = {\n entityType: 'workflow:transition',\n entityId: instance.id,\n user: context.userId ? { id: context.userId } : undefined,\n }\n\n // Evaluate condition using expression evaluator\n const result = await ruleEvaluator.evaluateConditions(\n transition.condition,\n data,\n evalContext\n )\n\n return {\n isValid: result,\n reason: result ? undefined : 'Transition condition evaluated to false',\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n return {\n isValid: false,\n reason: `Condition evaluation error: ${errorMessage}`,\n }\n }\n}\n\n/**\n * Evaluate pre-conditions using business rules engine\n *\n * Pre-conditions are GUARD rules that must pass before transition can execute.\n * If any GUARD rule fails, the transition is blocked.\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param transition - Transition definition\n * @param context - Execution context\n * @returns Rule engine result\n */\nasync function evaluatePreConditions(\n em: EntityManager,\n instance: WorkflowInstance,\n transition: any,\n context: TransitionExecutionContext,\n eventBus: Pick<EventBus, 'emitEvent'> | null\n): Promise<ruleEngine.RuleEngineResult> {\n try {\n // Load workflow definition to get workflow ID\n const definition = await em.findOne(WorkflowDefinition, {\n id: instance.definitionId,\n })\n\n if (!definition) {\n return {\n allowed: true,\n executedRules: [],\n totalExecutionTime: 0,\n }\n }\n\n // Build rule engine context\n const ruleContext: RuleEngineContext = {\n entityType: `workflow:${definition.workflowId}:transition`,\n entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,\n eventType: 'pre_transition',\n data: {\n workflowInstanceId: instance.id,\n workflowId: definition.workflowId,\n fromStepId: transition.fromStepId,\n toStepId: transition.toStepId,\n workflowContext: {\n ...instance.context,\n ...context.workflowContext,\n },\n triggerData: context.triggerData,\n },\n user: context.userId ? { id: context.userId } : undefined,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n executedBy: context.userId,\n }\n\n // Execute rules - only GUARD rules will affect the 'allowed' status\n const result = await ruleEngine.executeRules(em, ruleContext, { eventBus })\n\n return result\n } catch (error) {\n console.error('Error evaluating pre-conditions:', error)\n return {\n allowed: false,\n executedRules: [],\n totalExecutionTime: 0,\n errors: [error instanceof Error ? error.message : String(error)],\n }\n }\n}\n\n/**\n * Evaluate post-conditions using business rules engine\n *\n * Post-conditions are GUARD rules that should pass after transition executes.\n * Unlike pre-conditions, post-condition failures are logged but don't block the transition.\n *\n * @param em - Entity manager\n * @param instance - Workflow instance\n * @param transition - Transition definition\n * @param context - Execution context\n * @returns Rule engine result\n */\nasync function evaluatePostConditions(\n em: EntityManager,\n instance: WorkflowInstance,\n transition: any,\n context: TransitionExecutionContext,\n eventBus: Pick<EventBus, 'emitEvent'> | null\n): Promise<ruleEngine.RuleEngineResult> {\n try {\n // Load workflow definition to get workflow ID\n const definition = await em.findOne(WorkflowDefinition, {\n id: instance.definitionId,\n })\n\n if (!definition) {\n return {\n allowed: true,\n executedRules: [],\n totalExecutionTime: 0,\n }\n }\n\n // Build rule engine context\n const ruleContext: RuleEngineContext = {\n entityType: `workflow:${definition.workflowId}:transition`,\n entityId: transition.transitionId || `${transition.fromStepId}->${transition.toStepId}`,\n eventType: 'post_transition',\n data: {\n workflowInstanceId: instance.id,\n workflowId: definition.workflowId,\n fromStepId: transition.fromStepId,\n toStepId: transition.toStepId,\n workflowContext: {\n ...instance.context,\n ...context.workflowContext,\n },\n triggerData: context.triggerData,\n },\n user: context.userId ? { id: context.userId } : undefined,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n executedBy: context.userId,\n }\n\n // Execute rules\n const result = await ruleEngine.executeRules(em, ruleContext, { eventBus })\n\n return result\n } catch (error) {\n console.error('Error evaluating post-conditions:', error)\n return {\n allowed: false,\n executedRules: [],\n totalExecutionTime: 0,\n errors: [error instanceof Error ? error.message : String(error)],\n }\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Log transition-related event to event sourcing table\n */\nasync function logTransitionEvent(\n em: EntityManager,\n event: {\n workflowInstanceId: string\n eventType: string\n eventData: any\n userId?: string\n tenantId: string\n organizationId: string\n }\n): Promise<WorkflowEvent> {\n const workflowEvent = em.create(WorkflowEvent, {\n ...event,\n occurredAt: new Date(),\n })\n\n await em.persistAndFlush(workflowEvent)\n return workflowEvent\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.2-canary-
|
|
3
|
+
"version": "0.4.2-canary-0ba39cdeb6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.2-canary-
|
|
210
|
+
"@open-mercato/shared": "0.4.2-canary-0ba39cdeb6",
|
|
211
211
|
"@xyflow/react": "^12.6.0",
|
|
212
212
|
"date-fns": "^4.1.0",
|
|
213
213
|
"date-fns-tz": "^3.2.0"
|
|
@@ -8,7 +8,7 @@ Features:
|
|
|
8
8
|
- `mercato auth add-user --email <e> --password <p> --organizationId <id> [--roles r1,r2]`
|
|
9
9
|
- `mercato auth seed-roles`
|
|
10
10
|
- `mercato auth add-org --name <org>`
|
|
11
|
-
- `mercato auth setup --orgName <org> --email <e> --password <p> [--roles superadmin,admin]`
|
|
11
|
+
- `mercato auth setup --orgName <org> --email <e> --password <p> [--roles superadmin,admin] [--skip-password-policy]`
|
|
12
12
|
|
|
13
13
|
DB entities used (defined in root schema):
|
|
14
14
|
- `users` with: `email`, `password_hash`, `is_confirmed`, `last_login_at`, `organization_id`, timestamps.
|
|
@@ -46,7 +46,7 @@ describe('auth CLI setup seeds ACLs', () => {
|
|
|
46
46
|
findOneOrFail.mockImplementation(async (_: any, where: any) => ({ id: 'role-' + where.name, name: where.name }))
|
|
47
47
|
|
|
48
48
|
// Act
|
|
49
|
-
await setup.run(['--orgName', 'Acme', '--email', 'root@acme.com', '--password', 'secret'])
|
|
49
|
+
await setup.run(['--orgName', 'Acme', '--email', 'root@acme.com', '--password', 'secret', '--skip-password-policy'])
|
|
50
50
|
|
|
51
51
|
// Assert: persistAndFlush was called to create three RoleAcl rows with expected flags/features
|
|
52
52
|
const calls = persistAndFlush.mock.calls.map((c) => c[0])
|