@open-mercato/core 0.4.2-canary-ba1d84349b → 0.4.2-canary-07dbc98202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/dist/generated/entities/notification/index.js +57 -0
  2. package/dist/generated/entities/notification/index.js.map +7 -0
  3. package/dist/generated/entities.ids.generated.js +63 -59
  4. package/dist/generated/entities.ids.generated.js.map +2 -2
  5. package/dist/generated/entity-fields-registry.js +2 -0
  6. package/dist/generated/entity-fields-registry.js.map +2 -2
  7. package/dist/modules/api_docs/frontend/docs/api/page.js +3 -2
  8. package/dist/modules/api_docs/frontend/docs/api/page.js.map +2 -2
  9. package/dist/modules/auth/api/admin/nav.js +4 -3
  10. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  11. package/dist/modules/auth/api/profile/route.js +155 -0
  12. package/dist/modules/auth/api/profile/route.js.map +7 -0
  13. package/dist/modules/auth/api/reset/confirm.js +25 -2
  14. package/dist/modules/auth/api/reset/confirm.js.map +2 -2
  15. package/dist/modules/auth/api/reset.js +23 -0
  16. package/dist/modules/auth/api/reset.js.map +2 -2
  17. package/dist/modules/auth/api/sidebar/preferences/route.js +14 -9
  18. package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
  19. package/dist/modules/auth/backend/auth/profile/page.js +99 -0
  20. package/dist/modules/auth/backend/auth/profile/page.js.map +7 -0
  21. package/dist/modules/auth/backend/auth/profile/page.meta.js +12 -0
  22. package/dist/modules/auth/backend/auth/profile/page.meta.js.map +7 -0
  23. package/dist/modules/auth/commands/users.js +55 -0
  24. package/dist/modules/auth/commands/users.js.map +2 -2
  25. package/dist/modules/auth/lib/setup-app.js +1 -0
  26. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  27. package/dist/modules/auth/notifications.js +112 -0
  28. package/dist/modules/auth/notifications.js.map +7 -0
  29. package/dist/modules/auth/services/authService.js +3 -3
  30. package/dist/modules/auth/services/authService.js.map +2 -2
  31. package/dist/modules/business_rules/notifications.js +28 -0
  32. package/dist/modules/business_rules/notifications.js.map +7 -0
  33. package/dist/modules/business_rules/subscribers/rule-execution-failed-notification.js +37 -0
  34. package/dist/modules/business_rules/subscribers/rule-execution-failed-notification.js.map +7 -0
  35. package/dist/modules/catalog/notifications.js +28 -0
  36. package/dist/modules/catalog/notifications.js.map +7 -0
  37. package/dist/modules/catalog/subscribers/low-stock-notification.js +38 -0
  38. package/dist/modules/catalog/subscribers/low-stock-notification.js.map +7 -0
  39. package/dist/modules/configs/cli.js +6 -0
  40. package/dist/modules/configs/cli.js.map +2 -2
  41. package/dist/modules/customers/commands/deals.js +31 -0
  42. package/dist/modules/customers/commands/deals.js.map +2 -2
  43. package/dist/modules/customers/notifications.js +48 -0
  44. package/dist/modules/customers/notifications.js.map +7 -0
  45. package/dist/modules/notifications/acl.js +11 -0
  46. package/dist/modules/notifications/acl.js.map +7 -0
  47. package/dist/modules/notifications/api/[id]/action/route.js +74 -0
  48. package/dist/modules/notifications/api/[id]/action/route.js.map +7 -0
  49. package/dist/modules/notifications/api/[id]/dismiss/route.js +15 -0
  50. package/dist/modules/notifications/api/[id]/dismiss/route.js.map +7 -0
  51. package/dist/modules/notifications/api/[id]/read/route.js +15 -0
  52. package/dist/modules/notifications/api/[id]/read/route.js.map +7 -0
  53. package/dist/modules/notifications/api/[id]/restore/route.js +53 -0
  54. package/dist/modules/notifications/api/[id]/restore/route.js.map +7 -0
  55. package/dist/modules/notifications/api/batch/route.js +17 -0
  56. package/dist/modules/notifications/api/batch/route.js.map +7 -0
  57. package/dist/modules/notifications/api/feature/route.js +17 -0
  58. package/dist/modules/notifications/api/feature/route.js.map +7 -0
  59. package/dist/modules/notifications/api/mark-all-read/route.js +35 -0
  60. package/dist/modules/notifications/api/mark-all-read/route.js.map +7 -0
  61. package/dist/modules/notifications/api/openapi.js +76 -0
  62. package/dist/modules/notifications/api/openapi.js.map +7 -0
  63. package/dist/modules/notifications/api/role/route.js +17 -0
  64. package/dist/modules/notifications/api/role/route.js.map +7 -0
  65. package/dist/modules/notifications/api/route.js +85 -0
  66. package/dist/modules/notifications/api/route.js.map +7 -0
  67. package/dist/modules/notifications/api/settings/route.js +155 -0
  68. package/dist/modules/notifications/api/settings/route.js.map +7 -0
  69. package/dist/modules/notifications/api/unread-count/route.js +38 -0
  70. package/dist/modules/notifications/api/unread-count/route.js.map +7 -0
  71. package/dist/modules/notifications/backend/config/notifications/page.js +10 -0
  72. package/dist/modules/notifications/backend/config/notifications/page.js.map +7 -0
  73. package/dist/modules/notifications/backend/config/notifications/page.meta.js +24 -0
  74. package/dist/modules/notifications/backend/config/notifications/page.meta.js.map +7 -0
  75. package/dist/modules/notifications/cli.js +16 -0
  76. package/dist/modules/notifications/cli.js.map +7 -0
  77. package/dist/modules/notifications/data/entities.js +112 -0
  78. package/dist/modules/notifications/data/entities.js.map +7 -0
  79. package/dist/modules/notifications/data/validators.js +94 -0
  80. package/dist/modules/notifications/data/validators.js.map +7 -0
  81. package/dist/modules/notifications/di.js +13 -0
  82. package/dist/modules/notifications/di.js.map +7 -0
  83. package/dist/modules/notifications/emails/NotificationEmail.js +58 -0
  84. package/dist/modules/notifications/emails/NotificationEmail.js.map +7 -0
  85. package/dist/modules/notifications/frontend/NotificationInboxPageClient.js +44 -0
  86. package/dist/modules/notifications/frontend/NotificationInboxPageClient.js.map +7 -0
  87. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +219 -0
  88. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +7 -0
  89. package/dist/modules/notifications/index.js +14 -0
  90. package/dist/modules/notifications/index.js.map +7 -0
  91. package/dist/modules/notifications/lib/deliveryConfig.js +105 -0
  92. package/dist/modules/notifications/lib/deliveryConfig.js.map +7 -0
  93. package/dist/modules/notifications/lib/events.js +12 -0
  94. package/dist/modules/notifications/lib/events.js.map +7 -0
  95. package/dist/modules/notifications/lib/notificationBuilder.js +66 -0
  96. package/dist/modules/notifications/lib/notificationBuilder.js.map +7 -0
  97. package/dist/modules/notifications/lib/notificationFactory.js +54 -0
  98. package/dist/modules/notifications/lib/notificationFactory.js.map +7 -0
  99. package/dist/modules/notifications/lib/notificationMapper.js +34 -0
  100. package/dist/modules/notifications/lib/notificationMapper.js.map +7 -0
  101. package/dist/modules/notifications/lib/notificationRecipients.js +35 -0
  102. package/dist/modules/notifications/lib/notificationRecipients.js.map +7 -0
  103. package/dist/modules/notifications/lib/notificationService.js +279 -0
  104. package/dist/modules/notifications/lib/notificationService.js.map +7 -0
  105. package/dist/modules/notifications/lib/routeHelpers.js +101 -0
  106. package/dist/modules/notifications/lib/routeHelpers.js.map +7 -0
  107. package/dist/modules/notifications/lib/safeHref.js +24 -0
  108. package/dist/modules/notifications/lib/safeHref.js.map +7 -0
  109. package/dist/modules/notifications/migrations/Migration20260123000001.js +70 -0
  110. package/dist/modules/notifications/migrations/Migration20260123000001.js.map +7 -0
  111. package/dist/modules/notifications/migrations/Migration20260126150000.js +37 -0
  112. package/dist/modules/notifications/migrations/Migration20260126150000.js.map +7 -0
  113. package/dist/modules/notifications/subscribers/deliver-notification.js +139 -0
  114. package/dist/modules/notifications/subscribers/deliver-notification.js.map +7 -0
  115. package/dist/modules/notifications/workers/create-notification.worker.js +70 -0
  116. package/dist/modules/notifications/workers/create-notification.worker.js.map +7 -0
  117. package/dist/modules/sales/commands/documents.js +53 -0
  118. package/dist/modules/sales/commands/documents.js.map +2 -2
  119. package/dist/modules/sales/commands/payments.js +26 -0
  120. package/dist/modules/sales/commands/payments.js.map +2 -2
  121. package/dist/modules/sales/notifications.client.js +51 -0
  122. package/dist/modules/sales/notifications.client.js.map +7 -0
  123. package/dist/modules/sales/notifications.js +88 -0
  124. package/dist/modules/sales/notifications.js.map +7 -0
  125. package/dist/modules/sales/subscribers/quote-expiring-notification.js +38 -0
  126. package/dist/modules/sales/subscribers/quote-expiring-notification.js.map +7 -0
  127. package/dist/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.js +137 -0
  128. package/dist/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.js.map +7 -0
  129. package/dist/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.js +137 -0
  130. package/dist/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.js.map +7 -0
  131. package/dist/modules/sales/widgets/notifications/index.js +7 -0
  132. package/dist/modules/sales/widgets/notifications/index.js.map +7 -0
  133. package/dist/modules/sales/widgets/notifications/useSalesDocumentTotals.js +60 -0
  134. package/dist/modules/sales/widgets/notifications/useSalesDocumentTotals.js.map +7 -0
  135. package/dist/modules/staff/commands/leave-requests.js +79 -0
  136. package/dist/modules/staff/commands/leave-requests.js.map +2 -2
  137. package/dist/modules/staff/notifications.js +75 -0
  138. package/dist/modules/staff/notifications.js.map +7 -0
  139. package/dist/modules/workflows/notifications.js +28 -0
  140. package/dist/modules/workflows/notifications.js.map +7 -0
  141. package/dist/modules/workflows/subscribers/task-assigned-notification.js +38 -0
  142. package/dist/modules/workflows/subscribers/task-assigned-notification.js.map +7 -0
  143. package/generated/entities/notification/index.ts +27 -0
  144. package/generated/entities.ids.generated.ts +63 -59
  145. package/generated/entity-fields-registry.ts +2 -0
  146. package/package.json +2 -2
  147. package/src/modules/api_docs/frontend/docs/api/page.tsx +3 -2
  148. package/src/modules/auth/api/admin/nav.ts +10 -6
  149. package/src/modules/auth/api/profile/route.ts +160 -0
  150. package/src/modules/auth/api/reset/confirm.ts +25 -2
  151. package/src/modules/auth/api/reset.ts +23 -0
  152. package/src/modules/auth/api/sidebar/preferences/route.ts +21 -12
  153. package/src/modules/auth/backend/auth/profile/page.meta.ts +8 -0
  154. package/src/modules/auth/backend/auth/profile/page.tsx +127 -0
  155. package/src/modules/auth/commands/users.ts +68 -0
  156. package/src/modules/auth/i18n/de.json +29 -1
  157. package/src/modules/auth/i18n/en.json +29 -1
  158. package/src/modules/auth/i18n/es.json +29 -1
  159. package/src/modules/auth/i18n/pl.json +29 -1
  160. package/src/modules/auth/lib/setup-app.ts +1 -0
  161. package/src/modules/auth/notifications.ts +109 -0
  162. package/src/modules/auth/services/authService.ts +4 -4
  163. package/src/modules/business_rules/i18n/en.json +3 -1
  164. package/src/modules/business_rules/notifications.ts +25 -0
  165. package/src/modules/business_rules/subscribers/rule-execution-failed-notification.ts +50 -0
  166. package/src/modules/catalog/i18n/en.json +3 -1
  167. package/src/modules/catalog/notifications.ts +25 -0
  168. package/src/modules/catalog/subscribers/low-stock-notification.ts +52 -0
  169. package/src/modules/configs/cli.ts +6 -0
  170. package/src/modules/customers/commands/deals.ts +39 -0
  171. package/src/modules/customers/i18n/en.json +5 -1
  172. package/src/modules/customers/notifications.ts +44 -0
  173. package/src/modules/notifications/acl.ts +7 -0
  174. package/src/modules/notifications/api/[id]/action/route.ts +75 -0
  175. package/src/modules/notifications/api/[id]/dismiss/route.ts +12 -0
  176. package/src/modules/notifications/api/[id]/read/route.ts +12 -0
  177. package/src/modules/notifications/api/[id]/restore/route.ts +53 -0
  178. package/src/modules/notifications/api/batch/route.ts +14 -0
  179. package/src/modules/notifications/api/feature/route.ts +14 -0
  180. package/src/modules/notifications/api/mark-all-read/route.ts +34 -0
  181. package/src/modules/notifications/api/openapi.ts +76 -0
  182. package/src/modules/notifications/api/role/route.ts +14 -0
  183. package/src/modules/notifications/api/route.ts +92 -0
  184. package/src/modules/notifications/api/settings/route.ts +157 -0
  185. package/src/modules/notifications/api/unread-count/route.ts +38 -0
  186. package/src/modules/notifications/backend/config/notifications/page.meta.ts +22 -0
  187. package/src/modules/notifications/backend/config/notifications/page.tsx +12 -0
  188. package/src/modules/notifications/cli.ts +18 -0
  189. package/src/modules/notifications/data/entities.ts +99 -0
  190. package/src/modules/notifications/data/validators.ts +110 -0
  191. package/src/modules/notifications/di.ts +11 -0
  192. package/src/modules/notifications/emails/NotificationEmail.tsx +98 -0
  193. package/src/modules/notifications/frontend/NotificationInboxPageClient.tsx +42 -0
  194. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +231 -0
  195. package/src/modules/notifications/i18n/de.json +50 -0
  196. package/src/modules/notifications/i18n/en.json +50 -0
  197. package/src/modules/notifications/i18n/es.json +50 -0
  198. package/src/modules/notifications/i18n/pl.json +50 -0
  199. package/src/modules/notifications/index.ts +12 -0
  200. package/src/modules/notifications/lib/deliveryConfig.ts +145 -0
  201. package/src/modules/notifications/lib/events.ts +48 -0
  202. package/src/modules/notifications/lib/notificationBuilder.ts +121 -0
  203. package/src/modules/notifications/lib/notificationFactory.ts +76 -0
  204. package/src/modules/notifications/lib/notificationMapper.ts +33 -0
  205. package/src/modules/notifications/lib/notificationRecipients.ts +83 -0
  206. package/src/modules/notifications/lib/notificationService.ts +414 -0
  207. package/src/modules/notifications/lib/routeHelpers.ts +151 -0
  208. package/src/modules/notifications/lib/safeHref.ts +29 -0
  209. package/src/modules/notifications/migrations/.snapshot-open-mercato.json +300 -0
  210. package/src/modules/notifications/migrations/Migration20260123000001.ts +73 -0
  211. package/src/modules/notifications/migrations/Migration20260126150000.ts +39 -0
  212. package/src/modules/notifications/subscribers/deliver-notification.ts +175 -0
  213. package/src/modules/notifications/workers/create-notification.worker.ts +122 -0
  214. package/src/modules/sales/commands/documents.ts +65 -0
  215. package/src/modules/sales/commands/payments.ts +33 -0
  216. package/src/modules/sales/i18n/de.json +20 -0
  217. package/src/modules/sales/i18n/en.json +25 -1
  218. package/src/modules/sales/i18n/es.json +20 -0
  219. package/src/modules/sales/i18n/pl.json +20 -0
  220. package/src/modules/sales/notifications.client.ts +65 -0
  221. package/src/modules/sales/notifications.ts +82 -0
  222. package/src/modules/sales/subscribers/quote-expiring-notification.ts +53 -0
  223. package/src/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.tsx +156 -0
  224. package/src/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.tsx +156 -0
  225. package/src/modules/sales/widgets/notifications/index.ts +2 -0
  226. package/src/modules/sales/widgets/notifications/useSalesDocumentTotals.ts +81 -0
  227. package/src/modules/staff/commands/leave-requests.ts +94 -0
  228. package/src/modules/staff/i18n/de.json +4 -0
  229. package/src/modules/staff/i18n/en.json +9 -1
  230. package/src/modules/staff/i18n/es.json +4 -0
  231. package/src/modules/staff/i18n/pl.json +4 -0
  232. package/src/modules/staff/notifications.ts +71 -0
  233. package/src/modules/workflows/i18n/en.json +3 -1
  234. package/src/modules/workflows/notifications.ts +25 -0
  235. package/src/modules/workflows/subscribers/task-assigned-notification.ts +53 -0
