@open-mercato/core 0.6.8-develop.6984.1.f0cf23f0f6 → 0.6.8-develop.6985.1.fb93574faa

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 (597) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/notification/index.js +6 -0
  3. package/dist/generated/entities/notification/index.js.map +2 -2
  4. package/dist/generated/entities/notification_preference/index.js +19 -0
  5. package/dist/generated/entities/notification_preference/index.js.map +7 -0
  6. package/dist/generated/entities/notification_type/index.js +21 -0
  7. package/dist/generated/entities/notification_type/index.js.map +7 -0
  8. package/dist/generated/entities/notification_type_override/index.js +17 -0
  9. package/dist/generated/entities/notification_type_override/index.js.map +7 -0
  10. package/dist/generated/entities/push_notification_delivery/index.js +41 -0
  11. package/dist/generated/entities/push_notification_delivery/index.js.map +7 -0
  12. package/dist/generated/entities/user_device/index.js +35 -0
  13. package/dist/generated/entities/user_device/index.js.map +7 -0
  14. package/dist/generated/entities.ids.generated.js +12 -1
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +72 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/api.js +2 -1
  19. package/dist/helpers/integration/api.js.map +2 -2
  20. package/dist/helpers/integration/appRoot.js +9 -0
  21. package/dist/helpers/integration/appRoot.js.map +7 -0
  22. package/dist/helpers/integration/dbFixtures.js +1 -4
  23. package/dist/helpers/integration/dbFixtures.js.map +2 -2
  24. package/dist/helpers/integration/pushFake.js +126 -0
  25. package/dist/helpers/integration/pushFake.js.map +7 -0
  26. package/dist/modules/auth/notifications.js +16 -0
  27. package/dist/modules/auth/notifications.js.map +2 -2
  28. package/dist/modules/business_rules/notifications.js +2 -0
  29. package/dist/modules/business_rules/notifications.js.map +2 -2
  30. package/dist/modules/catalog/notifications.js +2 -0
  31. package/dist/modules/catalog/notifications.js.map +2 -2
  32. package/dist/modules/communication_channels/acl.js +9 -0
  33. package/dist/modules/communication_channels/acl.js.map +2 -2
  34. package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js +133 -0
  35. package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js.map +7 -0
  36. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +6 -2
  37. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +2 -2
  38. package/dist/modules/communication_channels/api/get/channels/[id]/route.js +6 -2
  39. package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +2 -2
  40. package/dist/modules/communication_channels/api/get/channels/route.js +2 -1
  41. package/dist/modules/communication_channels/api/get/channels/route.js.map +2 -2
  42. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +10 -5
  43. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
  44. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +29 -1
  45. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  46. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +157 -0
  47. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +7 -0
  48. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +8 -1
  49. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
  50. package/dist/modules/communication_channels/commands/admin-delete-channel.js +141 -0
  51. package/dist/modules/communication_channels/commands/admin-delete-channel.js.map +7 -0
  52. package/dist/modules/communication_channels/commands/connect-credential-channel.js +25 -8
  53. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
  54. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +2 -2
  55. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  56. package/dist/modules/communication_channels/commands/push-register.js +1 -1
  57. package/dist/modules/communication_channels/commands/push-register.js.map +2 -2
  58. package/dist/modules/communication_channels/commands/push-unregister.js +4 -3
  59. package/dist/modules/communication_channels/commands/push-unregister.js.map +2 -2
  60. package/dist/modules/communication_channels/data/entities.js +4 -0
  61. package/dist/modules/communication_channels/data/entities.js.map +2 -2
  62. package/dist/modules/communication_channels/data/validators.js +2 -1
  63. package/dist/modules/communication_channels/data/validators.js.map +2 -2
  64. package/dist/modules/communication_channels/lib/access-control.js +11 -1
  65. package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
  66. package/dist/modules/communication_channels/lib/connect-channel.js +20 -12
  67. package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
  68. package/dist/modules/communication_channels/lib/push-adapter.js +68 -0
  69. package/dist/modules/communication_channels/lib/push-adapter.js.map +7 -0
  70. package/dist/modules/communication_channels/lib/push-capabilities.js +31 -0
  71. package/dist/modules/communication_channels/lib/push-capabilities.js.map +7 -0
  72. package/dist/modules/communication_channels/lib/push-connect-error.js +20 -0
  73. package/dist/modules/communication_channels/lib/push-connect-error.js.map +7 -0
  74. package/dist/modules/communication_channels/lib/push-credential-errors.js +41 -0
  75. package/dist/modules/communication_channels/lib/push-credential-errors.js.map +7 -0
  76. package/dist/modules/communication_channels/lib/push-envelope.js +46 -0
  77. package/dist/modules/communication_channels/lib/push-envelope.js.map +7 -0
  78. package/dist/modules/communication_channels/lib/refcounted-client-cache.js +64 -0
  79. package/dist/modules/communication_channels/lib/refcounted-client-cache.js.map +7 -0
  80. package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js +13 -0
  81. package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js.map +7 -0
  82. package/dist/modules/communication_channels/notifications.js +4 -0
  83. package/dist/modules/communication_channels/notifications.js.map +2 -2
  84. package/dist/modules/communication_channels/setup.js +2 -0
  85. package/dist/modules/communication_channels/setup.js.map +2 -2
  86. package/dist/modules/configs/lib/upgrade-actions.js +21 -0
  87. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  88. package/dist/modules/customer_accounts/notifications.js +12 -0
  89. package/dist/modules/customer_accounts/notifications.js.map +2 -2
  90. package/dist/modules/customers/notifications.js +4 -0
  91. package/dist/modules/customers/notifications.js.map +2 -2
  92. package/dist/modules/devices/acl.js +11 -0
  93. package/dist/modules/devices/acl.js.map +7 -0
  94. package/dist/modules/devices/api/[id]/route.js +167 -0
  95. package/dist/modules/devices/api/[id]/route.js.map +7 -0
  96. package/dist/modules/devices/api/admin/devices/[id]/route.js +219 -0
  97. package/dist/modules/devices/api/admin/devices/[id]/route.js.map +7 -0
  98. package/dist/modules/devices/api/admin/devices/route.js +140 -0
  99. package/dist/modules/devices/api/admin/devices/route.js.map +7 -0
  100. package/dist/modules/devices/api/auth.js +9 -0
  101. package/dist/modules/devices/api/auth.js.map +7 -0
  102. package/dist/modules/devices/api/deviceList.js +53 -0
  103. package/dist/modules/devices/api/deviceList.js.map +7 -0
  104. package/dist/modules/devices/api/deviceOps.js +118 -0
  105. package/dist/modules/devices/api/deviceOps.js.map +7 -0
  106. package/dist/modules/devices/api/openapi.js +19 -0
  107. package/dist/modules/devices/api/openapi.js.map +7 -0
  108. package/dist/modules/devices/api/route.js +143 -0
  109. package/dist/modules/devices/api/route.js.map +7 -0
  110. package/dist/modules/devices/backend/devices/[id]/page.js +135 -0
  111. package/dist/modules/devices/backend/devices/[id]/page.js.map +7 -0
  112. package/dist/modules/devices/backend/devices/[id]/page.meta.js +16 -0
  113. package/dist/modules/devices/backend/devices/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/devices/backend/devices/create/page.js +68 -0
  115. package/dist/modules/devices/backend/devices/create/page.js.map +7 -0
  116. package/dist/modules/devices/backend/devices/create/page.meta.js +16 -0
  117. package/dist/modules/devices/backend/devices/create/page.meta.js.map +7 -0
  118. package/dist/modules/devices/backend/devices/page.js +235 -0
  119. package/dist/modules/devices/backend/devices/page.js.map +7 -0
  120. package/dist/modules/devices/backend/devices/page.meta.js +23 -0
  121. package/dist/modules/devices/backend/devices/page.meta.js.map +7 -0
  122. package/dist/modules/devices/commands/deactivate.js +107 -0
  123. package/dist/modules/devices/commands/deactivate.js.map +7 -0
  124. package/dist/modules/devices/commands/index.js +4 -0
  125. package/dist/modules/devices/commands/index.js.map +7 -0
  126. package/dist/modules/devices/commands/register.js +183 -0
  127. package/dist/modules/devices/commands/register.js.map +7 -0
  128. package/dist/modules/devices/commands/shared.js +74 -0
  129. package/dist/modules/devices/commands/shared.js.map +7 -0
  130. package/dist/modules/devices/commands/update.js +126 -0
  131. package/dist/modules/devices/commands/update.js.map +7 -0
  132. package/dist/modules/devices/data/entities.js +83 -0
  133. package/dist/modules/devices/data/entities.js.map +7 -0
  134. package/dist/modules/devices/data/validators.js +49 -0
  135. package/dist/modules/devices/data/validators.js.map +7 -0
  136. package/dist/modules/devices/di.js +11 -0
  137. package/dist/modules/devices/di.js.map +7 -0
  138. package/dist/modules/devices/encryption.js +12 -0
  139. package/dist/modules/devices/encryption.js.map +7 -0
  140. package/dist/modules/devices/events.js +19 -0
  141. package/dist/modules/devices/events.js.map +7 -0
  142. package/dist/modules/devices/index.js +17 -0
  143. package/dist/modules/devices/index.js.map +7 -0
  144. package/dist/modules/devices/lib/operationMetadata.js +20 -0
  145. package/dist/modules/devices/lib/operationMetadata.js.map +7 -0
  146. package/dist/modules/devices/migrations/Migration20260602120000.js +15 -0
  147. package/dist/modules/devices/migrations/Migration20260602120000.js.map +7 -0
  148. package/dist/modules/devices/migrations/Migration20260630120000.js +15 -0
  149. package/dist/modules/devices/migrations/Migration20260630120000.js.map +7 -0
  150. package/dist/modules/devices/migrations/Migration20260701120000.js +13 -0
  151. package/dist/modules/devices/migrations/Migration20260701120000.js.map +7 -0
  152. package/dist/modules/devices/migrations/Migration20260721120000.js +15 -0
  153. package/dist/modules/devices/migrations/Migration20260721120000.js.map +7 -0
  154. package/dist/modules/devices/setup.js +13 -0
  155. package/dist/modules/devices/setup.js.map +7 -0
  156. package/dist/modules/entities/cli.js +8 -3
  157. package/dist/modules/entities/cli.js.map +2 -2
  158. package/dist/modules/entities/migrations/Migration20260722120000.js +28 -0
  159. package/dist/modules/entities/migrations/Migration20260722120000.js.map +7 -0
  160. package/dist/modules/inbox_ops/notifications.js +2 -0
  161. package/dist/modules/inbox_ops/notifications.js.map +2 -2
  162. package/dist/modules/messages/notifications.js +2 -0
  163. package/dist/modules/messages/notifications.js.map +2 -2
  164. package/dist/modules/notifications/acl.js +3 -1
  165. package/dist/modules/notifications/acl.js.map +2 -2
  166. package/dist/modules/notifications/api/admin/preferences/route.js +167 -0
  167. package/dist/modules/notifications/api/admin/preferences/route.js.map +7 -0
  168. package/dist/modules/notifications/api/channels/route.js +54 -0
  169. package/dist/modules/notifications/api/channels/route.js.map +7 -0
  170. package/dist/modules/notifications/api/preferences/route.js +126 -0
  171. package/dist/modules/notifications/api/preferences/route.js.map +7 -0
  172. package/dist/modules/notifications/api/route.js +8 -1
  173. package/dist/modules/notifications/api/route.js.map +2 -2
  174. package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js +187 -0
  175. package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js.map +7 -0
  176. package/dist/modules/notifications/api/types/route.js +225 -0
  177. package/dist/modules/notifications/api/types/route.js.map +7 -0
  178. package/dist/modules/notifications/api/unread-count/route.js +5 -1
  179. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  180. package/dist/modules/notifications/backend/notifications/user-preferences/page.js +10 -0
  181. package/dist/modules/notifications/backend/notifications/user-preferences/page.js.map +7 -0
  182. package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js +28 -0
  183. package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js.map +7 -0
  184. package/dist/modules/notifications/backend/profile/notification-preferences/page.js +10 -0
  185. package/dist/modules/notifications/backend/profile/notification-preferences/page.js.map +7 -0
  186. package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +33 -0
  187. package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +7 -0
  188. package/dist/modules/notifications/data/entities.js +128 -1
  189. package/dist/modules/notifications/data/entities.js.map +2 -2
  190. package/dist/modules/notifications/data/validators.js +95 -2
  191. package/dist/modules/notifications/data/validators.js.map +2 -2
  192. package/dist/modules/notifications/di.js +7 -0
  193. package/dist/modules/notifications/di.js.map +2 -2
  194. package/dist/modules/notifications/events.js +17 -0
  195. package/dist/modules/notifications/events.js.map +2 -2
  196. package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js +190 -0
  197. package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js.map +7 -0
  198. package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js +124 -0
  199. package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js.map +7 -0
  200. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +166 -1
  201. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  202. package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js +172 -0
  203. package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js.map +7 -0
  204. package/dist/modules/notifications/generators.js +63 -0
  205. package/dist/modules/notifications/generators.js.map +7 -0
  206. package/dist/modules/notifications/lib/delivery-strategies-registry.js +19 -0
  207. package/dist/modules/notifications/lib/delivery-strategies-registry.js.map +7 -0
  208. package/dist/modules/notifications/lib/deliveryStrategies.js.map +2 -2
  209. package/dist/modules/notifications/lib/derive-category.js +8 -0
  210. package/dist/modules/notifications/lib/derive-category.js.map +7 -0
  211. package/dist/modules/notifications/lib/notification-channel-registry.js +35 -0
  212. package/dist/modules/notifications/lib/notification-channel-registry.js.map +7 -0
  213. package/dist/modules/notifications/lib/notification-type-registry.js +87 -0
  214. package/dist/modules/notifications/lib/notification-type-registry.js.map +7 -0
  215. package/dist/modules/notifications/lib/notificationCopy.js +14 -0
  216. package/dist/modules/notifications/lib/notificationCopy.js.map +7 -0
  217. package/dist/modules/notifications/lib/notificationFactory.js +4 -1
  218. package/dist/modules/notifications/lib/notificationFactory.js.map +2 -2
  219. package/dist/modules/notifications/lib/notificationMapper.js +2 -0
  220. package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
  221. package/dist/modules/notifications/lib/notificationPreferenceService.js +83 -0
  222. package/dist/modules/notifications/lib/notificationPreferenceService.js.map +7 -0
  223. package/dist/modules/notifications/lib/notificationService.js +83 -28
  224. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  225. package/dist/modules/notifications/lib/notificationVisibility.js +24 -0
  226. package/dist/modules/notifications/lib/notificationVisibility.js.map +7 -0
  227. package/dist/modules/notifications/lib/routeHelpers.js +11 -1
  228. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  229. package/dist/modules/notifications/lib/shouldDeliver.js +46 -0
  230. package/dist/modules/notifications/lib/shouldDeliver.js.map +7 -0
  231. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +54 -0
  232. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +7 -0
  233. package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js +12 -0
  234. package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js.map +7 -0
  235. package/dist/modules/notifications/lib/typeCatalogue.js +71 -0
  236. package/dist/modules/notifications/lib/typeCatalogue.js.map +7 -0
  237. package/dist/modules/notifications/lib/typeChannelSettings.js +12 -0
  238. package/dist/modules/notifications/lib/typeChannelSettings.js.map +7 -0
  239. package/dist/modules/notifications/lib/typeOverrides.js +19 -0
  240. package/dist/modules/notifications/lib/typeOverrides.js.map +7 -0
  241. package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js +14 -0
  242. package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js.map +7 -0
  243. package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js +15 -0
  244. package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js.map +7 -0
  245. package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js +13 -0
  246. package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js.map +7 -0
  247. package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js +15 -0
  248. package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js.map +7 -0
  249. package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js +13 -0
  250. package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js.map +7 -0
  251. package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js +14 -0
  252. package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js.map +7 -0
  253. package/dist/modules/notifications/notification-channels.js +26 -0
  254. package/dist/modules/notifications/notification-channels.js.map +7 -0
  255. package/dist/modules/notifications/notifications.delivery-strategies.js +12 -0
  256. package/dist/modules/notifications/notifications.delivery-strategies.js.map +7 -0
  257. package/dist/modules/notifications/setup.js +16 -1
  258. package/dist/modules/notifications/setup.js.map +2 -2
  259. package/dist/modules/notifications/subscribers/deliver-notification.js +66 -57
  260. package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
  261. package/dist/modules/notifications/subscribers/sync-notification-types.js +16 -0
  262. package/dist/modules/notifications/subscribers/sync-notification-types.js.map +7 -0
  263. package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js +23 -0
  264. package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js.map +7 -0
  265. package/dist/modules/notifications/widgets/injection-table.js +14 -0
  266. package/dist/modules/notifications/widgets/injection-table.js.map +7 -0
  267. package/dist/modules/push_notifications/acl.js +13 -0
  268. package/dist/modules/push_notifications/acl.js.map +7 -0
  269. package/dist/modules/push_notifications/api/custom-send/route.js +141 -0
  270. package/dist/modules/push_notifications/api/custom-send/route.js.map +7 -0
  271. package/dist/modules/push_notifications/api/deliveries/[id]/route.js +110 -0
  272. package/dist/modules/push_notifications/api/deliveries/[id]/route.js.map +7 -0
  273. package/dist/modules/push_notifications/api/deliveries/route.js +56 -0
  274. package/dist/modules/push_notifications/api/deliveries/route.js.map +7 -0
  275. package/dist/modules/push_notifications/api/openapi.js +19 -0
  276. package/dist/modules/push_notifications/api/openapi.js.map +7 -0
  277. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js +103 -0
  278. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js.map +7 -0
  279. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js +13 -0
  280. package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js.map +7 -0
  281. package/dist/modules/push_notifications/backend/push_notifications/page.js +213 -0
  282. package/dist/modules/push_notifications/backend/push_notifications/page.js.map +7 -0
  283. package/dist/modules/push_notifications/backend/push_notifications/page.meta.js +23 -0
  284. package/dist/modules/push_notifications/backend/push_notifications/page.meta.js.map +7 -0
  285. package/dist/modules/push_notifications/backend/push_notifications/send/page.js +245 -0
  286. package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +7 -0
  287. package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js +26 -0
  288. package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js.map +7 -0
  289. package/dist/modules/push_notifications/data/entities.js +92 -0
  290. package/dist/modules/push_notifications/data/entities.js.map +7 -0
  291. package/dist/modules/push_notifications/data/extensions.js +23 -0
  292. package/dist/modules/push_notifications/data/extensions.js.map +7 -0
  293. package/dist/modules/push_notifications/data/validators.js +89 -0
  294. package/dist/modules/push_notifications/data/validators.js.map +7 -0
  295. package/dist/modules/push_notifications/di.js +17 -0
  296. package/dist/modules/push_notifications/di.js.map +7 -0
  297. package/dist/modules/push_notifications/events.js +24 -0
  298. package/dist/modules/push_notifications/events.js.map +7 -0
  299. package/dist/modules/push_notifications/index.js +16 -0
  300. package/dist/modules/push_notifications/index.js.map +7 -0
  301. package/dist/modules/push_notifications/lib/fake-provider-recorder.js +80 -0
  302. package/dist/modules/push_notifications/lib/fake-provider-recorder.js.map +7 -0
  303. package/dist/modules/push_notifications/lib/push-delivery-strategy.js +54 -0
  304. package/dist/modules/push_notifications/lib/push-delivery-strategy.js.map +7 -0
  305. package/dist/modules/push_notifications/lib/push-delivery.js +297 -0
  306. package/dist/modules/push_notifications/lib/push-delivery.js.map +7 -0
  307. package/dist/modules/push_notifications/lib/push-fanout.js +138 -0
  308. package/dist/modules/push_notifications/lib/push-fanout.js.map +7 -0
  309. package/dist/modules/push_notifications/lib/push-health.js +32 -0
  310. package/dist/modules/push_notifications/lib/push-health.js.map +7 -0
  311. package/dist/modules/push_notifications/lib/push-reaper.js +87 -0
  312. package/dist/modules/push_notifications/lib/push-reaper.js.map +7 -0
  313. package/dist/modules/push_notifications/lib/push-receipt-reaper.js +136 -0
  314. package/dist/modules/push_notifications/lib/push-receipt-reaper.js.map +7 -0
  315. package/dist/modules/push_notifications/lib/push-stub-adapter.js +63 -0
  316. package/dist/modules/push_notifications/lib/push-stub-adapter.js.map +7 -0
  317. package/dist/modules/push_notifications/lib/queue.js +53 -0
  318. package/dist/modules/push_notifications/lib/queue.js.map +7 -0
  319. package/dist/modules/push_notifications/lib/reclaim-window.js +13 -0
  320. package/dist/modules/push_notifications/lib/reclaim-window.js.map +7 -0
  321. package/dist/modules/push_notifications/lib/send-custom-push.js +48 -0
  322. package/dist/modules/push_notifications/lib/send-custom-push.js.map +7 -0
  323. package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js +16 -0
  324. package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js.map +7 -0
  325. package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js +13 -0
  326. package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js.map +7 -0
  327. package/dist/modules/push_notifications/notifications.delivery-strategies.js +8 -0
  328. package/dist/modules/push_notifications/notifications.delivery-strategies.js.map +7 -0
  329. package/dist/modules/push_notifications/notifications.js +40 -0
  330. package/dist/modules/push_notifications/notifications.js.map +7 -0
  331. package/dist/modules/push_notifications/setup.js +52 -0
  332. package/dist/modules/push_notifications/setup.js.map +7 -0
  333. package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js +57 -0
  334. package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js.map +7 -0
  335. package/dist/modules/push_notifications/workers/send-push.worker.js +27 -0
  336. package/dist/modules/push_notifications/workers/send-push.worker.js.map +7 -0
  337. package/dist/modules/sales/notifications.js +8 -0
  338. package/dist/modules/sales/notifications.js.map +2 -2
  339. package/dist/modules/staff/notifications.js +6 -0
  340. package/dist/modules/staff/notifications.js.map +2 -2
  341. package/dist/modules/workflows/notifications.js +2 -0
  342. package/dist/modules/workflows/notifications.js.map +2 -2
  343. package/generated/entities/notification/index.ts +3 -0
  344. package/generated/entities/notification_preference/index.ts +8 -0
  345. package/generated/entities/notification_type/index.ts +9 -0
  346. package/generated/entities/notification_type_override/index.ts +7 -0
  347. package/generated/entities/push_notification_delivery/index.ts +19 -0
  348. package/generated/entities/user_device/index.ts +16 -0
  349. package/generated/entities.ids.generated.ts +12 -1
  350. package/generated/entity-fields-registry.ts +72 -0
  351. package/package.json +7 -7
  352. package/src/helpers/integration/api.ts +2 -1
  353. package/src/helpers/integration/appRoot.ts +13 -0
  354. package/src/helpers/integration/dbFixtures.ts +1 -5
  355. package/src/helpers/integration/pushFake.ts +219 -0
  356. package/src/modules/auth/i18n/de.json +1 -0
  357. package/src/modules/auth/i18n/en.json +1 -0
  358. package/src/modules/auth/i18n/es.json +1 -0
  359. package/src/modules/auth/i18n/ko.json +1 -0
  360. package/src/modules/auth/i18n/pl.json +1 -0
  361. package/src/modules/auth/notifications.ts +16 -0
  362. package/src/modules/business_rules/i18n/de.json +2 -1
  363. package/src/modules/business_rules/i18n/en.json +2 -1
  364. package/src/modules/business_rules/i18n/es.json +2 -1
  365. package/src/modules/business_rules/i18n/ko.json +2 -1
  366. package/src/modules/business_rules/i18n/pl.json +2 -1
  367. package/src/modules/business_rules/notifications.ts +2 -0
  368. package/src/modules/catalog/i18n/de.json +2 -1
  369. package/src/modules/catalog/i18n/en.json +2 -1
  370. package/src/modules/catalog/i18n/es.json +2 -1
  371. package/src/modules/catalog/i18n/ko.json +2 -1
  372. package/src/modules/catalog/i18n/pl.json +2 -1
  373. package/src/modules/catalog/notifications.ts +2 -0
  374. package/src/modules/communication_channels/acl.ts +9 -0
  375. package/src/modules/communication_channels/api/delete/admin/channels/[id]/route.ts +168 -0
  376. package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +6 -2
  377. package/src/modules/communication_channels/api/get/channels/[id]/route.ts +6 -2
  378. package/src/modules/communication_channels/api/get/channels/route.ts +2 -1
  379. package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +16 -5
  380. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +37 -1
  381. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +175 -0
  382. package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +11 -1
  383. package/src/modules/communication_channels/commands/admin-delete-channel.ts +203 -0
  384. package/src/modules/communication_channels/commands/connect-credential-channel.ts +63 -15
  385. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +10 -2
  386. package/src/modules/communication_channels/commands/push-register.ts +7 -1
  387. package/src/modules/communication_channels/commands/push-unregister.ts +14 -3
  388. package/src/modules/communication_channels/data/entities.ts +10 -0
  389. package/src/modules/communication_channels/data/validators.ts +1 -0
  390. package/src/modules/communication_channels/i18n/de.json +35 -1
  391. package/src/modules/communication_channels/i18n/en.json +35 -1
  392. package/src/modules/communication_channels/i18n/es.json +35 -1
  393. package/src/modules/communication_channels/i18n/ko.json +35 -1
  394. package/src/modules/communication_channels/i18n/pl.json +35 -1
  395. package/src/modules/communication_channels/lib/access-control.ts +42 -0
  396. package/src/modules/communication_channels/lib/adapter.ts +25 -0
  397. package/src/modules/communication_channels/lib/connect-channel.ts +61 -26
  398. package/src/modules/communication_channels/lib/push-adapter.ts +139 -0
  399. package/src/modules/communication_channels/lib/push-capabilities.ts +42 -0
  400. package/src/modules/communication_channels/lib/push-connect-error.ts +52 -0
  401. package/src/modules/communication_channels/lib/push-credential-errors.ts +58 -0
  402. package/src/modules/communication_channels/lib/push-envelope.ts +104 -0
  403. package/src/modules/communication_channels/lib/refcounted-client-cache.ts +123 -0
  404. package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +9 -0
  405. package/src/modules/communication_channels/migrations/Migration20260703123630_communication_channels.ts +20 -0
  406. package/src/modules/communication_channels/notifications.ts +4 -0
  407. package/src/modules/communication_channels/setup.ts +2 -0
  408. package/src/modules/configs/i18n/de.json +5 -1
  409. package/src/modules/configs/i18n/en.json +5 -1
  410. package/src/modules/configs/i18n/es.json +5 -1
  411. package/src/modules/configs/i18n/ko.json +5 -1
  412. package/src/modules/configs/i18n/pl.json +5 -1
  413. package/src/modules/configs/lib/upgrade-actions.ts +21 -0
  414. package/src/modules/customer_accounts/i18n/de.json +2 -1
  415. package/src/modules/customer_accounts/i18n/en.json +2 -1
  416. package/src/modules/customer_accounts/i18n/es.json +2 -1
  417. package/src/modules/customer_accounts/i18n/ko.json +2 -1
  418. package/src/modules/customer_accounts/i18n/pl.json +2 -1
  419. package/src/modules/customer_accounts/notifications.ts +12 -0
  420. package/src/modules/customers/i18n/de.json +2 -1
  421. package/src/modules/customers/i18n/en.json +2 -1
  422. package/src/modules/customers/i18n/es.json +2 -1
  423. package/src/modules/customers/i18n/ko.json +2 -1
  424. package/src/modules/customers/i18n/pl.json +2 -1
  425. package/src/modules/customers/notifications.ts +4 -0
  426. package/src/modules/devices/AGENTS.md +101 -0
  427. package/src/modules/devices/acl.ts +7 -0
  428. package/src/modules/devices/api/[id]/route.ts +180 -0
  429. package/src/modules/devices/api/admin/devices/[id]/route.ts +235 -0
  430. package/src/modules/devices/api/admin/devices/route.ts +160 -0
  431. package/src/modules/devices/api/auth.ts +13 -0
  432. package/src/modules/devices/api/deviceList.ts +54 -0
  433. package/src/modules/devices/api/deviceOps.ts +186 -0
  434. package/src/modules/devices/api/openapi.ts +23 -0
  435. package/src/modules/devices/api/route.ts +165 -0
  436. package/src/modules/devices/backend/devices/[id]/page.meta.ts +12 -0
  437. package/src/modules/devices/backend/devices/[id]/page.tsx +164 -0
  438. package/src/modules/devices/backend/devices/create/page.meta.ts +12 -0
  439. package/src/modules/devices/backend/devices/create/page.tsx +81 -0
  440. package/src/modules/devices/backend/devices/page.meta.ts +21 -0
  441. package/src/modules/devices/backend/devices/page.tsx +259 -0
  442. package/src/modules/devices/commands/deactivate.ts +126 -0
  443. package/src/modules/devices/commands/index.ts +3 -0
  444. package/src/modules/devices/commands/register.ts +199 -0
  445. package/src/modules/devices/commands/shared.ts +119 -0
  446. package/src/modules/devices/commands/update.ts +142 -0
  447. package/src/modules/devices/data/entities.ts +79 -0
  448. package/src/modules/devices/data/validators.ts +65 -0
  449. package/src/modules/devices/di.ts +9 -0
  450. package/src/modules/devices/encryption.ts +15 -0
  451. package/src/modules/devices/events.ts +16 -0
  452. package/src/modules/devices/i18n/de.json +45 -0
  453. package/src/modules/devices/i18n/en.json +45 -0
  454. package/src/modules/devices/i18n/es.json +45 -0
  455. package/src/modules/devices/i18n/ko.json +45 -0
  456. package/src/modules/devices/i18n/pl.json +45 -0
  457. package/src/modules/devices/index.ts +15 -0
  458. package/src/modules/devices/lib/operationMetadata.ts +35 -0
  459. package/src/modules/devices/migrations/.snapshot-open-mercato.json +200 -0
  460. package/src/modules/devices/migrations/Migration20260602120000.ts +15 -0
  461. package/src/modules/devices/migrations/Migration20260630120000.ts +20 -0
  462. package/src/modules/devices/migrations/Migration20260701120000.ts +13 -0
  463. package/src/modules/devices/migrations/Migration20260721120000.ts +18 -0
  464. package/src/modules/devices/setup.ts +11 -0
  465. package/src/modules/entities/cli.ts +16 -2
  466. package/src/modules/entities/migrations/Migration20260722120000.ts +38 -0
  467. package/src/modules/inbox_ops/i18n/de.json +2 -1
  468. package/src/modules/inbox_ops/i18n/en.json +2 -1
  469. package/src/modules/inbox_ops/i18n/es.json +2 -1
  470. package/src/modules/inbox_ops/i18n/ko.json +2 -1
  471. package/src/modules/inbox_ops/i18n/pl.json +2 -1
  472. package/src/modules/inbox_ops/notifications.ts +2 -0
  473. package/src/modules/messages/i18n/de.json +2 -1
  474. package/src/modules/messages/i18n/en.json +2 -1
  475. package/src/modules/messages/i18n/es.json +2 -1
  476. package/src/modules/messages/i18n/ko.json +2 -1
  477. package/src/modules/messages/i18n/pl.json +2 -1
  478. package/src/modules/messages/notifications.ts +2 -0
  479. package/src/modules/notifications/AGENTS.md +54 -0
  480. package/src/modules/notifications/acl.ts +2 -0
  481. package/src/modules/notifications/api/admin/preferences/route.ts +193 -0
  482. package/src/modules/notifications/api/channels/route.ts +54 -0
  483. package/src/modules/notifications/api/preferences/route.ts +131 -0
  484. package/src/modules/notifications/api/route.ts +8 -1
  485. package/src/modules/notifications/api/types/[id]/channels/[channel]/route.ts +226 -0
  486. package/src/modules/notifications/api/types/route.ts +239 -0
  487. package/src/modules/notifications/api/unread-count/route.ts +5 -1
  488. package/src/modules/notifications/backend/notifications/user-preferences/page.meta.ts +26 -0
  489. package/src/modules/notifications/backend/notifications/user-preferences/page.tsx +12 -0
  490. package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +31 -0
  491. package/src/modules/notifications/backend/profile/notification-preferences/page.tsx +12 -0
  492. package/src/modules/notifications/data/entities.ts +144 -0
  493. package/src/modules/notifications/data/validators.ts +116 -0
  494. package/src/modules/notifications/di.ts +7 -0
  495. package/src/modules/notifications/events.ts +23 -0
  496. package/src/modules/notifications/frontend/NotificationPreferenceMatrix.tsx +287 -0
  497. package/src/modules/notifications/frontend/NotificationPreferencesPageClient.tsx +155 -0
  498. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +273 -2
  499. package/src/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.tsx +214 -0
  500. package/src/modules/notifications/generators.ts +82 -0
  501. package/src/modules/notifications/i18n/de.json +43 -0
  502. package/src/modules/notifications/i18n/en.json +43 -0
  503. package/src/modules/notifications/i18n/es.json +43 -0
  504. package/src/modules/notifications/i18n/ko.json +43 -0
  505. package/src/modules/notifications/i18n/pl.json +44 -1
  506. package/src/modules/notifications/lib/delivery-strategies-registry.ts +30 -0
  507. package/src/modules/notifications/lib/deliveryStrategies.ts +40 -4
  508. package/src/modules/notifications/lib/derive-category.ts +15 -0
  509. package/src/modules/notifications/lib/notification-channel-registry.ts +59 -0
  510. package/src/modules/notifications/lib/notification-type-registry.ts +182 -0
  511. package/src/modules/notifications/lib/notificationCopy.ts +34 -0
  512. package/src/modules/notifications/lib/notificationFactory.ts +5 -1
  513. package/src/modules/notifications/lib/notificationMapper.ts +2 -0
  514. package/src/modules/notifications/lib/notificationPreferenceService.ts +138 -0
  515. package/src/modules/notifications/lib/notificationService.ts +129 -25
  516. package/src/modules/notifications/lib/notificationVisibility.ts +48 -0
  517. package/src/modules/notifications/lib/routeHelpers.ts +14 -1
  518. package/src/modules/notifications/lib/shouldDeliver.ts +112 -0
  519. package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +63 -0
  520. package/src/modules/notifications/lib/strategies/in-app-delivery-strategy.ts +21 -0
  521. package/src/modules/notifications/lib/typeCatalogue.ts +141 -0
  522. package/src/modules/notifications/lib/typeChannelSettings.ts +29 -0
  523. package/src/modules/notifications/lib/typeOverrides.ts +46 -0
  524. package/src/modules/notifications/migrations/.snapshot-open-mercato.json +874 -100
  525. package/src/modules/notifications/migrations/Migration20260625122947_notifications.ts +14 -0
  526. package/src/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.ts +15 -0
  527. package/src/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.ts +13 -0
  528. package/src/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.ts +15 -0
  529. package/src/modules/notifications/migrations/Migration20260706120000_notifications_channels.ts +13 -0
  530. package/src/modules/notifications/migrations/Migration20260716123121_notifications.ts +14 -0
  531. package/src/modules/notifications/notification-channels.ts +30 -0
  532. package/src/modules/notifications/notifications.delivery-strategies.ts +15 -0
  533. package/src/modules/notifications/setup.ts +23 -1
  534. package/src/modules/notifications/subscribers/deliver-notification.ts +105 -71
  535. package/src/modules/notifications/subscribers/sync-notification-types.ts +18 -0
  536. package/src/modules/notifications/widgets/injection/profile-preferences-menu/widget.ts +35 -0
  537. package/src/modules/notifications/widgets/injection-table.ts +18 -0
  538. package/src/modules/push_notifications/AGENTS.md +92 -0
  539. package/src/modules/push_notifications/acl.ts +9 -0
  540. package/src/modules/push_notifications/api/custom-send/route.ts +175 -0
  541. package/src/modules/push_notifications/api/deliveries/[id]/route.ts +122 -0
  542. package/src/modules/push_notifications/api/deliveries/route.ts +58 -0
  543. package/src/modules/push_notifications/api/openapi.ts +22 -0
  544. package/src/modules/push_notifications/backend/push_notifications/[id]/page.meta.ts +9 -0
  545. package/src/modules/push_notifications/backend/push_notifications/[id]/page.tsx +149 -0
  546. package/src/modules/push_notifications/backend/push_notifications/page.meta.ts +21 -0
  547. package/src/modules/push_notifications/backend/push_notifications/page.tsx +239 -0
  548. package/src/modules/push_notifications/backend/push_notifications/send/page.meta.ts +24 -0
  549. package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +327 -0
  550. package/src/modules/push_notifications/data/entities.ts +111 -0
  551. package/src/modules/push_notifications/data/extensions.ts +29 -0
  552. package/src/modules/push_notifications/data/validators.ts +102 -0
  553. package/src/modules/push_notifications/di.ts +20 -0
  554. package/src/modules/push_notifications/events.ts +21 -0
  555. package/src/modules/push_notifications/i18n/de.json +79 -0
  556. package/src/modules/push_notifications/i18n/en.json +79 -0
  557. package/src/modules/push_notifications/i18n/es.json +79 -0
  558. package/src/modules/push_notifications/i18n/ko.json +79 -0
  559. package/src/modules/push_notifications/i18n/pl.json +79 -0
  560. package/src/modules/push_notifications/index.ts +15 -0
  561. package/src/modules/push_notifications/lib/fake-provider-recorder.ts +154 -0
  562. package/src/modules/push_notifications/lib/push-delivery-strategy.ts +90 -0
  563. package/src/modules/push_notifications/lib/push-delivery.ts +455 -0
  564. package/src/modules/push_notifications/lib/push-fanout.ts +237 -0
  565. package/src/modules/push_notifications/lib/push-health.ts +47 -0
  566. package/src/modules/push_notifications/lib/push-reaper.ts +140 -0
  567. package/src/modules/push_notifications/lib/push-receipt-reaper.ts +210 -0
  568. package/src/modules/push_notifications/lib/push-stub-adapter.ts +80 -0
  569. package/src/modules/push_notifications/lib/queue.ts +74 -0
  570. package/src/modules/push_notifications/lib/reclaim-window.ts +16 -0
  571. package/src/modules/push_notifications/lib/send-custom-push.ts +100 -0
  572. package/src/modules/push_notifications/migrations/.snapshot-open-mercato.json +386 -0
  573. package/src/modules/push_notifications/migrations/Migration20260625150049_push_notifications.ts +16 -0
  574. package/src/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.ts +13 -0
  575. package/src/modules/push_notifications/notifications.delivery-strategies.ts +8 -0
  576. package/src/modules/push_notifications/notifications.ts +50 -0
  577. package/src/modules/push_notifications/setup.ts +92 -0
  578. package/src/modules/push_notifications/workers/reclaim-stuck.worker.ts +78 -0
  579. package/src/modules/push_notifications/workers/send-push.worker.ts +34 -0
  580. package/src/modules/sales/i18n/de.json +1 -0
  581. package/src/modules/sales/i18n/en.json +1 -0
  582. package/src/modules/sales/i18n/es.json +1 -0
  583. package/src/modules/sales/i18n/ko.json +1 -0
  584. package/src/modules/sales/i18n/pl.json +1 -0
  585. package/src/modules/sales/notifications.ts +8 -0
  586. package/src/modules/staff/i18n/de.json +1 -0
  587. package/src/modules/staff/i18n/en.json +1 -0
  588. package/src/modules/staff/i18n/es.json +1 -0
  589. package/src/modules/staff/i18n/ko.json +1 -0
  590. package/src/modules/staff/i18n/pl.json +1 -0
  591. package/src/modules/staff/notifications.ts +6 -0
  592. package/src/modules/workflows/i18n/de.json +1 -0
  593. package/src/modules/workflows/i18n/en.json +1 -0
  594. package/src/modules/workflows/i18n/es.json +1 -0
  595. package/src/modules/workflows/i18n/ko.json +1 -0
  596. package/src/modules/workflows/i18n/pl.json +1 -0
  597. package/src/modules/workflows/notifications.ts +2 -0
