@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,25 @@
1
+ import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
2
+
3
+ export const notificationTypes: NotificationTypeDefinition[] = [
4
+ {
5
+ type: 'catalog.product.low_stock',
6
+ module: 'catalog',
7
+ titleKey: 'catalog.notifications.product.lowStock.title',
8
+ bodyKey: 'catalog.notifications.product.lowStock.body',
9
+ icon: 'package-x',
10
+ severity: 'warning',
11
+ actions: [
12
+ {
13
+ id: 'view',
14
+ labelKey: 'common.view',
15
+ variant: 'outline',
16
+ href: '/backend/catalog/products/{sourceEntityId}',
17
+ icon: 'external-link',
18
+ },
19
+ ],
20
+ linkHref: '/backend/catalog/products/{sourceEntityId}',
21
+ expiresAfterHours: 72, // 3 days
22
+ },
23
+ ]
24
+
25
+ export default notificationTypes
@@ -0,0 +1,52 @@
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: 'catalog.product.stock_low',
8
+ persistent: true,
9
+ id: 'catalog:low-stock-notification',
10
+ }
11
+
12
+ type LowStockPayload = {
13
+ productId: string
14
+ productName: string
15
+ sku?: string | null
16
+ currentStock: number
17
+ threshold: number
18
+ tenantId: string
19
+ organizationId?: string | null
20
+ }
21
+
22
+ type ResolverContext = {
23
+ resolve: <T = unknown>(name: string) => T
24
+ }
25
+
26
+ export default async function handle(payload: LowStockPayload, ctx: ResolverContext) {
27
+ try {
28
+ const notificationService = resolveNotificationService(ctx)
29
+ const typeDef = notificationTypes.find((type) => type.type === 'catalog.product.low_stock')
30
+ if (!typeDef) return
31
+
32
+ const notificationInput = buildFeatureNotificationFromType(typeDef, {
33
+ requiredFeature: 'catalog.products.manage',
34
+ bodyVariables: {
35
+ productName: payload.productName,
36
+ sku: payload.sku ?? '',
37
+ currentStock: String(payload.currentStock),
38
+ threshold: String(payload.threshold),
39
+ },
40
+ sourceEntityType: 'catalog:product',
41
+ sourceEntityId: payload.productId,
42
+ linkHref: `/backend/catalog/products/${payload.productId}`,
43
+ })
44
+
45
+ await notificationService.createForFeature(notificationInput, {
46
+ tenantId: payload.tenantId,
47
+ organizationId: payload.organizationId ?? null,
48
+ })
49
+ } catch (err) {
50
+ console.error('[catalog:low-stock-notification] Failed to create notification:', err)
51
+ }
52
+ }
@@ -2,6 +2,7 @@ import type { ModuleCli } from '@open-mercato/shared/modules/registry'
2
2
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
3
  import type { ModuleConfigService } from './lib/module-config-service'
4
4
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
5
+ import { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, NOTIFICATIONS_DELIVERY_CONFIG_KEY } from '../notifications/lib/deliveryConfig'
5
6
 
6
7
  function envDisablesAutoIndexing(): boolean {
7
8
  const raw = process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING
@@ -31,6 +32,11 @@ const restoreDefaults: ModuleCli = {
31
32
  name: 'auto_index_enabled',
32
33
  value: defaultEnabled,
33
34
  },
35
+ {
36
+ moduleId: 'notifications',
37
+ name: NOTIFICATIONS_DELIVERY_CONFIG_KEY,
38
+ value: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
39
+ },
34
40
  ],
35
41
  { force: true },
36
42
  )
@@ -35,6 +35,9 @@ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
35
35
  import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
36
36
  import { E } from '#generated/entities.ids.generated'
37
37
  import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
38
+ import { resolveNotificationService } from '../../notifications/lib/notificationService'
39
+ import { buildNotificationFromType } from '../../notifications/lib/notificationBuilder'
40
+ import { notificationTypes } from '../notifications'
38
41
 
