@open-mercato/core 0.4.2-canary-36ab8921da → 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,157 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
+ import { notificationDeliveryConfigSchema } from '../../data/validators'
6
+ import {
7
+ errorResponseSchema,
8
+ notificationSettingsResponseSchema,
9
+ notificationSettingsUpdateResponseSchema,
10
+ } from '../openapi'
11
+ import {
12
+ DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
13
+ resolveNotificationDeliveryConfig,
14
+ saveNotificationDeliveryConfig,
15
+ } from '../../lib/deliveryConfig'
16
+
17
+ export const metadata = {
18
+ GET: { requireAuth: true, requireFeatures: ['notifications.manage'] },
19
+ POST: { requireAuth: true, requireFeatures: ['notifications.manage'] },
20
+ }
21
+
22
+ const unauthorized = async () => {
23
+ const { t } = await resolveTranslations()
24
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
25
+ }
26
+
27
+ export async function GET(req: Request) {
28
+ const auth = await getAuthFromRequest(req)
29
+ if (!auth?.sub) return await unauthorized()
30
+
31
+ const container = await createRequestContainer()
32
+ try {
33
+ const settings = await resolveNotificationDeliveryConfig(container, {
34
+ defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
35
+ })
36
+ return NextResponse.json({ settings })
37
+ } finally {
38
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
39
+ if (typeof disposable.dispose === 'function') {
40
+ await disposable.dispose()
41
+ }
42
+ }
43
+ }
44
+
45
+ export async function POST(req: Request) {
46
+ const { t } = await resolveTranslations()
47
+ const auth = await getAuthFromRequest(req)
48
+ if (!auth?.sub) return await unauthorized()
49
+
50
+ let body: unknown
51
+ try {
52
+ body = await req.json()
53
+ } catch {
54
+ return NextResponse.json(
55
+ { error: t('api.errors.invalidPayload', 'Invalid request body') },
56
+ { status: 400 }
57
+ )
58
+ }
59
+
60
+ const parsed = notificationDeliveryConfigSchema.safeParse(body)
61
+ if (!parsed.success) {
62
+ return NextResponse.json(
63
+ { error: t('notifications.delivery.settings.invalid', 'Invalid delivery settings') },
64
+ { status: 400 }
65
+ )
66
+ }
67
+
68
+ const container = await createRequestContainer()
69
+ try {
70
+ await saveNotificationDeliveryConfig(container, parsed.data)
71
+ const settings = await resolveNotificationDeliveryConfig(container, {
72
+ defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
73
+ })
74
+ return NextResponse.json({ ok: true, settings })
75
+ } catch (error) {
76
+ return NextResponse.json(
77
+ { error: error instanceof Error ? error.message : t('api.errors.internal', 'Internal error') },
78
+ { status: 500 }
79
+ )
80
+ } finally {
81
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
82
+ if (typeof disposable.dispose === 'function') {
83
+ await disposable.dispose()
84
+ }
85
+ }
86
+ }
87
+
88
+ export const openApi = {
89
+ GET: {
90
+ summary: 'Get notification delivery settings',
91
+ tags: ['Notifications'],
92
+ responses: {
93
+ 200: {
94
+ description: 'Current delivery settings',
95
+ content: {
96
+ 'application/json': {
97
+ schema: notificationSettingsResponseSchema,
98
+ },
99
+ },
100
+ },
101
+ 401: {
102
+ description: 'Unauthorized',
103
+ content: {
104
+ 'application/json': {
105
+ schema: errorResponseSchema,
106
+ },
107
+ },
108
+ },
109
+ },
110
+ },
111
+ POST: {
112
+ summary: 'Update notification delivery settings',
113
+ tags: ['Notifications'],
114
+ requestBody: {
115
+ required: true,
116
+ content: {
117
+ 'application/json': {
118
+ schema: notificationDeliveryConfigSchema,
119
+ },
120
+ },
121
+ },
122
+ responses: {
123
+ 200: {
124
+ description: 'Delivery settings updated',
125
+ content: {
126
+ 'application/json': {
127
+ schema: notificationSettingsUpdateResponseSchema,
128
+ },
129
+ },
130
+ },
131
+ 400: {
132
+ description: 'Invalid request body',
133
+ content: {
134
+ 'application/json': {
135
+ schema: errorResponseSchema,
136
+ },
137
+ },
138
+ },
139
+ 401: {
140
+ description: 'Unauthorized',
141
+ content: {
142
+ 'application/json': {
143
+ schema: errorResponseSchema,
144
+ },
145
+ },
146
+ },
147
+ 500: {
148
+ description: 'Internal error',
149
+ content: {
150
+ 'application/json': {
151
+ schema: errorResponseSchema,
152
+ },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ }
@@ -0,0 +1,38 @@
1
+ import type { EntityManager } from '@mikro-orm/core'
2
+ import { Notification } from '../../data/entities'
3
+ import { unreadCountResponseSchema } from '../openapi'
4
+ import { resolveNotificationContext } from '../../lib/routeHelpers'
5
+
6
+ export const metadata = {
7
+ GET: { requireAuth: true },
8
+ }
9
+
10
+ export async function GET(req: Request) {
11
+ const { scope, ctx } = await resolveNotificationContext(req)
12
+ const em = ctx.container.resolve('em') as EntityManager
13
+
14
+ const count = await em.count(Notification, {
15
+ recipientUserId: scope.userId,
16
+ tenantId: scope.tenantId,
17
+ status: 'unread',
18
+ })
19
+
20
+ return Response.json({ unreadCount: count })
21
+ }
22
+
23
+ export const openApi = {
24
+ GET: {
25
+ summary: 'Get unread notification count',
26
+ tags: ['Notifications'],
27
+ responses: {
28
+ 200: {
29
+ description: 'Unread count',
30
+ content: {
31
+ 'application/json': {
32
+ schema: unreadCountResponseSchema,
33
+ },
34
+ },
35
+ },
36
+ },
37
+ },
38
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react'
2
+
3
+ const bellIcon = React.createElement(
4
+ 'svg',
5
+ { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
6
+ React.createElement('path', { d: 'M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9' }),
7
+ React.createElement('path', { d: 'M13.73 21a2 2 0 0 1-3.46 0' }),
8
+ )
9
+
10
+ export const metadata = {
11
+ requireAuth: true,
12
+ requireFeatures: ['notifications.manage'],
13
+ pageTitle: 'Notification Delivery',
14
+ pageTitleKey: 'notifications.settings.pageTitle',
15
+ pageGroup: 'Configuration',
16
+ pageGroupKey: 'backend.nav.configuration',
17
+ pageOrder: 435,
18
+ icon: bellIcon,
19
+ breadcrumb: [
20
+ { label: 'Notification Delivery', labelKey: 'notifications.settings.pageTitle' },
21
+ ],
22
+ } as const
@@ -0,0 +1,12 @@
1
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
2
+ import { NotificationSettingsPageClient } from '../../../frontend/NotificationSettingsPageClient'
3
+
4
+ export default async function NotificationSettingsPage() {
5
+ return (
6
+ <Page>
7
+ <PageBody>
8
+ <NotificationSettingsPageClient />
9
+ </PageBody>
10
+ </Page>
11
+ )
12
+ }
@@ -0,0 +1,18 @@
1
+ import type { ModuleCli } from '@open-mercato/shared/modules/registry'
2
+ import { createQueue } from '@open-mercato/queue'
3
+ import type { CleanupExpiredJob } from './workers/create-notification.worker'
4
+
5
+ const cleanupExpiredCommand: ModuleCli = {
6
+ command: 'cleanup-expired',
7
+ async run() {
8
+ const queue = createQueue('notifications', 'async')
9
+
10
+ await queue.enqueue({
11
+ type: 'cleanup-expired',
12
+ } satisfies CleanupExpiredJob)
13
+
14
+ console.log('✓ Cleanup job enqueued')
15
+ },
16
+ }
17
+
18
+ export default [cleanupExpiredCommand]
@@ -0,0 +1,99 @@
1
+ import { Entity, PrimaryKey, Property, Index, OptionalProps } from '@mikro-orm/core'
2
+ import type { NotificationActionData } from '@open-mercato/shared/modules/notifications/types'
3
+
4
+ export type NotificationStatus = 'unread' | 'read' | 'actioned' | 'dismissed'
5
+ export type NotificationSeverity = 'info' | 'warning' | 'success' | 'error'
6
+
7
+ @Entity({ tableName: 'notifications' })
8
+ @Index({ name: 'notifications_recipient_status_idx', properties: ['recipientUserId', 'status', 'createdAt'] })
9
+ @Index({ name: 'notifications_source_idx', properties: ['sourceEntityType', 'sourceEntityId'] })
10
+ @Index({ name: 'notifications_tenant_idx', properties: ['tenantId', 'organizationId'] })
11
+ @Index({ name: 'notifications_expires_idx', properties: ['expiresAt'] })
12
+ @Index({ name: 'notifications_group_idx', properties: ['groupKey', 'recipientUserId'] })
13
+ export class Notification {
14
+ [OptionalProps]?: 'status' | 'severity' | 'createdAt'
15
+
16
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
17
+ id!: string
18
+
19
+ @Property({ name: 'recipient_user_id', type: 'uuid' })
20
+ recipientUserId!: string
21
+
22
+ @Property({ name: 'type', type: 'text' })
23
+ type!: string
24
+
25
+ // i18n keys (preferred for i18n-first approach)
26
+ @Property({ name: 'title_key', type: 'text', nullable: true })
27
+ titleKey?: string | null
28
+
29
+ @Property({ name: 'body_key', type: 'text', nullable: true })
30
+ bodyKey?: string | null
31
+
32
+ // Template variables for i18n interpolation (stored as JSONB)
33
+ @Property({ name: 'title_variables', type: 'json', nullable: true })
34
+ titleVariables?: Record<string, string> | null
35
+
36
+ @Property({ name: 'body_variables', type: 'json', nullable: true })
37
+ bodyVariables?: Record<string, string> | null
38
+
39
+ // Fallback text (for backward compatibility or when keys are not available)
40
+ @Property({ name: 'title', type: 'text' })
41
+ title!: string
42
+
43
+ @Property({ name: 'body', type: 'text', nullable: true })
44
+ body?: string | null
45
+
46
+ @Property({ name: 'icon', type: 'text', nullable: true })
47
+ icon?: string | null
48
+
49
+ @Property({ name: 'severity', type: 'text', default: 'info' })
50
+ severity: NotificationSeverity = 'info'
51
+
52
+ @Property({ name: 'status', type: 'text', default: 'unread' })
53
+ status: NotificationStatus = 'unread'
54
+
55
+ @Property({ name: 'action_data', type: 'json', nullable: true })
56
+ actionData?: NotificationActionData | null
57
+
58
+ @Property({ name: 'action_result', type: 'json', nullable: true })
59
+ actionResult?: Record<string, unknown> | null
60
+
61
+ @Property({ name: 'action_taken', type: 'text', nullable: true })
62
+ actionTaken?: string | null
63
+
64
+ @Property({ name: 'source_module', type: 'text', nullable: true })
65
+ sourceModule?: string | null
66
+
67
+ @Property({ name: 'source_entity_type', type: 'text', nullable: true })
68
+ sourceEntityType?: string | null
69
+
70
+ @Property({ name: 'source_entity_id', type: 'uuid', nullable: true })
71
+ sourceEntityId?: string | null
72
+
73
+ @Property({ name: 'link_href', type: 'text', nullable: true })
74
+ linkHref?: string | null
75
+
76
+ @Property({ name: 'group_key', type: 'text', nullable: true })
77
+ groupKey?: string | null
78
+
79
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
80
+ createdAt: Date = new Date()
81
+
82
+ @Property({ name: 'read_at', type: Date, nullable: true })
83
+ readAt?: Date | null
84
+
85
+ @Property({ name: 'actioned_at', type: Date, nullable: true })
86
+ actionedAt?: Date | null
87
+
88
+ @Property({ name: 'dismissed_at', type: Date, nullable: true })
89
+ dismissedAt?: Date | null
90
+
91
+ @Property({ name: 'expires_at', type: Date, nullable: true })
92
+ expiresAt?: Date | null
93
+
94
+ @Property({ name: 'tenant_id', type: 'uuid' })
95
+ tenantId!: string
96
+
97
+ @Property({ name: 'organization_id', type: 'uuid', nullable: true })
98
+ organizationId?: string | null
99
+ }
@@ -0,0 +1,110 @@
1
+ import { z } from 'zod'
2
+ import { isSafeNotificationHref } from '../lib/safeHref'
3
+
4
+ export const notificationStatusSchema = z.enum(['unread', 'read', 'actioned', 'dismissed'])
5
+ export const notificationSeveritySchema = z.enum(['info', 'warning', 'success', 'error'])
6
+
7
+ export const safeRelativeHrefSchema = z.string().min(1).refine(
8
+ (href) => isSafeNotificationHref(href),
9
+ { message: 'Href must be a same-origin relative path starting with /' }
10
+ )
11
+
12
+ export const notificationActionSchema = z.object({
13
+ id: z.string().min(1),
14
+ label: z.string().min(1),
15
+ labelKey: z.string().optional(),
16
+ variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost']).optional(),
17
+ icon: z.string().optional(),
18
+ commandId: z.string().optional(),
19
+ href: safeRelativeHrefSchema.optional(),
20
+ confirmRequired: z.boolean().optional(),
21
+ confirmMessage: z.string().optional(),
22
+ })
23
+
24
+ const baseNotificationFieldsSchema = z.object({
25
+ type: z.string().min(1).max(100),
26
+ titleKey: z.string().min(1).max(200).optional(),
27
+ bodyKey: z.string().min(1).max(200).optional(),
28
+ titleVariables: z.record(z.string(), z.string()).optional(),
29
+ bodyVariables: z.record(z.string(), z.string()).optional(),
30
+ title: z.string().min(1).max(500).optional(),
31
+ body: z.string().max(2000).optional(),
32
+ icon: z.string().max(100).optional(),
33
+ severity: notificationSeveritySchema.optional().default('info'),
34
+ actions: z.array(notificationActionSchema).optional(),
35
+ primaryActionId: z.string().optional(),
36
+ sourceModule: z.string().optional(),
37
+ sourceEntityType: z.string().optional(),
38
+ sourceEntityId: z.string().uuid().optional(),
39
+ linkHref: safeRelativeHrefSchema.optional(),
40
+ groupKey: z.string().optional(),
41
+ expiresAt: z.string().datetime().optional(),
42
+ })
43
+
44
+ const titleRequiredRefinement = {
45
+ refine: (data: { titleKey?: string; title?: string }) => data.titleKey || data.title,
46
+ message: 'Either titleKey or title must be provided',
47
+ } as const
48
+
49
+ export const createNotificationSchema = baseNotificationFieldsSchema
50
+ .extend({ recipientUserId: z.string().uuid() })
51
+ .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })
52
+
53
+ export const createBatchNotificationSchema = baseNotificationFieldsSchema
54
+ .extend({ recipientUserIds: z.array(z.string().uuid()).min(1).max(1000) })
55
+ .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })
56
+
57
+ export const createRoleNotificationSchema = baseNotificationFieldsSchema
58
+ .extend({ roleId: z.string().uuid() })
59
+ .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })
60
+
61
+ export const createFeatureNotificationSchema = baseNotificationFieldsSchema
62
+ .extend({ requiredFeature: z.string().min(1).max(100) })
63
+ .refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })
64
+
65
+ export const listNotificationsSchema = z.object({
66
+ status: z.union([notificationStatusSchema, z.array(notificationStatusSchema)]).optional(),
67
+ type: z.string().optional(),
68
+ severity: notificationSeveritySchema.optional(),
69
+ sourceEntityType: z.string().optional(),
70
+ sourceEntityId: z.string().uuid().optional(),
71
+ since: z.string().datetime().optional(),
72
+ page: z.coerce.number().int().min(1).optional().default(1),
73
+ pageSize: z.coerce.number().int().min(1).max(100).optional().default(20),
74
+ })
75
+
76
+ export const executeActionSchema = z.object({
77
+ actionId: z.string().min(1),
78
+ payload: z.record(z.string(), z.unknown()).optional(),
79
+ })
80
+
81
+ export const restoreNotificationSchema = z.object({
82
+ status: z.enum(['read', 'unread']).optional(),
83
+ })
84
+
85
+ const notificationDeliveryStrategySchema = z.object({
86
+ enabled: z.boolean().optional(),
87
+ })
88
+
89
+ const notificationDeliveryEmailSchema = notificationDeliveryStrategySchema.extend({
90
+ from: z.string().trim().min(1).optional(),
91
+ replyTo: z.string().trim().min(1).optional(),
92
+ subjectPrefix: z.string().trim().min(1).optional(),
93
+ })
94
+
95
+ export const notificationDeliveryConfigSchema = z.object({
96
+ appUrl: z.string().url().optional(),
97
+ panelPath: safeRelativeHrefSchema.optional(),
98
+ strategies: z.object({
99
+ database: notificationDeliveryStrategySchema.optional(),
100
+ email: notificationDeliveryEmailSchema.optional(),
101
+ }).optional(),
102
+ })
103
+
104
+ export type CreateNotificationInput = z.infer<typeof createNotificationSchema>
105
+ export type CreateBatchNotificationInput = z.infer<typeof createBatchNotificationSchema>
106
+ export type CreateRoleNotificationInput = z.infer<typeof createRoleNotificationSchema>
107
+ export type CreateFeatureNotificationInput = z.infer<typeof createFeatureNotificationSchema>
108
+ export type ListNotificationsInput = z.infer<typeof listNotificationsSchema>
109
+ export type ExecuteActionInput = z.infer<typeof executeActionSchema>
110
+ export type NotificationDeliveryConfigInput = z.infer<typeof notificationDeliveryConfigSchema>
@@ -0,0 +1,11 @@
1
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
2
+ import { asFunction } from 'awilix'
3
+ import { createNotificationService } from './lib/notificationService'
4
+
5
+ export function register(container: AppContainer): void {
6
+ container.register({
7
+ notificationService: asFunction(({ em, eventBus, commandBus }) =>
8
+ createNotificationService({ em, eventBus, commandBus })
9
+ ).scoped(),
10
+ })
11
+ }
@@ -0,0 +1,98 @@
1
+ import React from 'react'
2
+ import { Html, Head, Preview, Body, Container, Heading, Text, Section, Button, Hr } from '@react-email/components'
3
+
4
+ export type NotificationEmailAction = {
5
+ id: string
6
+ label: string
7
+ href: string
8
+ }
9
+
10
+ export type NotificationEmailCopy = {
11
+ preview: string
12
+ heading: string
13
+ bodyIntro: string
14
+ actionNotice: string
15
+ openCta: string
16
+ footer: string
17
+ }
18
+
19
+ type NotificationEmailProps = {
20
+ title: string
21
+ body?: string | null
22
+ actions: NotificationEmailAction[]
23
+ panelUrl: string
24
+ copy: NotificationEmailCopy
25
+ }
26
+
27
+ export function NotificationEmail({ title, body, actions, panelUrl, copy }: NotificationEmailProps) {
28
+ return (
29
+ <Html>
30
+ <Head />
31
+ <Preview>{copy.preview}</Preview>
32
+ <Body style={{ backgroundColor: '#f5f5f5', fontFamily: 'Arial, sans-serif', margin: 0, padding: 0 }}>
33
+ <Container style={{ maxWidth: 560, margin: '0 auto', padding: '24px' }}>
34
+ <Section style={{ backgroundColor: '#ffffff', padding: '24px', borderRadius: 12 }}>
35
+ <Heading as="h1" style={{ fontSize: '22px', margin: '0 0 8px', color: '#111827' }}>
36
+ {copy.heading}
37
+ </Heading>
38
+ <Text style={{ margin: '0 0 16px', color: '#4b5563' }}>{title}</Text>
39
+ {body && (
40
+ <Text style={{ margin: '0 0 16px', color: '#111827', fontSize: '15px' }}>
41
+ {body}
42
+ </Text>
43
+ )}
44
+ <Text style={{ margin: '0 0 16px', color: '#6b7280', fontSize: '13px' }}>
45
+ {copy.bodyIntro}
46
+ </Text>
47
+ {actions.length > 0 && (
48
+ <Section style={{ marginBottom: 16 }}>
49
+ {actions.map((action) => (
50
+ <Button
51
+ key={action.id}
52
+ href={action.href}
53
+ style={{
54
+ backgroundColor: '#111827',
55
+ color: '#ffffff',
56
+ padding: '10px 16px',
57
+ borderRadius: 8,
58
+ textDecoration: 'none',
59
+ display: 'inline-block',
60
+ marginRight: 8,
61
+ marginBottom: 8,
62
+ fontSize: '13px',
63
+ }}
64
+ >
65
+ {action.label}
66
+ </Button>
67
+ ))}
68
+ </Section>
69
+ )}
70
+ <Text style={{ margin: '0 0 16px', color: '#6b7280', fontSize: '12px' }}>
71
+ {copy.actionNotice}
72
+ </Text>
73
+ <Button
74
+ href={panelUrl}
75
+ style={{
76
+ backgroundColor: '#2563eb',
77
+ color: '#ffffff',
78
+ padding: '12px 18px',
79
+ borderRadius: 8,
80
+ textDecoration: 'none',
81
+ display: 'inline-block',
82
+ fontSize: '14px',
83
+ }}
84
+ >
85
+ {copy.openCta}
86
+ </Button>
87
+ <Hr style={{ margin: '24px 0', borderColor: '#e5e7eb' }} />
88
+ <Text style={{ margin: 0, color: '#9ca3af', fontSize: '12px' }}>
89
+ {copy.footer}
90
+ </Text>
91
+ </Section>
92
+ </Container>
93
+ </Body>
94
+ </Html>
95
+ )
96
+ }
97
+
98
+ export default NotificationEmail
@@ -0,0 +1,42 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { NotificationPanel } from '@open-mercato/ui/backend/notifications'
7
+ import { useNotificationsPoll } from '@open-mercato/ui/backend/notifications'
8
+
9
+ export function NotificationInboxPageClient() {
10
+ const t = useT()
11
+ const router = useRouter()
12
+ const {
13
+ notifications,
14
+ unreadCount,
15
+ markAsRead,
16
+ executeAction,
17
+ dismiss,
18
+ dismissUndo,
19
+ undoDismiss,
20
+ markAllRead,
21
+ } = useNotificationsPoll()
22
+
23
+ return (
24
+ <NotificationPanel
25
+ open
26
+ onOpenChange={(open) => {
27
+ if (!open) router.push('/backend')
28
+ }}
29
+ notifications={notifications}
30
+ unreadCount={unreadCount}
31
+ onMarkAsRead={markAsRead}
32
+ onExecuteAction={executeAction}
33
+ onDismiss={dismiss}
34
+ dismissUndo={dismissUndo}
35
+ onUndoDismiss={undoDismiss}
36
+ onMarkAllRead={markAllRead}
37
+ t={t}
38
+ />
39
+ )
40
+ }
41
+
42
+ export default NotificationInboxPageClient