@@ -0,0 +1,127 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { useRouter } from 'next/navigation'
4
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
+ import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
6
+ import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
7
+ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
8
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
+ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
10
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
11
+
12
+ type ProfileResponse = {
13
+ email?: string | null
14
+ }
15
+
16
+ type ProfileUpdateResponse = {
17
+ ok?: boolean
18
+ email?: string | null
19
+ }
20
+
21
+ type ProfileFormValues = {
22
+ email: string
23
+ password: string
24
+ confirmPassword: string
25
+ }
26
+
27
+ export default function AuthProfilePage() {
28
+ const t = useT()
29
+ const router = useRouter()
30
+ const [loading, setLoading] = React.useState(true)
31
+ const [error, setError] = React.useState<string | null>(null)
32
+ const [email, setEmail] = React.useState('')
33
+ const [formKey, setFormKey] = React.useState(0)
34
+
35
+ React.useEffect(() => {
36
+ let cancelled = false
37
+ async function load() {
38
+ setLoading(true)
39
+ setError(null)
40
+ try {
41
+ const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')
42
+ if (!ok) throw new Error('load_failed')
43
+ const resolvedEmail = typeof result?.email === 'string' ? result.email : ''
44
+ if (!cancelled) setEmail(resolvedEmail)
45
+ } catch (err) {
46
+ console.error('Failed to load auth profile', err)
47
+ if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))
48
+ } finally {
49
+ if (!cancelled) setLoading(false)
50
+ }
51
+ }
52
+ load()
53
+ return () => { cancelled = true }
54
+ }, [t])
55
+
56
+ const fields = React.useMemo<CrudField[]>(() => [
57
+ { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },
58
+ { id: 'password', label: t('auth.profile.form.password', 'New password'), type: 'text' },
59
+ { id: 'confirmPassword', label: t('auth.profile.form.confirmPassword', 'Confirm new password'), type: 'text' },
60
+ ], [t])
61
+
62
+ const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {
63
+ const nextEmail = values.email?.trim() ?? ''
64
+ const password = values.password?.trim() ?? ''
65
+ const confirmPassword = values.confirmPassword?.trim() ?? ''
66
+
67
+ if (!nextEmail) {
68
+ const message = t('auth.profile.form.errors.emailRequired', 'Email is required.')
69
+ throw createCrudFormError(message, { email: message })
70
+ }
71
+
72
+ if (password || confirmPassword) {
73
+ if (password !== confirmPassword) {
74
+ const message = t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.')
75
+ throw createCrudFormError(message, { confirmPassword: message })
76
+ }
77
+ }
78
+
79
+ if (!password && nextEmail === email) {
80
+ throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))
81
+ }
82
+
83
+ const payload: { email: string; password?: string } = { email: nextEmail }
84
+ if (password) payload.password = password
85
+
86
+ const result = await readApiResultOrThrow<ProfileUpdateResponse>(
87
+ '/api/auth/profile',
88
+ {
89
+ method: 'PUT',
90
+ headers: { 'content-type': 'application/json' },
91
+ body: JSON.stringify(payload),
92
+ },
93
+ { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },
94
+ )
95
+
96
+ const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail
97
+ setEmail(resolvedEmail)
98
+ setFormKey((prev) => prev + 1)
99
+ flash(t('auth.profile.form.success', 'Profile updated.'), 'success')
100
+ router.refresh()
101
+ }, [email, router, t])
102
+
103
+ return (
104
+ <Page>
105
+ <PageBody>
106
+ {loading ? (
107
+ <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />
108
+ ) : error ? (
109
+ <ErrorMessage label={error} />
110
+ ) : (
111
+ <CrudForm<ProfileFormValues>
112
+ key={formKey}
113
+ title={t('auth.profile.title', 'Profile')}
114
+ fields={fields}
115
+ initialValues={{
116
+ email,
117
+ password: '',
118
+ confirmPassword: '',
119
+ }}
120
+ submitLabel={t('auth.profile.form.save', 'Save changes')}
121
+ onSubmit={handleSubmit}
122
+ />
123
+ )}
124
+ </PageBody>
125
+ </Page>
126
+ )
127
+ }
@@ -27,6 +27,9 @@ import {
27
27
  import { normalizeTenantId } from '@open-mercato/core/modules/auth/lib/tenantAccess'
28
28
  import { computeEmailHash } from '@open-mercato/core/modules/auth/lib/emailHash'
29
29
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
30
+ import { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'
31
+ import { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'
32
+ import notificationTypes from '@open-mercato/core/modules/auth/notifications'
30
33
 
31
34
  type SerializedUser = {
32
35
  email: string
@@ -105,6 +108,46 @@ export const userCrudIndexer: CrudIndexerConfig = {
105
108
  }),
106
109
  }
107
110
 
111
+ async function notifyRoleChanges(
112
+ ctx: CommandRuntimeContext,
113
+ user: User,
114
+ assignedRoles: string[],
115
+ revokedRoles: string[],
116
+ ): Promise<void> {
117
+ const tenantId = user.tenantId ? String(user.tenantId) : null
118
+ if (!tenantId) return
119
+ const organizationId = user.organizationId ? String(user.organizationId) : null
120
+
121
+ try {
122
+ const notificationService = resolveNotificationService(ctx.container)
123
+ if (assignedRoles.length) {
124
+ const assignedType = notificationTypes.find((type) => type.type === 'auth.role.assigned')
125
+ if (assignedType) {
126
+ const notificationInput = buildNotificationFromType(assignedType, {
127
+ recipientUserId: String(user.id),
128
+ sourceEntityType: 'auth:user',
129
+ sourceEntityId: String(user.id),
130
+ })
131
+ await notificationService.create(notificationInput, { tenantId, organizationId })
132
+ }
133
+ }
134
+
135
+ if (revokedRoles.length) {
136
+ const revokedType = notificationTypes.find((type) => type.type === 'auth.role.revoked')
137
+ if (revokedType) {
138
+ const notificationInput = buildNotificationFromType(revokedType, {
139
+ recipientUserId: String(user.id),
140
+ sourceEntityType: 'auth:user',
141
+ sourceEntityId: String(user.id),
142
+ })
143
+ await notificationService.create(notificationInput, { tenantId, organizationId })
144
+ }
145
+ }
146
+ } catch (err) {
147
+ console.error('[auth.users.roles] Failed to create notification:', err)
148
+ }
149
+ }
150
+
108
151
  const createUserCommand: CommandHandler<Record<string, unknown>, User> = {
109
152
  id: 'auth.users.create',
110
153
  async execute(rawInput, ctx) {
@@ -147,8 +190,10 @@ const createUserCommand: CommandHandler<Record<string, unknown>, User> = {
147
190
  throw error
148
191
  }
149
192
 
193
+ let assignedRoles: string[] = []
150
194
  if (Array.isArray(parsed.roles) && parsed.roles.length) {
151
195
  await syncUserRoles(em, user, parsed.roles, tenantId)
196
+ assignedRoles = await loadUserRoleNames(em, String(user.id))
152
197
  }
153
198
 
154
199
  await setCustomFieldsIfAny({
@@ -173,6 +218,10 @@ const createUserCommand: CommandHandler<Record<string, unknown>, User> = {
173
218
  indexer: userCrudIndexer,
174
219
  })
175
220
 
221
+ if (assignedRoles.length) {
222
+ await notifyRoleChanges(ctx, user, assignedRoles, [])
223
+ }
224
+
176
225
  return user
177
226
  },
178
227
  captureAfter: async (_input, result, ctx) => {
@@ -288,6 +337,9 @@ const updateUserCommand: CommandHandler<Record<string, unknown>, User> = {
288
337
  async execute(rawInput, ctx) {
289
338
  const { parsed, custom } = parseWithCustomFields(updateSchema, rawInput)
290
339
  const em = (ctx.container.resolve('em') as EntityManager)
340
+ const rolesBefore = Array.isArray(parsed.roles)
341
+ ? await loadUserRoleNames(em, parsed.id)
342
+ : null
291
343
 
292
344
  if (parsed.email !== undefined) {
293
345
  const emailHash = computeEmailHash(parsed.email)
@@ -377,6 +429,14 @@ const updateUserCommand: CommandHandler<Record<string, unknown>, User> = {
377
429
  indexer: userCrudIndexer,
378
430
  })
379
431
 
432
+ if (Array.isArray(parsed.roles) && rolesBefore) {
433
+ const rolesAfter = await loadUserRoleNames(em, String(user.id))
434
+ const { assigned, revoked } = diffRoleChanges(rolesBefore, rolesAfter)
435
+ if (assigned.length || revoked.length) {
436
+ await notifyRoleChanges(ctx, user, assigned, revoked)
437
+ }
438
+ }
439
+
380
440
  await invalidateUserCache(ctx, parsed.id)
381
441
 
382
442
  return user
@@ -772,6 +832,14 @@ async function invalidateUserCache(ctx: CommandRuntimeContext, userId: string) {
772
832
  }
773
833
  }
774
834
 
835
+ function diffRoleChanges(before: string[], after: string[]) {
836
+ const beforeSet = new Set(before)
837
+ const afterSet = new Set(after)
838
+ const assigned = after.filter((role) => !beforeSet.has(role))
839
+ const revoked = before.filter((role) => !afterSet.has(role))
840
+ return { assigned, revoked }
841
+ }
842
+
775
843
  function arrayEquals(left: string[] | undefined, right: string[]): boolean {
776
844
  if (!left) return false
777
845
  if (left.length !== right.length) return false
@@ -80,6 +80,19 @@
80
80
  "auth.users.form.errors.load": "Benutzerdaten konnten nicht geladen werden",
81
81
  "auth.users.form.errors.aclUpdate": "Aktualisierung der Benutzerberechtigungen fehlgeschlagen",
82
82
  "auth.users.form.errors.delete": "Benutzer konnte nicht gelöscht werden",
83
+ "auth.profile.title": "Profil",
84
+ "auth.profile.form.email": "E-Mail",
85
+ "auth.profile.form.password": "Neues Passwort",
86
+ "auth.profile.form.confirmPassword": "Neues Passwort bestätigen",
87
+ "auth.profile.form.save": "Änderungen speichern",
88
+ "auth.profile.form.loading": "Profil wird geladen...",
89
+ "auth.profile.form.errors.load": "Profil konnte nicht geladen werden.",
90
+ "auth.profile.form.errors.save": "Profil konnte nicht aktualisiert werden.",
91
+ "auth.profile.form.errors.invalid": "Ungültige Profilaktualisierung.",
92
+ "auth.profile.form.errors.passwordMismatch": "Die Passwörter stimmen nicht überein.",
93
+ "auth.profile.form.errors.noChanges": "Keine Änderungen zu speichern.",
94
+ "auth.profile.form.errors.emailRequired": "E-Mail ist erforderlich.",
95
+ "auth.profile.form.success": "Profil aktualisiert.",
83
96
  "auth.users.list.error.load": "Benutzer konnten nicht geladen werden",
84
97
  "auth.users.list.error.delete": "Benutzer konnte nicht gelöscht werden",
85
98
  "auth.users.flash.created": "Benutzer erstellt",
@@ -95,5 +108,20 @@
95
108
  "auth.email.resetPassword.title": "Passwort zurücksetzen",
96
109
  "auth.email.resetPassword.body": "Klicken Sie auf den Link unten, um ein neues Passwort festzulegen. Dieser Link läuft in 60 Minuten ab.",
97
110
  "auth.email.resetPassword.cta": "Neues Passwort festlegen",
98
- "auth.email.resetPassword.hint": "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren."
111
+ "auth.email.resetPassword.hint": "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren.",
112
+ "auth.notifications.passwordReset.requested.title": "Passwort-Zurücksetzung angefordert",
113
+ "auth.notifications.passwordReset.requested.body": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail gesendet",
114
+ "auth.notifications.passwordReset.completed.title": "Passwort erfolgreich geändert",
115
+ "auth.notifications.passwordReset.completed.body": "Ihr Passwort wurde erfolgreich aktualisiert",
116
+ "auth.notifications.account.locked.title": "Konto gesperrt",
117
+ "auth.notifications.account.locked.body": "Ihr Konto wurde aus Sicherheitsgründen gesperrt. Bitte wenden Sie sich an den Support.",
118
+ "auth.notifications.login.newDevice.title": "Neues Gerät erkannt",
119
+ "auth.notifications.login.newDevice.body": "Es wurde eine Anmeldung von einem unbekannten Gerät für Ihr Konto erkannt",
120
+ "auth.notifications.role.assigned.title": "Neue Rolle zugewiesen",
121
+ "auth.notifications.role.assigned.body": "Ihnen wurde eine neue Rolle mit zusätzlichen Berechtigungen zugewiesen",
122
+ "auth.notifications.role.revoked.title": "Rolle entfernt",
123
+ "auth.notifications.role.revoked.body": "Eine Rolle wurde von Ihrem Konto entfernt",
124
+ "auth.actions.contactSupport": "Support kontaktieren",
125
+ "auth.actions.viewSessions": "Sitzungen anzeigen",
126
+ "auth.actions.viewPermissions": "Berechtigungen anzeigen"
99
127
  }
@@ -80,6 +80,19 @@
80
80
  "auth.users.form.errors.load": "Failed to load user data",
81
81
  "auth.users.form.errors.aclUpdate": "Failed to update user access control",
82
82
  "auth.users.form.errors.delete": "Failed to delete user",
83
+ "auth.profile.title": "Profile",
84
+ "auth.profile.form.email": "Email",
85
+ "auth.profile.form.password": "New password",
86
+ "auth.profile.form.confirmPassword": "Confirm new password",
87
+ "auth.profile.form.save": "Save changes",
88
+ "auth.profile.form.loading": "Loading profile...",
89
+ "auth.profile.form.errors.load": "Failed to load profile.",
90
+ "auth.profile.form.errors.save": "Failed to update profile.",
91
+ "auth.profile.form.errors.invalid": "Invalid profile update.",
92
+ "auth.profile.form.errors.passwordMismatch": "Passwords do not match.",
93
+ "auth.profile.form.errors.noChanges": "No changes to save.",
94
+ "auth.profile.form.errors.emailRequired": "Email is required.",
95
+ "auth.profile.form.success": "Profile updated.",
83
96
  "auth.users.list.error.load": "Failed to load users",
84
97
  "auth.users.list.error.delete": "Failed to delete user",
85
98
  "auth.users.flash.created": "User created",
@@ -95,5 +108,20 @@
95
108
  "auth.email.resetPassword.title": "Reset your password",
96
109
  "auth.email.resetPassword.body": "Click the link below to set a new password. This link will expire in 60 minutes.",
97
110
  "auth.email.resetPassword.cta": "Set a new password",
98
- "auth.email.resetPassword.hint": "If you didn't request this, you can safely ignore this email."
111
+ "auth.email.resetPassword.hint": "If you didn't request this, you can safely ignore this email.",
112
+ "auth.notifications.passwordReset.requested.title": "Password reset requested",
113
+ "auth.notifications.passwordReset.requested.body": "A password reset link has been sent to your email",
114
+ "auth.notifications.passwordReset.completed.title": "Password successfully changed",
115
+ "auth.notifications.passwordReset.completed.body": "Your password has been updated successfully",
116
+ "auth.notifications.account.locked.title": "Account locked",
117
+ "auth.notifications.account.locked.body": "Your account has been locked due to security reasons. Please contact support.",
118
+ "auth.notifications.login.newDevice.title": "New device login detected",
119
+ "auth.notifications.login.newDevice.body": "A new login from an unrecognized device was detected on your account",
120
+ "auth.notifications.role.assigned.title": "New role assigned",
121
+ "auth.notifications.role.assigned.body": "You have been assigned a new role with additional permissions",
122
+ "auth.notifications.role.revoked.title": "Role removed",
123
+ "auth.notifications.role.revoked.body": "A role has been removed from your account",
124
+ "auth.actions.contactSupport": "Contact Support",
125
+ "auth.actions.viewSessions": "View Sessions",
126
+ "auth.actions.viewPermissions": "View Permissions"
99
127
  }
@@ -80,6 +80,19 @@
80
80
  "auth.users.form.errors.load": "No se pudieron cargar los datos del usuario",
81
81
  "auth.users.form.errors.aclUpdate": "No se pudo actualizar el control de acceso del usuario",
82
82
  "auth.users.form.errors.delete": "No se pudo eliminar el usuario",
83
+ "auth.profile.title": "Perfil",
84
+ "auth.profile.form.email": "Correo electrónico",
85
+ "auth.profile.form.password": "Nueva contraseña",
86
+ "auth.profile.form.confirmPassword": "Confirmar nueva contraseña",
87
+ "auth.profile.form.save": "Guardar cambios",
88
+ "auth.profile.form.loading": "Cargando perfil...",
89
+ "auth.profile.form.errors.load": "No se pudo cargar el perfil.",
90
+ "auth.profile.form.errors.save": "No se pudo actualizar el perfil.",
91
+ "auth.profile.form.errors.invalid": "Actualización de perfil inválida.",
92
+ "auth.profile.form.errors.passwordMismatch": "Las contraseñas no coinciden.",
93
+ "auth.profile.form.errors.noChanges": "No hay cambios para guardar.",
94
+ "auth.profile.form.errors.emailRequired": "El correo electrónico es obligatorio.",
95
+ "auth.profile.form.success": "Perfil actualizado.",
83
96
  "auth.users.list.error.load": "No se pudieron cargar los usuarios",
84
97
  "auth.users.list.error.delete": "No se pudo eliminar el usuario",
85
98
  "auth.users.flash.created": "Usuario creado",
@@ -95,5 +108,20 @@
95
108
  "auth.email.resetPassword.title": "Restablecer tu contraseña",
96
109
  "auth.email.resetPassword.body": "Haz clic en el siguiente enlace para establecer una nueva contraseña. Este enlace caducará en 60 minutos.",
97
110
  "auth.email.resetPassword.cta": "Establecer nueva contraseña",
98
- "auth.email.resetPassword.hint": "Si no solicitaste esto, puedes ignorar este correo de forma segura."
111
+ "auth.email.resetPassword.hint": "Si no solicitaste esto, puedes ignorar este correo de forma segura.",
112
+ "auth.notifications.passwordReset.requested.title": "Solicitud de restablecimiento de contraseña",
113
+ "auth.notifications.passwordReset.requested.body": "Se ha enviado un enlace de restablecimiento de contraseña a tu correo electrónico",
114
+ "auth.notifications.passwordReset.completed.title": "Contraseña cambiada correctamente",
115
+ "auth.notifications.passwordReset.completed.body": "Tu contraseña se actualizó correctamente",
116
+ "auth.notifications.account.locked.title": "Cuenta bloqueada",
117
+ "auth.notifications.account.locked.body": "Tu cuenta ha sido bloqueada por razones de seguridad. Ponte en contacto con soporte.",
118
+ "auth.notifications.login.newDevice.title": "Nuevo inicio de sesión detectado",
119
+ "auth.notifications.login.newDevice.body": "Se detectó un inicio de sesión desde un dispositivo no reconocido en tu cuenta",
120
+ "auth.notifications.role.assigned.title": "Nuevo rol asignado",
121
+ "auth.notifications.role.assigned.body": "Se te ha asignado un nuevo rol con permisos adicionales",
122
+ "auth.notifications.role.revoked.title": "Rol eliminado",
123
+ "auth.notifications.role.revoked.body": "Se ha eliminado un rol de tu cuenta",
124
+ "auth.actions.contactSupport": "Contactar soporte",
125
+ "auth.actions.viewSessions": "Ver sesiones",
126
+ "auth.actions.viewPermissions": "Ver permisos"
99
127
  }
@@ -80,6 +80,19 @@
80
80
  "auth.users.form.errors.load": "Nie udało się wczytać danych użytkownika",
81
81
  "auth.users.form.errors.aclUpdate": "Nie udało się zaktualizować uprawnień użytkownika",
82
82
  "auth.users.form.errors.delete": "Nie udało się usunąć użytkownika",
83
+ "auth.profile.title": "Profil",
84
+ "auth.profile.form.email": "Email",
85
+ "auth.profile.form.password": "Nowe hasło",
86
+ "auth.profile.form.confirmPassword": "Potwierdź nowe hasło",
87
+ "auth.profile.form.save": "Zapisz zmiany",
88
+ "auth.profile.form.loading": "Ładowanie profilu...",
89
+ "auth.profile.form.errors.load": "Nie udało się wczytać profilu.",
90
+ "auth.profile.form.errors.save": "Nie udało się zaktualizować profilu.",
91
+ "auth.profile.form.errors.invalid": "Nieprawidłowa aktualizacja profilu.",
92
+ "auth.profile.form.errors.passwordMismatch": "Hasła nie są zgodne.",
93
+ "auth.profile.form.errors.noChanges": "Brak zmian do zapisania.",
94
+ "auth.profile.form.errors.emailRequired": "Email jest wymagany.",
95
+ "auth.profile.form.success": "Profil zaktualizowany.",
83
96
  "auth.users.list.error.load": "Nie udało się wczytać użytkowników",
84
97
  "auth.users.list.error.delete": "Nie udało się usunąć użytkownika",
85
98
  "auth.users.flash.created": "Użytkownik utworzony",
@@ -95,5 +108,20 @@
95
108
  "auth.email.resetPassword.title": "Zresetuj swoje hasło",
96
109
  "auth.email.resetPassword.body": "Kliknij poniższy link, aby ustawić nowe hasło. Link wygaśnie za 60 minut.",
97
110
  "auth.email.resetPassword.cta": "Ustaw nowe hasło",
98
- "auth.email.resetPassword.hint": "Jeśli nie prosiłeś o tę wiadomość, możesz ją bezpiecznie zignorować."
111
+ "auth.email.resetPassword.hint": "Jeśli nie prosiłeś o tę wiadomość, możesz ją bezpiecznie zignorować.",
112
+ "auth.notifications.passwordReset.requested.title": "Żądanie resetu hasła",
113
+ "auth.notifications.passwordReset.requested.body": "Link do resetu hasła został wysłany na Twój adres e-mail",
114
+ "auth.notifications.passwordReset.completed.title": "Hasło zmienione pomyślnie",
115
+ "auth.notifications.passwordReset.completed.body": "Twoje hasło zostało pomyślnie zaktualizowane",
116
+ "auth.notifications.account.locked.title": "Konto zablokowane",
117
+ "auth.notifications.account.locked.body": "Twoje konto zostało zablokowane z powodów bezpieczeństwa. Skontaktuj się z pomocą techniczną.",
118
+ "auth.notifications.login.newDevice.title": "Wykryto logowanie z nowego urządzenia",
119
+ "auth.notifications.login.newDevice.body": "Wykryto logowanie z nieznanego urządzenia na Twoim koncie",
120
+ "auth.notifications.role.assigned.title": "Przypisano nową rolę",
121
+ "auth.notifications.role.assigned.body": "Przypisano Ci nową rolę z dodatkowymi uprawnieniami",
122
+ "auth.notifications.role.revoked.title": "Rola usunięta",
123
+ "auth.notifications.role.revoked.body": "Z Twojego konta usunięto rolę",
124
+ "auth.actions.contactSupport": "Skontaktuj się z pomocą",
125
+ "auth.actions.viewSessions": "Zobacz sesje",
126
+ "auth.actions.viewPermissions": "Zobacz uprawnienia"
99
127
  }
@@ -395,6 +395,7 @@ async function ensureDefaultRoleAcls(
395
395
  'dashboards.*',
396
396
  'dashboards.admin.assign-widgets',
397
397
  'api_keys.*',
398
+ 'notifications.manage',
398
399
  'perspectives.use',
399
400
  'perspectives.role_defaults',
400
401
  'business_rules.*',
@@ -0,0 +1,109 @@
1
+ import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
2
+
3
+ export const notificationTypes: NotificationTypeDefinition[] = [
4
+ {
5
+ type: 'auth.password_reset.requested',
6
+ module: 'auth',
7
+ titleKey: 'auth.notifications.passwordReset.requested.title',
8
+ bodyKey: 'auth.notifications.passwordReset.requested.body',
9
+ icon: 'key',
10
+ severity: 'info',
11
+ actions: [
12
+ {
13
+ id: 'view',
14
+ labelKey: 'common.view',
15
+ variant: 'outline',
16
+ href: '/backend/auth/profile',
17
+ icon: 'external-link',
18
+ },
19
+ ],
20
+ linkHref: '/backend/auth/profile',
21
+ expiresAfterHours: 24,
22
+ },
23
+ {
24
+ type: 'auth.password_reset.completed',
25
+ module: 'auth',
26
+ titleKey: 'auth.notifications.passwordReset.completed.title',
27
+ bodyKey: 'auth.notifications.passwordReset.completed.body',
28
+ icon: 'check-circle',
29
+ severity: 'success',
30
+ actions: [],
31
+ expiresAfterHours: 72,
32
+ },
33
+ {
34
+ type: 'auth.account.locked',
35
+ module: 'auth',
36
+ titleKey: 'auth.notifications.account.locked.title',
37
+ bodyKey: 'auth.notifications.account.locked.body',
38
+ icon: 'lock',
39
+ severity: 'warning',
40
+ actions: [
41
+ {
42
+ id: 'contact_support',
43
+ labelKey: 'auth.actions.contactSupport',
44
+ variant: 'default',
45
+ href: '/backend/support',
46
+ icon: 'mail',
47
+ },
48
+ ],
49
+ linkHref: '/backend/support',
50
+ },
51
+ {
52
+ type: 'auth.login.new_device',
53
+ module: 'auth',
54
+ titleKey: 'auth.notifications.login.newDevice.title',
55
+ bodyKey: 'auth.notifications.login.newDevice.body',
56
+ icon: 'smartphone',
57
+ severity: 'info',
58
+ actions: [
59
+ {
60
+ id: 'view_sessions',
61
+ labelKey: 'auth.actions.viewSessions',
62
+ variant: 'outline',
63
+ href: '/backend/auth/sessions',
64
+ icon: 'list',
65
+ },
66
+ ],
67
+ linkHref: '/backend/auth/sessions',
68
+ expiresAfterHours: 168, // 7 days
69
+ },
70
+ {
71
+ type: 'auth.role.assigned',
72
+ module: 'auth',
73
+ titleKey: 'auth.notifications.role.assigned.title',
74
+ bodyKey: 'auth.notifications.role.assigned.body',
75
+ icon: 'user-plus',
76
+ severity: 'success',
77
+ actions: [
78
+ {
79
+ id: 'view_permissions',
80
+ labelKey: 'auth.actions.viewPermissions',
81
+ variant: 'outline',
82
+ href: '/backend/auth/profile',
83
+ icon: 'shield',
84
+ },
85
+ ],
86
+ linkHref: '/backend/auth/profile',
87
+ expiresAfterHours: 168,
88
+ },
89
+ {
90
+ type: 'auth.role.revoked',
91
+ module: 'auth',
92
+ titleKey: 'auth.notifications.role.revoked.title',
93
+ bodyKey: 'auth.notifications.role.revoked.body',
94
+ icon: 'user-minus',
95
+ severity: 'warning',
96
+ actions: [
97
+ {
98
+ id: 'view_profile',
99
+ labelKey: 'common.view',
100
+ variant: 'outline',
101
+ href: '/backend/auth/profile',
102
+ },
103
+ ],
104
+ linkHref: '/backend/auth/profile',
105
+ expiresAfterHours: 168,
106
+ },
107
+ ]
108
+
109
+ export default notificationTypes
@@ -75,15 +75,15 @@ export class AuthService {
75
75
  return { user, token }
76
76
  }
77
77
 
78
- async confirmPasswordReset(token: string, newPassword: string) {
78
+ async confirmPasswordReset(token: string, newPassword: string): Promise<User | null> {
79
79
  const now = new Date()
80
80
  const row = await this.em.findOne(PasswordReset, { token })
81
- if (!row || (row.usedAt && row.usedAt <= now) || row.expiresAt <= now) return false
81
+ if (!row || (row.usedAt && row.usedAt <= now) || row.expiresAt <= now) return null
82
82
  const user = await this.em.findOne(User, { id: row.user.id })
83
- if (!user) return false
83
+ if (!user) return null
84
84
  user.passwordHash = await hash(newPassword, 10)
85
85
  row.usedAt = new Date()
86
86
  await this.em.flush()
87
- return true
87
+ return user
88
88
  }
89
89
  }
@@ -367,5 +367,7 @@
367
367
  "business_rules.components.conditionRow.field.comparisonPlaceholder": "e.g., user.role",
368
368
  "business_rules.components.conditionRow.value.help": "Use JSON for arrays: [\"a\",\"b\"]",
369
369
  "business_rules.components.conditionRow.field.comparisonHelp": "Field path to compare with",
370
- "business_rules.components.conditionRow.deleteCondition": "Delete condition"
370
+ "business_rules.components.conditionRow.deleteCondition": "Delete condition",
371
+ "businessRules.notifications.rule.executionFailed.title": "Business Rule Failed",
372
+ "businessRules.notifications.rule.executionFailed.body": "Rule \"{ruleName}\" failed to execute{entityType, select, other { on {entityType}}}: {errorMessage}"
371
373
  }
@@ -0,0 +1,25 @@
1
+ import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
2
+
3
+ export const notificationTypes: NotificationTypeDefinition[] = [
4
+ {
5
+ type: 'business_rules.rule.execution_failed',
6
+ module: 'business_rules',
7
+ titleKey: 'businessRules.notifications.rule.executionFailed.title',
8
+ bodyKey: 'businessRules.notifications.rule.executionFailed.body',
9
+ icon: 'alert-triangle',
10
+ severity: 'error',
11
+ actions: [
12
+ {
13
+ id: 'view',
14
+ labelKey: 'common.view',
15
+ variant: 'outline',
16
+ href: '/backend/business-rules/{sourceEntityId}',
17
+ icon: 'external-link',
18
+ },
19
+ ],
20
+ linkHref: '/backend/business-rules/{sourceEntityId}',
21
+ expiresAfterHours: 168, // 7 days
22
+ },
23
+ ]
24
+
25
+ export default notificationTypes
@@ -0,0 +1,50 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { resolveNotificationService } from '../../notifications/lib/notificationService'
3
+ import { buildFeatureNotificationFromType } from '../../notifications/lib/notificationBuilder'
4
+ import { notificationTypes } from '../notifications'
5
+
6
+ export const metadata = {
7
+ event: 'business_rules.rule.execution_failed',
8
+ persistent: true,
9
+ id: 'business_rules:rule-execution-failed-notification',
10
+ }
11
+
12
+ type RuleExecutionFailedPayload = {
13
+ ruleId: string
14
+ ruleName: string
15
+ entityType?: string | null
16
+ errorMessage?: string | null
17
+ tenantId: string
18
+ organizationId?: string | null
19
+ }
20
+
21
+ type ResolverContext = {
22
+ resolve: <T = unknown>(name: string) => T
23
+ }
24
+
25
+ export default async function handle(payload: RuleExecutionFailedPayload, ctx: ResolverContext) {
26
+ try {
27
+ const notificationService = resolveNotificationService(ctx)
28
+ const typeDef = notificationTypes.find((type) => type.type === 'business_rules.rule.execution_failed')
29
+ if (!typeDef) return
30
+
31
+ const notificationInput = buildFeatureNotificationFromType(typeDef, {
32
+ requiredFeature: 'business_rules.manage',
33
+ bodyVariables: {
34
+ ruleName: payload.ruleName,
35
+ entityType: payload.entityType ?? '',
36
+ errorMessage: payload.errorMessage ?? 'Unknown error',
37
+ },
38
+ sourceEntityType: 'business_rules:rule',
39
+ sourceEntityId: payload.ruleId,
40
+ linkHref: `/backend/business-rules/${payload.ruleId}`,
41
+ })
42
+
43
+ await notificationService.createForFeature(notificationInput, {
44
+ tenantId: payload.tenantId,
45
+ organizationId: payload.organizationId ?? null,
46
+ })
47
+ } catch (err) {
48
+ console.error('[business_rules:rule-execution-failed-notification] Failed to create notification:', err)
49
+ }
50
+ }
@@ -681,5 +681,7 @@
681
681
  "deleteError": "Failed to delete variant."
682
682
  }
683
683
  }
684
- }
684
+ },
685
+ "catalog.notifications.product.lowStock.title": "Low Stock Alert",
686
+ "catalog.notifications.product.lowStock.body": "{productName}{sku, select, other { ({sku})}} is running low on stock ({currentStock} remaining, threshold: {threshold})"
685
687
  }