39
42
  const DEAL_ENTITY_ID = 'customers:customer_deal'
40
43
  const dealCrudIndexer: CrudIndexerConfig<CustomerDeal> = {
@@ -281,6 +284,8 @@ const updateDealCommand: CommandHandler<DealUpdateInput, { dealId: string }> = {
281
284
  ensureTenantScope(ctx, record.tenantId)
282
285
  ensureOrganizationScope(ctx, record.organizationId)
283
286
 
287
+ const previousStatus = record.status
288
+
284
289
  if (parsed.title !== undefined) record.title = parsed.title
285
290
  if (parsed.description !== undefined) record.description = parsed.description ?? null
286
291
  if (parsed.status !== undefined) record.status = parsed.status ?? record.status
@@ -319,6 +324,40 @@ const updateDealCommand: CommandHandler<DealUpdateInput, { dealId: string }> = {
319
324
  indexer: dealCrudIndexer,
320
325
  })
321
326
 
327
+ // Send notifications for deal won/lost status changes
328
+ const newStatus = record.status
329
+ const normalizedStatus = newStatus === 'win' ? 'won' : newStatus === 'loose' ? 'lost' : newStatus
330
+ if (previousStatus !== newStatus && (normalizedStatus === 'won' || normalizedStatus === 'lost') && record.ownerUserId) {
331
+ try {
332
+ const notificationService = resolveNotificationService(ctx.container)
333
+ const notificationType = normalizedStatus === 'won' ? 'customers.deal.won' : 'customers.deal.lost'
334
+ const typeDef = notificationTypes.find((type) => type.type === notificationType)
335
+ if (typeDef) {
336
+ const valueDisplay = record.valueAmount && record.valueCurrency
337
+ ? `${record.valueCurrency} ${record.valueAmount}`
338
+ : ''
339
+
340
+ const notificationInput = buildNotificationFromType(typeDef, {
341
+ recipientUserId: record.ownerUserId,
342
+ bodyVariables: {
343
+ dealTitle: record.title,
344
+ dealValue: valueDisplay,
345
+ },
346
+ sourceEntityType: 'customers:customer_deal',
347
+ sourceEntityId: record.id,
348
+ linkHref: `/backend/customers/deals/${record.id}`,
349
+ })
350
+
351
+ await notificationService.create(notificationInput, {
352
+ tenantId: record.tenantId,
353
+ organizationId: record.organizationId,
354
+ })
355
+ }
356
+ } catch {
357
+ // Notification creation is non-critical, don't fail the command
358
+ }
359
+ }
360
+
322
361
  return { dealId: record.id }
323
362
  },
324
363
  buildLog: async ({ snapshots, ctx }) => {
@@ -943,5 +943,9 @@
943
943
  "customers.workPlan.customerTodos.table.state.empty": "No customer tasks yet.",
944
944
  "customers.workPlan.customerTodos.table.error.load": "Failed to load customer tasks.",
945
945
  "customers.workPlan.customerTodos.table.export.view": "Exports the current list with filters and visible columns.",
946
- "customers.workPlan.customerTodos.table.export.full": "Exports every linked task field, including hidden attributes."
946
+ "customers.workPlan.customerTodos.table.export.full": "Exports every linked task field, including hidden attributes.",
947
+ "customers.notifications.deal.won.title": "Deal Won",
948
+ "customers.notifications.deal.won.body": "{dealTitle} has been marked as won{dealValue, select, other { ({dealValue})}}",
949
+ "customers.notifications.deal.lost.title": "Deal Lost",
950
+ "customers.notifications.deal.lost.body": "{dealTitle} has been marked as lost"
947
951
  }