@@ -23,6 +23,15 @@ import {
23
23
  } from './notificationRecipients'
24
24
  import { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'
25
25
  import { createLogger } from '@open-mercato/shared/lib/logger'
26
+ import { getNotificationType } from './notification-type-registry'
27
+ import { getNotificationTypeOverrides, type NotificationTypeOverrides } from './typeOverrides'
28
+ import { getNotificationDeliveryStrategies } from './deliveryStrategies'
29
+ import { resolveEffectiveChannels } from './shouldDeliver'
30
+ import {
31
+ createNotificationPreferenceService,
32
+ type NotificationPreferenceService,
33
+ } from './notificationPreferenceService'
34
+ import { inAppVisibleFilter, inAppVisibleSql, isInAppVisible } from './notificationVisibility'
26
35
 
27
36
  const logger = createLogger('notifications').child({ component: 'service' })
28
37
 
@@ -83,6 +92,7 @@ function applyNotificationContent(
83
92
  input: NotificationContentInput,
84
93
  recipientUserId: string,
85
94
  ctx: NotificationTenantContext,
95
+ channels: string[] | null,
86
96
  ) {
87
97
  const actions = sanitizeNotificationActions(input.actions)
88
98
  const linkHref = assertSafeNotificationHref(input.linkHref)
@@ -108,6 +118,7 @@ function applyNotificationContent(
108
118
  notification.sourceEntityId = input.sourceEntityId
109
119
  notification.linkHref = linkHref
110
120
  notification.groupKey = input.groupKey
121
+ notification.channels = channels
111
122
  notification.expiresAt = input.expiresAt ? new Date(input.expiresAt) : null
112
123
  notification.tenantId = ctx.tenantId
113
124
  notification.organizationId = normalizeOrgScope(ctx.organizationId)
@@ -149,16 +160,21 @@ async function emitNotificationSseEvents(
149
160
  eventBus: { emit: (event: string, payload: unknown) => Promise<void> },
150
161
  notifications: Notification[],
151
162
  ctx: NotificationServiceContext,
152
- recipientUserIds: string[],
153
163
  ): Promise<void> {
164
+ // Live bell updates only for notifications actually delivered to the in-app channel; the others
165
+ // exist as records but must not bump anyone's badge/inbox.
166
+ const visible = notifications.filter((notification) => isInAppVisible(notification.channels))
167
+ if (visible.length === 0) return
168
+
169
+ const visibleRecipientUserIds = Array.from(new Set(visible.map((n) => n.recipientUserId)))
154
170
  await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {
155
171
  tenantId: ctx.tenantId,
156
172
  organizationId: normalizeOrgScope(ctx.organizationId),
157
- recipientUserIds,
158
- count: notifications.length,
173
+ recipientUserIds: visibleRecipientUserIds,
174
+ count: visible.length,
159
175
  })
160
176
 
161
- for (const notification of notifications) {
177
+ for (const notification of visible) {
162
178
  await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {
163
179
  tenantId: notification.tenantId,
164
180
  organizationId: notification.organizationId ?? null,
@@ -173,6 +189,7 @@ async function createOrRefreshNotification(
173
189
  input: NotificationContentInput,
174
190
  recipientUserId: string,
175
191
  ctx: NotificationTenantContext,
192
+ channels: string[] | null,
176
193
  ): Promise<Notification> {
177
194
  if (input.groupKey && input.groupKey.trim().length > 0) {
178
195
  const orgScope = normalizeOrgScope(ctx.organizationId) ?? 'global'
@@ -196,12 +213,12 @@ async function createOrRefreshNotification(
196
213
  })
197
214
 
198
215
  if (existing) {
199
- applyNotificationContent(existing, input, recipientUserId, ctx)
216
+ applyNotificationContent(existing, input, recipientUserId, ctx, channels)
200
217
  return existing
201
218
  }
202
219
  }
203
220
 
204
- return buildNotificationEntity(em, input, recipientUserId, ctx)
221
+ return buildNotificationEntity(em, input, recipientUserId, ctx, channels)
205
222
  }
206
223
 
207
224
  export interface NotificationServiceContext {
@@ -258,25 +275,94 @@ export interface NotificationServiceDeps {
258
275
  export function createNotificationService(deps: NotificationServiceDeps): NotificationService {
259
276
  const { em: rootEm, eventBus, commandBus, container } = deps
260
277
 
278
+ /**
279
+ * Resolves the authoritative delivery-channel set for one recipient at create time — the single
280
+ * gate that folds per-send target, per-type eligibility, registered strategies, and the
281
+ * recipient's per-channel preferences (`nonOptOut` bypasses opt-out). Stored on the row and
282
+ * replayed by the dispatcher; `in_app` membership also drives bell/inbox visibility.
283
+ *
284
+ * Returns `null` (⇒ "all channels", legacy behavior) when no delivery strategies are registered
285
+ * (e.g. a minimal bootstrap/test), so notifications never become silently undeliverable/invisible
286
+ * in an environment that simply hasn't wired the seam.
287
+ */
288
+ const resolveChannelsFor = async (
289
+ content: NotificationContentInput,
290
+ recipientUserId: string,
291
+ scopeCtx: NotificationServiceContext,
292
+ preferences: NotificationPreferenceService = createNotificationPreferenceService({ em: rootEm.fork() }),
293
+ typeOverrides?: NotificationTypeOverrides | null,
294
+ ): Promise<string[] | null> => {
295
+ const registeredChannels = getNotificationDeliveryStrategies().map((strategy) => strategy.id)
296
+ if (registeredChannels.length === 0) return null
297
+ // Treat an empty target as "no restriction" (all channels) rather than "no deliverable channel":
298
+ // a programmatic caller that computed an empty array should not silently black-hole the
299
+ // notification. The HTTP layer rejects an empty `channels` outright (see validators.ts).
300
+ const targetChannels = content.channels && content.channels.length > 0 ? content.channels : null
301
+ const overrides = typeOverrides === undefined
302
+ ? (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null
303
+ : typeOverrides
304
+ return resolveEffectiveChannels({
305
+ typeId: content.type,
306
+ type: getNotificationType(content.type),
307
+ scope: { tenantId: scopeCtx.tenantId, userId: recipientUserId },
308
+ targetChannels,
309
+ registeredChannels,
310
+ preferences,
311
+ channelsOverride: overrides?.channels ?? null,
312
+ nonOptOutOverride: overrides?.nonOptOut ?? null,
313
+ })
314
+ }
315
+
316
+ /**
317
+ * Broadcast counterpart of {@link resolveChannelsFor}: resolves the channel set for every recipient
318
+ * up front, BEFORE the write transaction opens, reusing a single forked EM / preference service
319
+ * across the whole set. This keeps the preference reads out of the write transaction (matching
320
+ * `create`, which resolves before its transaction) and avoids one EM fork per recipient on large
321
+ * role/feature broadcasts.
322
+ */
323
+ const resolveChannelsForRecipients = async (
324
+ content: NotificationContentInput,
325
+ recipientUserIds: string[],
326
+ scopeCtx: NotificationServiceContext,
327
+ ): Promise<Array<{ recipientUserId: string; channels: string[] | null }>> => {
328
+ const preferences = createNotificationPreferenceService({ em: rootEm.fork() })
329
+ // Stored overrides are per-type (not per-recipient) — read once for the whole broadcast.
330
+ const typeOverrides =
331
+ (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null
332
+ const resolved: Array<{ recipientUserId: string; channels: string[] | null }> = []
333
+ for (const recipientUserId of recipientUserIds) {
334
+ resolved.push({
335
+ recipientUserId,
336
+ channels: await resolveChannelsFor(content, recipientUserId, scopeCtx, preferences, typeOverrides),
337
+ })
338
+ }
339
+ return resolved
340
+ }
341
+
261
342
  return {
262
343
  async create(input, ctx) {
263
344
  const { recipientUserId, ...content } = input
345
+ const channels = await resolveChannelsFor(content, recipientUserId, ctx)
264
346
  const writeEm = rootEm.fork()
265
347
  const notification = await writeEm.transactional(async (tx) => {
266
348
  await assertNotificationRecipientsInScope(tx, [recipientUserId], ctx)
267
- const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
349
+ const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
268
350
  await tx.flush()
269
351
  return entity
270
352
  })
271
353
 
272
354
  await invalidateNotificationCache(container, ctx, 'created')
355
+ // Always emit the domain event (drives the deliver subscriber → push/email/…). Only bump the
356
+ // live in-app bell when this notification is actually visible in-app.
273
357
  await emitNotificationCreated(eventBus, notification, ctx)
274
- await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {
275
- tenantId: notification.tenantId,
276
- organizationId: notification.organizationId ?? null,
277
- recipientUserId: notification.recipientUserId,
278
- notification: toNotificationDto(notification),
279
- })
358
+ if (isInAppVisible(notification.channels)) {
359
+ await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {
360
+ tenantId: notification.tenantId,
361
+ organizationId: notification.organizationId ?? null,
362
+ recipientUserId: notification.recipientUserId,
363
+ notification: toNotificationDto(notification),
364
+ })
365
+ }
280
366
 
281
367
  return notification
282
368
  },
@@ -285,12 +371,13 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
285
371
  const recipientUserIds = Array.from(new Set(input.recipientUserIds))
286
372
  const { recipientUserIds: _recipientUserIds, ...content } = input
287
373
  const notifications: Notification[] = []
374
+ const resolved = await resolveChannelsForRecipients(content, recipientUserIds, ctx)
288
375
  const writeEm = rootEm.fork()
289
376
 
290
377
  await writeEm.transactional(async (tx) => {
291
378
  await assertNotificationRecipientsInScope(tx, recipientUserIds, ctx)
292
- for (const recipientUserId of recipientUserIds) {
293
- const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
379
+ for (const { recipientUserId, channels } of resolved) {
380
+ const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
294
381
  notifications.push(notification)
295
382
  }
296
383
  await tx.flush()
@@ -298,7 +385,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
298
385
 
299
386
  await invalidateNotificationCache(container, ctx, 'created')
300
387
  await emitNotificationCreatedBatch(eventBus, notifications, ctx)
301
- await emitNotificationSseEvents(eventBus, notifications, ctx, recipientUserIds)
388
+ await emitNotificationSseEvents(eventBus, notifications, ctx)
302
389
 
303
390
  return notifications
304
391
  },
@@ -315,11 +402,12 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
315
402
  const { roleId: _roleId, ...content } = input
316
403
  const notifications: Notification[] = []
317
404
  const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))
405
+ const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)
318
406
  const writeEm = rootEm.fork()
319
407
 
320
408
  await writeEm.transactional(async (tx) => {
321
- for (const recipientUserId of uniqueRecipientUserIds) {
322
- const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
409
+ for (const { recipientUserId, channels } of resolved) {
410
+ const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
323
411
  notifications.push(notification)
324
412
  }
325
413
  await tx.flush()
@@ -327,7 +415,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
327
415
 
328
416
  await invalidateNotificationCache(container, ctx, 'created')
329
417
  await emitNotificationCreatedBatch(eventBus, notifications, ctx)
330
- await emitNotificationSseEvents(eventBus, notifications, ctx, uniqueRecipientUserIds)
418
+ await emitNotificationSseEvents(eventBus, notifications, ctx)
331
419
 
332
420
  return notifications
333
421
  },
@@ -422,11 +510,12 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
422
510
  } = input
423
511
  const notifications: Notification[] = []
424
512
  const uniqueRecipientUserIds = Array.from(new Set(authorizedRecipientUserIds))
513
+ const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)
425
514
  const writeEm = rootEm.fork()
426
515
 
427
516
  await writeEm.transactional(async (tx) => {
428
- for (const recipientUserId of uniqueRecipientUserIds) {
429
- const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
517
+ for (const { recipientUserId, channels } of resolved) {
518
+ const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
430
519
  notifications.push(notification)
431
520
  }
432
521
  await tx.flush()
@@ -434,7 +523,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
434
523
 
435
524
  await invalidateNotificationCache(container, ctx, 'created')
436
525
  await emitNotificationCreatedBatch(eventBus, notifications, ctx)
437
- await emitNotificationSseEvents(eventBus, notifications, ctx, uniqueRecipientUserIds)
526
+ await emitNotificationSseEvents(eventBus, notifications, ctx)
438
527
 
439
528
  return notifications
440
529
  },
@@ -467,6 +556,12 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
467
556
  .where('recipient_user_id' as any, '=', ctx.userId as any)
468
557
  .where('tenant_id' as any, '=', ctx.tenantId)
469
558
  .where('status' as any, '=', 'unread')
559
+ // Only in-app-visible rows count toward the badge (see getUnreadCount),
560
+ // so "mark all as read" must scope to the SAME set — otherwise it flips
561
+ // push/email-only rows the user never saw and SSE-broadcasts a read for
562
+ // notifications that were never in the bell, inflating the returned
563
+ // count past what the badge showed.
564
+ .where(inAppVisibleSql() as any)
470
565
  if (ctx.organizationId) {
471
566
  chain = chain.where('organization_id' as any, '=', ctx.organizationId)
472
567
  }
@@ -707,7 +802,10 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
707
802
  recipientUserId: ctx.userId,
708
803
  tenantId: ctx.tenantId,
709
804
  status: 'unread',
710
- ...buildNotificationReadScopeWhere(ctx),
805
+ $and: [
806
+ buildNotificationReadScopeWhere(ctx),
807
+ inAppVisibleFilter(),
808
+ ],
711
809
  })
712
810
  },
713
811
 
@@ -716,7 +814,10 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
716
814
  const filters: Record<string, unknown> = {
717
815
  recipientUserId: ctx.userId,
718
816
  tenantId: ctx.tenantId,
719
- ...buildNotificationReadScopeWhere(ctx),
817
+ $and: [
818
+ buildNotificationReadScopeWhere(ctx),
819
+ inAppVisibleFilter(),
820
+ ],
720
821
  }
721
822
 
722
823
  if (since) {
@@ -732,7 +833,10 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
732
833
  recipientUserId: ctx.userId,
733
834
  tenantId: ctx.tenantId,
734
835
  status: 'unread',
735
- ...buildNotificationReadScopeWhere(ctx),
836
+ $and: [
837
+ buildNotificationReadScopeWhere(ctx),
838
+ inAppVisibleFilter(),
839
+ ],
736
840
  }),
737
841
  ])
738
842
 
@@ -0,0 +1,48 @@
1
+ import { sql, type RawBuilder } from 'kysely'
2
+ import { IN_APP_CHANNEL } from './strategies/in-app-delivery-strategy'
3
+
4
+ /**
5
+ * MikroORM filter fragment selecting notifications that should be VISIBLE in the in-app surfaces
6
+ * (bell, inbox list, unread count, poll). A row is visible when the `in_app` channel survived the
7
+ * create-time delivery gate — i.e. `in_app ∈ channels` — or when `channels` is NULL (legacy rows
8
+ * and any environment without the seam wired, which behave as "all channels" for backward
9
+ * compatibility). Rows targeted away from in-app (e.g. a push-only send, or a type/preference that
10
+ * excluded in_app) still exist as durable records but are hidden here.
11
+ *
12
+ * AND-composed with the caller's other conditions when merged into a filter object.
13
+ *
14
+ * `channels` is a JSONB column (`data/entities.ts`); MikroORM's `$contains` maps to the Postgres
15
+ * `@>` containment operator with a JSON-encoded operand — i.e. `channels @> '["in_app"]'` — so a row
16
+ * with `["in_app","email"]` matches and one with `["push"]` does not. Same operator MikroORM emits for
17
+ * the JSONB `$contains` filters already in production on `staff.role_ids` and `workflows.metadata`.
18
+ */
19
+ export function inAppVisibleFilter(): { $or: Array<Record<string, unknown>> } {
20
+ return {
21
+ $or: [
22
+ { channels: null },
23
+ { channels: { $contains: [IN_APP_CHANNEL] } },
24
+ ],
25
+ }
26
+ }
27
+
28
+ /**
29
+ * In-memory counterpart of {@link inAppVisibleFilter} for a single row's resolved channel set. Used
30
+ * to gate the live bell SSE emit so a notification suppressed from the in-app channel does not push a
31
+ * real-time badge/inbox update. `null`/`undefined` channels ⇒ visible (legacy / all-channels).
32
+ */
33
+ export function isInAppVisible(channels: string[] | null | undefined): boolean {
34
+ return channels == null || channels.includes(IN_APP_CHANNEL)
35
+ }
36
+
37
+ /**
38
+ * Raw-SQL counterpart of {@link inAppVisibleFilter} for queries built with Kysely
39
+ * (which cannot consume MikroORM's `$contains` fragment). Emits
40
+ * `(<column> IS NULL OR <column> @> '["in_app"]'::jsonb)` — the exact predicate
41
+ * MikroORM produces — so raw and ORM paths stay in lock-step. `columnRef` MUST be
42
+ * a trusted, code-controlled column identifier (never user input).
43
+ */
44
+ export function inAppVisibleSql(columnRef = 'channels'): RawBuilder<boolean> {
45
+ return sql<boolean>`(${sql.ref(columnRef)} is null or ${sql.ref(columnRef)} @> ${sql.val(
46
+ JSON.stringify([IN_APP_CHANNEL]),
47
+ )}::jsonb)`
48
+ }
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod'
2
2
  import type { AwilixContainer } from 'awilix'
3
3
  import { resolveRequestContext } from '@open-mercato/shared/lib/api/context'
4
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
4
5
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
6
  import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
6
7
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
- import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
8
8
  import { resolveNotificationService, type NotificationService } from './notificationService'
9
9
 
10
10
  /**
@@ -17,6 +17,11 @@ export const NOTIFICATION_RESOURCE_KIND = 'notifications.notification'
17
17
  */
18
18
  export const NOTIFICATION_SETTINGS_RESOURCE_KIND = 'notifications.settings'
19
19
 
20
+ /**
21
+ * Mutation-guard resource kind for a user's channel preferences.
22
+ */
23
+ export const NOTIFICATION_PREFERENCE_RESOURCE_KIND = 'notifications.preference'
24
+
20
25
  /**
21
26
  * Notification scope context for service calls
22
27
  */
@@ -120,6 +125,14 @@ export async function resolveNotificationContext(req: Request): Promise<Notifica
120
125
  service: resolveNotificationService(ctx.container),
121
126
  scope: {
122
127
  tenantId,
128
+ // The *creator's* org (the shared resolver already falls back to the caller's own org when
129
+ // nothing is selected), and the push strategy later scopes the recipient's device lookup to
130
+ // it. Same-org and self-notify (the common paths) match; a cross-org notify — an admin in
131
+ // org A notifying a user whose devices live in org B — won't match the recipient's devices,
132
+ // so push is silently dropped for that case. Same for a deliberate all-organizations
133
+ // selection, which stays tenant-level (org=null) and so only reaches null-org devices.
134
+ // In-app delivery is unaffected. If cross-org push is ever required, scope the device
135
+ // lookup by the recipient's org instead.
123
136
  organizationId,
124
137
  organizationIds,
125
138
  userId: ctx.auth?.sub ?? null,
@@ -0,0 +1,112 @@
1
+ import { createLogger } from '@open-mercato/shared/lib/logger'
2
+ import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
3
+ import type { NotificationPreferenceScope } from './notificationPreferenceService'
4
+
5
+ const logger = createLogger('notifications').child({ component: 'delivery-gate' })
6
+
7
+ // One warning per unregistered type id per process — unregistered ids are rare (a renamed/removed type
8
+ // still referenced by an in-flight notification), so the set stays tiny and the log never floods.
9
+ const warnedUnregisteredTypes = new Set<string>()
10
+
11
+ /**
12
+ * The single per-channel delivery gate. Every channel (`in_app`, `email`, `push`, custom) is
13
+ * governed by this one function so opt-out / eligibility / targeting enforcement can never be
14
+ * bypassed or re-implemented inconsistently per strategy.
15
+ *
16
+ * A channel is delivered when ALL hold:
17
+ * 1. it is a registered strategy id (`registeredChannels`),
18
+ * 2. the type is eligible for it — the operator's stored override
19
+ * (`notification_types.channels`, when set) or the code-declared `type.channels`;
20
+ * this runs BEFORE the `nonOptOut` bypass and before user preferences, so a channel
21
+ * outside the effective set is completely off for the type,
22
+ * 3. it is in the per-send target (`targetChannels`, when provided),
23
+ * 4. either the type is effectively `nonOptOut` (operator override ?? code flag), or the
24
+ * recipient has not disabled it (`isChannelEnabled`).
25
+ *
26
+ * `silent` is intentionally NOT consulted here — it selects push delivery STYLE, not whether a
27
+ * channel delivers at all. Absent `type.channels` and absent `targetChannels` both mean "no
28
+ * restriction", so a fully-default call resolves to every registered channel (pre-Phase-7 behavior).
29
+ */
30
+ export type ChannelPreferenceReader = {
31
+ isChannelEnabled(scope: NotificationPreferenceScope, typeId: string, channel: string): Promise<boolean>
32
+ }
33
+
34
+ /**
35
+ * The effective eligibility set for a type: the operator's stored override replaces the
36
+ * code-declared `type.channels`; `null`/absent on both ⇒ no restriction (every channel).
37
+ */
38
+ export function resolveEligibleChannels(
39
+ type: NotificationTypeDefinition | undefined,
40
+ channelsOverride: string[] | null | undefined,
41
+ ): string[] | null {
42
+ return channelsOverride ?? type?.channels ?? null
43
+ }
44
+
45
+ export type ShouldDeliverParams = {
46
+ /** The notification's `type` string (used for the preference lookup even when unregistered). */
47
+ typeId: string
48
+ /** The registered type definition, when the type id is known. Supplies eligibility + nonOptOut. */
49
+ type: NotificationTypeDefinition | undefined
50
+ channel: string
51
+ scope: NotificationPreferenceScope
52
+ /** Per-send channel target. `null`/`undefined` → no target restriction. */
53
+ targetChannels?: string[] | null
54
+ /** Ids of the currently registered delivery strategies. */
55
+ registeredChannels: string[]
56
+ preferences: ChannelPreferenceReader
57
+ /**
58
+ * Operator override of the type's eligibility from `notification_types.channels`.
59
+ * `undefined`/`null` ⇒ no override; the code-declared `type.channels` applies.
60
+ */
61
+ channelsOverride?: string[] | null
62
+ /**
63
+ * Operator override of the type's `nonOptOut` flag from `notification_types.non_opt_out`.
64
+ * `undefined`/`null` ⇒ no override; the code-declared flag applies.
65
+ */
66
+ nonOptOutOverride?: boolean | null
67
+ }
68
+
69
+ export async function shouldDeliver(params: ShouldDeliverParams): Promise<boolean> {
70
+ const {
71
+ typeId, type, channel, scope, targetChannels, registeredChannels, preferences,
72
+ channelsOverride, nonOptOutOverride,
73
+ } = params
74
+
75
+ if (!registeredChannels.includes(channel)) return false
76
+ const eligible = resolveEligibleChannels(type, channelsOverride)
77
+ if (eligible && !eligible.includes(channel)) return false
78
+ if (targetChannels && !targetChannels.includes(channel)) return false
79
+ if ((nonOptOutOverride ?? type?.nonOptOut) === true) return true
80
+
81
+ // The type id resolved to no registered definition (renamed/removed type), so its code-declared
82
+ // `nonOptOut` and eligibility cannot be enforced here — a security type that was `nonOptOut` becomes
83
+ // silently user-suppressible. Surface that once so it is not invisible (there is no operator override
84
+ // in play either, else the branch above would have returned).
85
+ if (!type && nonOptOutOverride == null && !warnedUnregisteredTypes.has(typeId)) {
86
+ warnedUnregisteredTypes.add(typeId)
87
+ logger.warn(
88
+ 'Delivery gate evaluated an unregistered notification type; its code-declared nonOptOut/eligibility cannot be enforced and it is subject to user opt-out',
89
+ { typeId },
90
+ )
91
+ }
92
+
93
+ return preferences.isChannelEnabled(scope, typeId, channel)
94
+ }
95
+
96
+ export type ResolveEffectiveChannelsParams = Omit<ShouldDeliverParams, 'channel'>
97
+
98
+ /**
99
+ * Resolves the authoritative channel set for a notification at create time: every registered
100
+ * channel that passes {@link shouldDeliver}. Stored on `Notification.channels` and looped by the
101
+ * dispatcher; `in_app` membership also gates bell/inbox visibility.
102
+ */
103
+ export async function resolveEffectiveChannels(
104
+ params: ResolveEffectiveChannelsParams,
105
+ ): Promise<string[]> {
106
+ const results = await Promise.all(
107
+ params.registeredChannels.map(async (channel) =>
108
+ (await shouldDeliver({ ...params, channel })) ? channel : null,
109
+ ),
110
+ )
111
+ return results.filter((channel): channel is string => channel !== null)
112
+ }
@@ -0,0 +1,63 @@
1
+ import { sendEmail } from '@open-mercato/shared/lib/email/send'
2
+ import { createLogger } from '@open-mercato/shared/lib/logger'
3
+ import NotificationEmail from '../../emails/NotificationEmail'
4
+ import type { NotificationDeliveryContext, NotificationDeliveryStrategy } from '../deliveryStrategies'
5
+
6
+ export const EMAIL_CHANNEL = 'email'
7
+
8
+ const logger = createLogger('notifications')
9
+
10
+ const DEBUG = process.env.NOTIFICATIONS_DEBUG === 'true'
11
+
12
+ function debug(msg: string, fields?: Record<string, unknown>): void {
13
+ if (DEBUG) logger.debug(msg, fields)
14
+ }
15
+
16
+ /**
17
+ * Email delivery as a first-class strategy on the seam (previously hard-coded inline in the dispatch
18
+ * subscriber). Per-user opt-out and `nonOptOut`/`silent` are enforced upstream at create time via
19
+ * `shouldDeliver`; this strategy only owns technical deliverability: it runs when email is enabled by
20
+ * tenant config (`isConfigured`) and there is both a recipient address and a panel deep link.
21
+ */
22
+ export const emailDeliveryStrategy: NotificationDeliveryStrategy = {
23
+ id: EMAIL_CHANNEL,
24
+ defaultEnabled: true,
25
+ isConfigured: (ctx: NotificationDeliveryContext) => ctx.deliveryConfig.strategies.email.enabled === true,
26
+ async deliver(ctx: NotificationDeliveryContext) {
27
+ const { recipient, panelLink, title, body, actionLinks, deliveryConfig, t } = ctx
28
+ if (!recipient?.email || !panelLink) {
29
+ debug('email skipped: missing recipient email or panelLink')
30
+ return
31
+ }
32
+
33
+ const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim()
34
+ const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title
35
+ const copy = {
36
+ preview: t('notifications.delivery.email.preview', 'New notification'),
37
+ heading: t('notifications.delivery.email.heading', 'You have a new notification'),
38
+ bodyIntro: t('notifications.delivery.email.bodyIntro', 'Review the notification details and take any required actions.'),
39
+ actionNotice: t('notifications.delivery.email.actionNotice', 'Actions are available in Open Mercato and are read-only in this email.'),
40
+ openCta: t('notifications.delivery.email.openCta', 'Open notification center'),
41
+ footer: t('notifications.delivery.email.footer', 'Open Mercato notifications'),
42
+ }
43
+
44
+ try {
45
+ debug('sending email', { to: recipient.email, from: deliveryConfig.strategies.email.from, subject })
46
+ await sendEmail({
47
+ to: recipient.email,
48
+ subject,
49
+ from: deliveryConfig.strategies.email.from,
50
+ replyTo: deliveryConfig.strategies.email.replyTo,
51
+ react: NotificationEmail({
52
+ title,
53
+ body,
54
+ actions: actionLinks,
55
+ panelUrl: panelLink,
56
+ copy,
57
+ }),
58
+ })
59
+ } catch (error) {
60
+ logger.error('email delivery failed', { error })
61
+ }
62
+ },
63
+ }
@@ -0,0 +1,21 @@
1
+ import type { NotificationDeliveryStrategy } from '../deliveryStrategies'
2
+
3
+ export const IN_APP_CHANNEL = 'in_app'
4
+
5
+ /**
6
+ * In-app is a first-class channel of the delivery seam, but its "delivery" is the `Notification` row
7
+ * that `notificationService.create()` writes synchronously (the durable record that push/email
8
+ * reference and that the bell renders). There is nothing to send here.
9
+ *
10
+ * Whether the row is *visible* in the bell/inbox is governed by `in_app ∈ notification.channels`
11
+ * (resolved at create time by `shouldDeliver`) at the read layer — not by this strategy. Registering
12
+ * it makes in_app appear in the strategy registry so the create-time gate and the dispatcher treat it
13
+ * exactly like every other channel, with no channel-specific branch in the dispatcher.
14
+ */
15
+ export const inAppDeliveryStrategy: NotificationDeliveryStrategy = {
16
+ id: IN_APP_CHANNEL,
17
+ defaultEnabled: true,
18
+ deliver: () => {
19
+ /* no-op: the row IS the in-app delivery; visibility is a read-layer concern */
20
+ },
21
+ }