@open-mercato/core 0.4.2-canary-7c76659938 → 0.4.2-canary-70c8402224

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 +5 -1
  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 +69 -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 +57 -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 +96 -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 +5 -1
  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 +70 -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 +52 -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 +98 -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,53 @@
1
+ import { z } from 'zod'
2
+ import { restoreNotificationSchema } from '../../../data/validators'
3
+ import { resolveNotificationContext } from '../../../lib/routeHelpers'
4
+
5
+ export const metadata = {
6
+ PUT: { requireAuth: true },
7
+ }
8
+
9
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ const { service, scope } = await resolveNotificationContext(req)
12
+
13
+ const body = await req.json().catch(() => ({}))
14
+ const input = restoreNotificationSchema.parse(body)
15
+
16
+ await service.restoreDismissed(id, input.status, scope)
17
+
18
+ return Response.json({ ok: true })
19
+ }
20
+
21
+ export const openApi = {
22
+ PUT: {
23
+ summary: 'Restore dismissed notification',
24
+ description: 'Undo a dismissal and restore a notification to read or unread.',
25
+ tags: ['Notifications'],
26
+ parameters: [
27
+ {
28
+ name: 'id',
29
+ in: 'path',
30
+ required: true,
31
+ schema: { type: 'string', format: 'uuid' },
32
+ },
33
+ ],
34
+ requestBody: {
35
+ required: false,
36
+ content: {
37
+ 'application/json': {
38
+ schema: restoreNotificationSchema,
39
+ },
40
+ },
41
+ },
42
+ responses: {
43
+ 200: {
44
+ description: 'Notification restored',
45
+ content: {
46
+ 'application/json': {
47
+ schema: z.object({ ok: z.boolean() }),
48
+ },
49
+ },
50
+ },
51
+ },
52
+ },
53
+ }
@@ -0,0 +1,14 @@
1
+ import { createBatchNotificationSchema } from '../../data/validators'
2
+ import { createBulkNotificationRoute, createBulkNotificationOpenApi } from '../../lib/routeHelpers'
3
+
4
+ export const metadata = {
5
+ POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
6
+ }
7
+
8
+ export const POST = createBulkNotificationRoute(createBatchNotificationSchema, 'createBatch')
9
+
10
+ export const openApi = createBulkNotificationOpenApi(
11
+ createBatchNotificationSchema,
12
+ 'Create batch notifications',
13
+ 'Send the same notification to multiple users'
14
+ )
@@ -0,0 +1,14 @@
1
+ import { createFeatureNotificationSchema } from '../../data/validators'
2
+ import { createBulkNotificationRoute, createBulkNotificationOpenApi } from '../../lib/routeHelpers'
3
+
4
+ export const metadata = {
5
+ POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
6
+ }
7
+
8
+ export const POST = createBulkNotificationRoute(createFeatureNotificationSchema, 'createForFeature')
9
+
10
+ export const openApi = createBulkNotificationOpenApi(
11
+ createFeatureNotificationSchema,
12
+ 'Create notifications for all users with a specific feature/permission',
13
+ 'Send the same notification to all users who have the specified feature permission (via role ACL or user ACL). Supports wildcard matching.'
14
+ )
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod'
2
+ import { resolveNotificationContext } from '../../lib/routeHelpers'
3
+
4
+ export const metadata = {
5
+ PUT: { requireAuth: true },
6
+ }
7
+
8
+ export async function PUT(req: Request) {
9
+ const { service, scope } = await resolveNotificationContext(req)
10
+
11
+ const count = await service.markAllAsRead(scope)
12
+
13
+ return Response.json({ ok: true, count })
14
+ }
15
+
16
+ export const openApi = {
17
+ PUT: {
18
+ summary: 'Mark all notifications as read',
19
+ tags: ['Notifications'],
20
+ responses: {
21
+ 200: {
22
+ description: 'All notifications marked as read',
23
+ content: {
24
+ 'application/json': {
25
+ schema: z.object({
26
+ ok: z.boolean(),
27
+ count: z.number(),
28
+ }),
29
+ },
30
+ },
31
+ },
32
+ },
33
+ },
34
+ }
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod'
2
+ import { createCrudOpenApiFactory, createPagedListResponseSchema } from '@open-mercato/shared/lib/openapi/crud'
3
+ import { listNotificationsSchema, createNotificationSchema, executeActionSchema } from '../data/validators'
4
+
5
+ export const buildNotificationsCrudOpenApi = createCrudOpenApiFactory({
6
+ defaultTag: 'Notifications',
7
+ })
8
+
9
+ export const notificationItemSchema = z.object({
10
+ id: z.string().uuid(),
11
+ type: z.string(),
12
+ title: z.string(),
13
+ body: z.string().nullable().optional(),
14
+ titleKey: z.string().nullable().optional(),
15
+ bodyKey: z.string().nullable().optional(),
16
+ titleVariables: z.record(z.string(), z.string()).nullable().optional(),
17
+ bodyVariables: z.record(z.string(), z.string()).nullable().optional(),
18
+ icon: z.string().nullable().optional(),
19
+ severity: z.string(),
20
+ status: z.string(),
21
+ actions: z.array(z.object({
22
+ id: z.string(),
23
+ label: z.string(),
24
+ labelKey: z.string().optional(),
25
+ variant: z.string().optional(),
26
+ icon: z.string().optional(),
27
+ })),
28
+ primaryActionId: z.string().optional(),
29
+ sourceModule: z.string().nullable().optional(),
30
+ sourceEntityType: z.string().nullable().optional(),
31
+ sourceEntityId: z.string().uuid().nullable().optional(),
32
+ linkHref: z.string().nullable().optional(),
33
+ createdAt: z.string(),
34
+ readAt: z.string().nullable().optional(),
35
+ actionTaken: z.string().nullable().optional(),
36
+ })
37
+
38
+ export const okResponseSchema = z.object({
39
+ ok: z.boolean(),
40
+ })
41
+
42
+ export const unreadCountResponseSchema = z.object({
43
+ unreadCount: z.number(),
44
+ })
45
+
46
+ export const actionResultResponseSchema = z.object({
47
+ ok: z.boolean(),
48
+ result: z.unknown().optional(),
49
+ href: z.string().optional(),
50
+ })
51
+
52
+ export { createPagedListResponseSchema, listNotificationsSchema, createNotificationSchema, executeActionSchema }
@@ -0,0 +1,14 @@
1
+ import { createRoleNotificationSchema } from '../../data/validators'
2
+ import { createBulkNotificationRoute, createBulkNotificationOpenApi } from '../../lib/routeHelpers'
3
+
4
+ export const metadata = {
5
+ POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
6
+ }
7
+
8
+ export const POST = createBulkNotificationRoute(createRoleNotificationSchema, 'createForRole')
9
+
10
+ export const openApi = createBulkNotificationOpenApi(
11
+ createRoleNotificationSchema,
12
+ 'Create notifications for all users in a role',
13
+ 'Send the same notification to all users who have the specified role within the organization'
14
+ )
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/core'
3
+ import { Notification } from '../data/entities'
4
+ import { listNotificationsSchema, createNotificationSchema } from '../data/validators'
5
+ import { toNotificationDto } from '../lib/notificationMapper'
6
+ import { resolveNotificationContext } from '../lib/routeHelpers'
7
+ import {
8
+ buildNotificationsCrudOpenApi,
9
+ createPagedListResponseSchema,
10
+ notificationItemSchema,
11
+ } from './openapi'
12
+
13
+ export const metadata = {
14
+ GET: { requireAuth: true },
15
+ POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
16
+ }
17
+
18
+ export async function GET(req: Request) {
19
+ const { ctx, scope } = await resolveNotificationContext(req)
20
+ const em = ctx.container.resolve('em') as EntityManager
21
+
22
+ const url = new URL(req.url)
23
+ const queryParams = Object.fromEntries(url.searchParams.entries())
24
+ const input = listNotificationsSchema.parse(queryParams)
25
+
26
+ const filters: Record<string, unknown> = {
27
+ recipientUserId: scope.userId,
28
+ tenantId: scope.tenantId,
29
+ }
30
+
31
+ if (input.status) {
32
+ filters.status = Array.isArray(input.status) ? { $in: input.status } : input.status
33
+ } else {
34
+ filters.status = { $ne: 'dismissed' }
35
+ }
36
+ if (input.type) {
37
+ filters.type = input.type
38
+ }
39
+ if (input.severity) {
40
+ filters.severity = input.severity
41
+ }
42
+ if (input.sourceEntityType) {
43
+ filters.sourceEntityType = input.sourceEntityType
44
+ }
45
+ if (input.sourceEntityId) {
46
+ filters.sourceEntityId = input.sourceEntityId
47
+ }
48
+ if (input.since) {
49
+ filters.createdAt = { $gt: new Date(input.since) }
50
+ }
51
+
52
+ const [notifications, total] = await Promise.all([
53
+ em.find(Notification, filters, {
54
+ orderBy: { createdAt: 'desc' },
55
+ limit: input.pageSize,
56
+ offset: (input.page - 1) * input.pageSize,
57
+ }),
58
+ em.count(Notification, filters),
59
+ ])
60
+
61
+ const items = notifications.map(toNotificationDto)
62
+
63
+ return Response.json({
64
+ items,
65
+ total,
66
+ page: input.page,
67
+ pageSize: input.pageSize,
68
+ totalPages: Math.ceil(total / input.pageSize),
69
+ })
70
+ }
71
+
72
+ export async function POST(req: Request) {
73
+ const { service, scope } = await resolveNotificationContext(req)
74
+
75
+ const body = await req.json().catch(() => ({}))
76
+ const input = createNotificationSchema.parse(body)
77
+
78
+ const notification = await service.create(input, scope)
79
+
80
+ return Response.json({ id: notification.id }, { status: 201 })
81
+ }
82
+
83
+ export const openApi = buildNotificationsCrudOpenApi({
84
+ resourceName: 'Notification',
85
+ querySchema: listNotificationsSchema,
86
+ listResponseSchema: createPagedListResponseSchema(notificationItemSchema),
87
+ create: {
88
+ schema: createNotificationSchema,
89
+ responseSchema: z.object({ id: z.string().uuid() }),
90
+ description: 'Creates a notification for a user.',
91
+ },
92
+ })
@@ -0,0 +1,98 @@
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
+ DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
8
+ resolveNotificationDeliveryConfig,
9
+ saveNotificationDeliveryConfig,
10
+ } from '../../lib/deliveryConfig'
11
+
12
+ export const metadata = {
13
+ GET: { requireAuth: true, requireFeatures: ['notifications.manage'] },
14
+ POST: { requireAuth: true, requireFeatures: ['notifications.manage'] },
15
+ }
16
+
17
+ const unauthorized = async () => {
18
+ const { t } = await resolveTranslations()
19
+ return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
20
+ }
21
+
22
+ export async function GET(req: Request) {
23
+ const auth = await getAuthFromRequest(req)
24
+ if (!auth?.sub) return await unauthorized()
25
+
26
+ const container = await createRequestContainer()
27
+ try {
28
+ const settings = await resolveNotificationDeliveryConfig(container, {
29
+ defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
30
+ })
31
+ return NextResponse.json({ settings })
32
+ } finally {
33
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
34
+ if (typeof disposable.dispose === 'function') {
35
+ await disposable.dispose()
36
+ }
37
+ }
38
+ }
39
+
40
+ export async function POST(req: Request) {
41
+ const { t } = await resolveTranslations()
42
+ const auth = await getAuthFromRequest(req)
43
+ if (!auth?.sub) return await unauthorized()
44
+
45
+ let body: unknown
46
+ try {
47
+ body = await req.json()
48
+ } catch {
49
+ return NextResponse.json(
50
+ { error: t('api.errors.invalidPayload', 'Invalid request body') },
51
+ { status: 400 }
52
+ )
53
+ }
54
+
55
+ const parsed = notificationDeliveryConfigSchema.safeParse(body)
56
+ if (!parsed.success) {
57
+ return NextResponse.json(
58
+ { error: t('notifications.delivery.settings.invalid', 'Invalid delivery settings') },
59
+ { status: 400 }
60
+ )
61
+ }
62
+
63
+ const container = await createRequestContainer()
64
+ try {
65
+ await saveNotificationDeliveryConfig(container, parsed.data)
66
+ const settings = await resolveNotificationDeliveryConfig(container, {
67
+ defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
68
+ })
69
+ return NextResponse.json({ ok: true, settings })
70
+ } catch (error) {
71
+ return NextResponse.json(
72
+ { error: error instanceof Error ? error.message : t('api.errors.internal', 'Internal error') },
73
+ { status: 500 }
74
+ )
75
+ } finally {
76
+ const disposable = container as unknown as { dispose?: () => Promise<void> }
77
+ if (typeof disposable.dispose === 'function') {
78
+ await disposable.dispose()
79
+ }
80
+ }
81
+ }
82
+
83
+ export const openApi = {
84
+ GET: {
85
+ summary: 'Get notification delivery settings',
86
+ tags: ['Notifications'],
87
+ responses: {
88
+ 200: { description: 'Current delivery settings' },
89
+ },
90
+ },
91
+ POST: {
92
+ summary: 'Update notification delivery settings',
93
+ tags: ['Notifications'],
94
+ responses: {
95
+ 200: { description: 'Delivery settings updated' },
96
+ },
97
+ },
98
+ }
@@ -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>