@@ -0,0 +1,44 @@
1
+ import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
2
+
3
+ export const notificationTypes: NotificationTypeDefinition[] = [
4
+ {
5
+ type: 'customers.deal.won',
6
+ module: 'customers',
7
+ titleKey: 'customers.notifications.deal.won.title',
8
+ bodyKey: 'customers.notifications.deal.won.body',
9
+ icon: 'trophy',
10
+ severity: 'success',
11
+ actions: [
12
+ {
13
+ id: 'view',
14
+ labelKey: 'common.view',
15
+ variant: 'outline',
16
+ href: '/backend/customers/deals/{sourceEntityId}',
17
+ icon: 'external-link',
18
+ },
19
+ ],
20
+ linkHref: '/backend/customers/deals/{sourceEntityId}',
21
+ expiresAfterHours: 168, // 7 days
22
+ },
23
+ {
24
+ type: 'customers.deal.lost',
25
+ module: 'customers',
26
+ titleKey: 'customers.notifications.deal.lost.title',
27
+ bodyKey: 'customers.notifications.deal.lost.body',
28
+ icon: 'x-circle',
29
+ severity: 'warning',
30
+ actions: [
31
+ {
32
+ id: 'view',
33
+ labelKey: 'common.view',
34
+ variant: 'outline',
35
+ href: '/backend/customers/deals/{sourceEntityId}',
36
+ icon: 'external-link',
37
+ },
38
+ ],
39
+ linkHref: '/backend/customers/deals/{sourceEntityId}',
40
+ expiresAfterHours: 168, // 7 days
41
+ },
42
+ ]
43
+
44
+ export default notificationTypes
@@ -0,0 +1,7 @@
1
+ export const features = [
2
+ { id: 'notifications.view', title: 'View own notifications', module: 'notifications' },
3
+ { id: 'notifications.create', title: 'Create notifications for others', module: 'notifications' },
4
+ { id: 'notifications.manage', title: 'Manage all notifications', module: 'notifications' },
5
+ ]
6
+
7
+ export default features
@@ -0,0 +1,75 @@
1
+ import { executeActionSchema } from '../../../data/validators'
2
+ import { actionResultResponseSchema, errorResponseSchema } from '../../openapi'
3
+ import { resolveNotificationContext } from '../../../lib/routeHelpers'
4
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
+
6
+ export const metadata = {
7
+ POST: { requireAuth: true },
8
+ }
9
+
10
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
11
+ const { id } = await params
12
+ const { service, scope } = await resolveNotificationContext(req)
13
+
14
+ const body = await req.json().catch(() => ({}))
15
+ const input = executeActionSchema.parse(body)
16
+
17
+ try {
18
+ const { notification, result } = await service.executeAction(id, input, scope)
19
+
20
+ const action = notification.actionData?.actions?.find((a) => a.id === input.actionId)
21
+ const href = action?.href?.replace('{sourceEntityId}', notification.sourceEntityId ?? '')
22
+
23
+ return Response.json({
24
+ ok: true,
25
+ result,
26
+ href,
27
+ })
28
+ } catch (error) {
29
+ const { t } = await resolveTranslations()
30
+ const fallback = t('notifications.error.action', 'Failed to execute action')
31
+ const message = error instanceof Error && error.message ? error.message : fallback
32
+ return Response.json({ error: message }, { status: 400 })
33
+ }
34
+ }
35
+
36
+ export const openApi = {
37
+ POST: {
38
+ summary: 'Execute notification action',
39
+ tags: ['Notifications'],
40
+ parameters: [
41
+ {
42
+ name: 'id',
43
+ in: 'path',
44
+ required: true,
45
+ schema: { type: 'string', format: 'uuid' },
46
+ },
47
+ ],
48
+ requestBody: {
49
+ required: true,
50
+ content: {
51
+ 'application/json': {
52
+ schema: executeActionSchema,
53
+ },
54
+ },
55
+ },
56
+ responses: {
57
+ 200: {
58
+ description: 'Action executed successfully',
59
+ content: {
60
+ 'application/json': {
61
+ schema: actionResultResponseSchema,
62
+ },
63
+ },
64
+ },
65
+ 400: {
66
+ description: 'Action not found or failed',
67
+ content: {
68
+ 'application/json': {
69
+ schema: errorResponseSchema,
70
+ },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ }
@@ -0,0 +1,12 @@
1
+ import { createSingleNotificationActionRoute, createSingleNotificationActionOpenApi } from '../../../lib/routeHelpers'
2
+
3
+ export const metadata = {
4
+ PUT: { requireAuth: true },
5
+ }
6
+
7
+ export const PUT = createSingleNotificationActionRoute('dismiss')
8
+
9
+ export const openApi = createSingleNotificationActionOpenApi(
10
+ 'Dismiss notification',
11
+ 'Notification dismissed'
12
+ )
@@ -0,0 +1,12 @@
1
+ import { createSingleNotificationActionRoute, createSingleNotificationActionOpenApi } from '../../../lib/routeHelpers'
2
+
3
+ export const metadata = {
4
+ PUT: { requireAuth: true },
5
+ }
6
+
7
+ export const PUT = createSingleNotificationActionRoute('markAsRead')
8
+
9
+ export const openApi = createSingleNotificationActionOpenApi(
10
+ 'Mark notification as read',
11
+ 'Notification marked as read'
12
+ )
@@ -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,76 @@
1
+ import { z } from 'zod'
2
+ import { createCrudOpenApiFactory, createPagedListResponseSchema } from '@open-mercato/shared/lib/openapi/crud'
3
+ import {
4
+ listNotificationsSchema,
5
+ createNotificationSchema,
6
+ executeActionSchema,
7
+ notificationDeliveryConfigSchema,
8
+ } from '../data/validators'
9
+
10
+ export const buildNotificationsCrudOpenApi = createCrudOpenApiFactory({
11
+ defaultTag: 'Notifications',
12
+ })
13
+
14
+ export const notificationItemSchema = z.object({
15
+ id: z.string().uuid(),
16
+ type: z.string(),
17
+ title: z.string(),
18
+ body: z.string().nullable().optional(),
19
+ titleKey: z.string().nullable().optional(),
20
+ bodyKey: z.string().nullable().optional(),
21
+ titleVariables: z.record(z.string(), z.string()).nullable().optional(),
22
+ bodyVariables: z.record(z.string(), z.string()).nullable().optional(),
23
+ icon: z.string().nullable().optional(),
24
+ severity: z.string(),
25
+ status: z.string(),
26
+ actions: z.array(z.object({
27
+ id: z.string(),
28
+ label: z.string(),
29
+ labelKey: z.string().optional(),
30
+ variant: z.string().optional(),
31
+ icon: z.string().optional(),
32
+ })),
33
+ primaryActionId: z.string().optional(),
34
+ sourceModule: z.string().nullable().optional(),
35
+ sourceEntityType: z.string().nullable().optional(),
36
+ sourceEntityId: z.string().uuid().nullable().optional(),
37
+ linkHref: z.string().nullable().optional(),
38
+ createdAt: z.string(),
39
+ readAt: z.string().nullable().optional(),
40
+ actionTaken: z.string().nullable().optional(),
41
+ })
42
+
43
+ export const okResponseSchema = z.object({
44
+ ok: z.boolean(),
45
+ })
46
+
47
+ export const errorResponseSchema = z.object({
48
+ error: z.string(),
49
+ })
50
+
51
+ export const unreadCountResponseSchema = z.object({
52
+ unreadCount: z.number(),
53
+ })
54
+
55
+ export const actionResultResponseSchema = z.object({
56
+ ok: z.boolean(),
57
+ result: z.unknown().optional(),
58
+ href: z.string().optional(),
59
+ })
60
+
61
+ export const notificationSettingsResponseSchema = z.object({
62
+ settings: notificationDeliveryConfigSchema,
63
+ })
64
+
65
+ export const notificationSettingsUpdateResponseSchema = z.object({
66
+ ok: z.boolean(),
67
+ settings: notificationDeliveryConfigSchema,
68
+ })
69
+
70
+ export {
71
+ createPagedListResponseSchema,
72
+ listNotificationsSchema,
73
+ createNotificationSchema,
74
+ executeActionSchema,
75
+ notificationDeliveryConfigSchema,
76
+ }
@@ -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
+ })