@open-mercato/core 0.6.8-develop.6983.1.e4f794099e → 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
@@ -77,6 +77,21 @@ export class Notification {
77
77
  @Property({ name: 'group_key', type: 'text', nullable: true })
78
78
  groupKey?: string | null
79
79
 
80
+ // Arbitrary app-readable key/values delivered with the push payload and exposed to in-app clients.
81
+ @Property({ name: 'data', type: 'json', nullable: true })
82
+ data?: Record<string, string> | null
83
+
84
+ // Per-provider push customization (sound/badge/image/priority/channelId/body); push-only.
85
+ // Typed loosely to keep notifications decoupled from communication_channels' PushOptions.
86
+ @Property({ name: 'push_options', type: 'json', nullable: true })
87
+ pushOptions?: Record<string, unknown> | null
88
+
89
+ // Resolved delivery channels for this notification (target ∩ type-eligibility ∩ registered ∩ preference).
90
+ // Authoritative target filter the dispatcher loops over; `in_app` membership also gates bell/inbox visibility.
91
+ // NULL = legacy/all-channels (delivered and visible everywhere) — preserves pre-Phase-7 behavior.
92
+ @Property({ name: 'channels', type: 'json', nullable: true })
93
+ channels?: string[] | null
94
+
80
95
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
81
96
  createdAt: Date = new Date()
82
97
 
@@ -98,3 +113,132 @@ export class Notification {
98
113
  @Property({ name: 'organization_id', type: 'uuid', nullable: true })
99
114
  organizationId?: string | null
100
115
  }
116
+
117
+ /**
118
+ * DB-backed mirror of the code-registered notification type catalogue
119
+ * (the in-memory `NotificationTypeDefinition` seam stays the source of truth).
120
+ * Lets remote clients (mobile apps) enumerate types over HTTP to render a
121
+ * preferences screen without shipping a copy of the catalogue.
122
+ *
123
+ * `tenant_id` is nullable: code-registered types are system-wide (`null`).
124
+ * The string `id` is the frozen notification-type id (e.g. `sales.order.created`).
125
+ */
126
+ @Entity({ tableName: 'notification_types' })
127
+ @Index({ name: 'notification_types_tenant_idx', properties: ['tenantId'] })
128
+ export class NotificationType {
129
+ [OptionalProps]?: 'tenantId' | 'descriptionKey' | 'category' | 'silent' | 'nonOptOut' | 'createdAt' | 'updatedAt'
130
+
131
+ @PrimaryKey({ name: 'id', type: 'text' })
132
+ id!: string
133
+
134
+ @Property({ name: 'tenant_id', type: 'uuid', nullable: true })
135
+ tenantId?: string | null
136
+
137
+ @Property({ name: 'label_key', type: 'text' })
138
+ labelKey!: string
139
+
140
+ @Property({ name: 'description_key', type: 'text', nullable: true })
141
+ descriptionKey?: string | null
142
+
143
+ @Property({ name: 'category', type: 'text', nullable: true })
144
+ category?: string | null
145
+
146
+ @Property({ name: 'silent', type: 'boolean', default: false })
147
+ silent: boolean = false
148
+
149
+ @Property({ name: 'non_opt_out', type: 'boolean', default: false })
150
+ nonOptOut: boolean = false
151
+
152
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
153
+ createdAt: Date = new Date()
154
+
155
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
156
+ updatedAt: Date = new Date()
157
+ }
158
+
159
+ /**
160
+ * Tenant-scoped operator override of a notification type's delivery contract.
161
+ * Lazy-seeded like `NotificationPreference`: an absent row means the code
162
+ * declarations apply unchanged. `channels` REPLACES the code-declared
163
+ * `NotificationTypeDefinition.channels` when set (`null` inherits it) — a
164
+ * channel outside the effective set never delivers for the type in this tenant
165
+ * (checked before both the `nonOptOut` bypass and user preferences) and users
166
+ * cannot opt into it; the preference UIs render the cell locked off.
167
+ * `nonOptOut` overrides the code-declared flag the same way: `true` forces the
168
+ * type on for the tenant's users, `false` makes a code-required type
169
+ * user-editable, `null` inherits. The `notification_type_id` is a soft string
170
+ * reference to a `notification_types.id` (no cross-module ORM relationship);
171
+ * `syncNotificationTypes` never touches this table, so operator edits survive
172
+ * catalogue re-syncs.
173
+ */
174
+ @Entity({ tableName: 'notification_type_overrides' })
175
+ @Index({
176
+ name: 'notification_type_overrides_unique',
177
+ expression:
178
+ 'create unique index "notification_type_overrides_unique" on "notification_type_overrides" ("tenant_id", "notification_type_id");',
179
+ })
180
+ export class NotificationTypeOverride {
181
+ [OptionalProps]?: 'createdAt' | 'updatedAt'
182
+
183
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
184
+ id!: string
185
+
186
+ @Property({ name: 'tenant_id', type: 'uuid' })
187
+ tenantId!: string
188
+
189
+ @Property({ name: 'notification_type_id', type: 'text' })
190
+ notificationTypeId!: string
191
+
192
+ @Property({ name: 'channels', type: 'json', nullable: true })
193
+ channels?: string[] | null
194
+
195
+ @Property({ name: 'non_opt_out', type: 'boolean', nullable: true })
196
+ nonOptOut?: boolean | null
197
+
198
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
199
+ createdAt: Date = new Date()
200
+
201
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
202
+ updatedAt: Date = new Date()
203
+ }
204
+
205
+ /**
206
+ * Channel-agnostic per-user notification preference. Lazy-seeded: an absent row
207
+ * means the channel is enabled (default-on). Channels (in_app, push, future
208
+ * email/sms) are free-form strings; the `notification_type_id` is a soft string
209
+ * reference to a `notification_types.id` (no cross-module ORM relationship).
210
+ */
211
+ @Entity({ tableName: 'notification_preferences' })
212
+ @Index({ name: 'notification_preferences_tenant_user_idx', properties: ['tenantId', 'userId'] })
213
+ @Index({
214
+ name: 'notification_preferences_unique',
215
+ expression:
216
+ 'create unique index "notification_preferences_unique" on "notification_preferences" ("tenant_id", "user_id", "notification_type_id", "channel");',
217
+ })
218
+ export class NotificationPreference {
219
+ [OptionalProps]?: 'enabled' | 'createdAt' | 'updatedAt'
220
+
221
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
222
+ id!: string
223
+
224
+ @Property({ name: 'tenant_id', type: 'uuid' })
225
+ tenantId!: string
226
+
227
+ @Property({ name: 'user_id', type: 'uuid' })
228
+ userId!: string
229
+
230
+ @Property({ name: 'notification_type_id', type: 'text' })
231
+ notificationTypeId!: string
232
+
233
+ @Property({ name: 'channel', type: 'text' })
234
+ channel!: string
235
+
236
+ @Property({ name: 'enabled', type: 'boolean', default: true })
237
+ enabled: boolean = true
238
+
239
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
240
+ createdAt: Date = new Date()
241
+
242
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
243
+ updatedAt: Date = new Date()
244
+ }
@@ -21,6 +21,22 @@ export const notificationActionSchema = z.object({
21
21
  confirmMessage: z.string().optional(),
22
22
  })
23
23
 
24
+ /**
25
+ * Flat per-provider push customization. Known keys are mapped onto each provider's native message
26
+ * by the push adapters; unknown keys are accepted (passthrough) for providers that understand them.
27
+ * See `communication_channels/lib/push-envelope.ts` (`PushOptions`).
28
+ */
29
+ export const pushOptionsSchema = z
30
+ .object({
31
+ sound: z.string().optional(),
32
+ badge: z.number().int().nonnegative().optional(),
33
+ image: z.string().optional(),
34
+ priority: z.enum(['high', 'normal']).optional(),
35
+ channelId: z.string().optional(),
36
+ body: z.string().optional(),
37
+ })
38
+ .catchall(z.unknown())
39
+
24
40
  const baseNotificationFieldsSchema = z.object({
25
41
  type: z.string().min(1).max(100),
26
42
  titleKey: z.string().min(1).max(200).optional(),
@@ -39,6 +55,16 @@ const baseNotificationFieldsSchema = z.object({
39
55
  linkHref: safeRelativeHrefSchema.optional(),
40
56
  groupKey: z.string().optional(),
41
57
  expiresAt: z.string().datetime().optional(),
58
+ // Arbitrary app-readable key/values delivered with the push (and visible to in-app clients).
59
+ data: z.record(z.string(), z.string()).optional(),
60
+ // Per-provider push customization (sound, badge, image, priority, channelId, body override).
61
+ pushOptions: pushOptionsSchema.optional(),
62
+ // Explicit per-send channel target. When present, delivery is restricted to these channels
63
+ // (intersected with the type's eligibility, the registered strategies, and the recipient's
64
+ // preferences). Absent → all registered channels (pre-Phase-7 behavior). Must be non-empty when
65
+ // provided: an empty array would resolve to zero deliverable channels and silently black-hole the
66
+ // notification (invisible + undelivered) — omit the field instead to target every channel.
67
+ channels: z.array(z.string().min(1).max(32)).min(1).optional(),
42
68
  })
43
69
 
44
70
  const titleRequiredRefinement = {
@@ -113,3 +139,93 @@ export type CreateFeatureNotificationInput = z.infer<typeof createFeatureNotific
113
139
  export type ListNotificationsInput = z.infer<typeof listNotificationsSchema>
114
140
  export type ExecuteActionInput = z.infer<typeof executeActionSchema>
115
141
  export type NotificationDeliveryConfigInput = z.infer<typeof notificationDeliveryConfigSchema>
142
+
143
+ // Notification type catalogue (DB-mirrored read model)
144
+ export const notificationTypeItemSchema = z.object({
145
+ id: z.string(),
146
+ labelKey: z.string(),
147
+ descriptionKey: z.string().nullable().optional(),
148
+ // Free-form grouping label so a client can list/group types under a heading. Defaults to
149
+ // the prefix before the first dot in the type id (`sales.order.created` → `sales`), so in
150
+ // practice it is always populated; kept nullable to avoid narrowing the response contract.
151
+ // This is the STABLE grouping key — clients group on it and display `categoryLabel`.
152
+ category: z.string().nullable().optional(),
153
+ // Localized heading for `category`, resolved server-side from
154
+ // `notifications.categories.<key>` with a humanized fallback. Display-only: grouping on it
155
+ // re-partitions the list whenever the locale changes.
156
+ categoryLabel: z.string().nullable(),
157
+ // Server-resolved display strings so clients without the app dictionary (the mobile app)
158
+ // need no i18n bundle. `null` iff the corresponding `*Key` is null.
159
+ label: z.string().nullable(),
160
+ description: z.string().nullable(),
161
+ // When true the type is delivered as a silent / content-available push.
162
+ silent: z.boolean(),
163
+ // Effective "cannot be opted out of" flag (operator override ?? code-declared); a preferences
164
+ // UI should lock the type on when true.
165
+ nonOptOut: z.boolean(),
166
+ // Effective channel eligibility for the caller's tenant (stored override ?? code-declared
167
+ // `type.channels`). `null` = no restriction (every registered channel). A channel outside
168
+ // the set never delivers for this type in the tenant and users cannot opt into it —
169
+ // preference UIs lock the cell off.
170
+ channels: z.array(z.string()).nullable(),
171
+ // The raw tenant-stored override (`notification_type_overrides.channels`; `null` = inherit
172
+ // code). Admin editors base PATCH payloads on the EFFECTIVE set, but this shows whether an
173
+ // override exists at all.
174
+ storedChannels: z.array(z.string()).nullable(),
175
+ // The raw tenant-stored `nonOptOut` override (`null` = inherit the code-declared flag).
176
+ storedNonOptOut: z.boolean().nullable(),
177
+ // Optimistic-lock version of the tenant's override row (ISO timestamp; `null` when the
178
+ // tenant stores no override yet). PATCH callers echo it back via the standard
179
+ // `x-om-ext-optimistic-lock-expected-updated-at` header.
180
+ updatedAt: z.string().nullable(),
181
+ })
182
+
183
+ // PATCH /api/notifications/types — operator override of a type's channel eligibility and/or
184
+ // nonOptOut governance. Omitted fields stay untouched; `null` clears the stored override so
185
+ // the code declaration applies again.
186
+ export const updateNotificationTypeSchema = z
187
+ .object({
188
+ id: z.string().min(1),
189
+ // Mirror the create guard: a non-empty array restricts eligibility, `null` clears the override
190
+ // (code declaration reapplies). An empty array is rejected — it would resolve to zero deliverable
191
+ // channels and silently black-hole the type (invisible + undelivered); clear with `null` instead.
192
+ channels: z.array(z.string().min(1)).min(1).nullable().optional(),
193
+ nonOptOut: z.boolean().nullable().optional(),
194
+ })
195
+ .refine((value) => value.channels !== undefined || value.nonOptOut !== undefined, {
196
+ message: 'At least one of channels or nonOptOut must be provided',
197
+ })
198
+
199
+ // Per-user channel preferences
200
+ export const notificationPreferenceItemSchema = z.object({
201
+ notificationTypeId: z.string(),
202
+ channel: z.string(),
203
+ enabled: z.boolean(),
204
+ })
205
+
206
+ export const updatePreferencesSchema = z.object({
207
+ preferences: z
208
+ .array(
209
+ z.object({
210
+ notificationTypeId: z.string().trim().min(1).max(128),
211
+ channel: z.string().trim().min(1).max(32),
212
+ enabled: z.boolean(),
213
+ }),
214
+ )
215
+ .max(500),
216
+ })
217
+
218
+ // Admin-on-behalf preference management (target a specific user)
219
+ export const adminPreferencesQuerySchema = z.object({
220
+ userId: z.string().uuid(),
221
+ })
222
+
223
+ export const adminUpdatePreferencesSchema = updatePreferencesSchema.extend({
224
+ userId: z.string().uuid(),
225
+ })
226
+
227
+ export type NotificationTypeItem = z.infer<typeof notificationTypeItemSchema>
228
+ export type UpdateNotificationTypeInput = z.infer<typeof updateNotificationTypeSchema>
229
+ export type NotificationPreferenceItem = z.infer<typeof notificationPreferenceItemSchema>
230
+ export type UpdatePreferencesInput = z.infer<typeof updatePreferencesSchema>
231
+ export type AdminUpdatePreferencesInput = z.infer<typeof adminUpdatePreferencesSchema>
@@ -1,11 +1,18 @@
1
1
  import type { AppContainer } from '@open-mercato/shared/lib/di/container'
2
2
  import { asFunction } from 'awilix'
3
3
  import { createNotificationService } from './lib/notificationService'
4
+ import { createNotificationPreferenceService } from './lib/notificationPreferenceService'
4
5
 
5
6
  export function register(container: AppContainer): void {
6
7
  container.register({
7
8
  notificationService: asFunction(({ em, eventBus, commandBus }) =>
8
9
  createNotificationService({ em, eventBus, commandBus, container })
9
10
  ).scoped().proxy(),
11
+ // Cross-module/worker seam advertised by AGENTS.md ("DI: notificationPreferenceService").
12
+ // In-module routes resolve via resolveNotificationPreferenceService() (news up from the
13
+ // request-scoped em) since the request container does not expose this binding.
14
+ notificationPreferenceService: asFunction(({ em }) =>
15
+ createNotificationPreferenceService({ em })
16
+ ).scoped().proxy(),
10
17
  })
11
18
  }
@@ -1,6 +1,16 @@
1
1
  import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
2
  import { NOTIFICATION_SSE_EVENTS } from './lib/events'
3
3
 
4
+ /**
5
+ * Reconcile the code-registered notification type catalogue into the
6
+ * `notification_types` table. Emitted at tenant init; consumed by
7
+ * `subscribers/sync-notification-types.ts`.
8
+ */
9
+ export const TYPE_REGISTRY_SYNC_EVENT = 'notifications.type_registry.sync'
10
+
11
+ /** Lifecycle event emitted after a user's channel preferences change (workflow-trigger / subscriber surface). */
12
+ export const PREFERENCE_UPDATED_EVENT = 'notifications.preference.updated'
13
+
4
14
  const events = [
5
15
  {
6
16
  id: NOTIFICATION_SSE_EVENTS.CREATED,
@@ -18,6 +28,19 @@ const events = [
18
28
  clientBroadcast: true,
19
29
  portalBroadcast: true,
20
30
  },
31
+ {
32
+ id: TYPE_REGISTRY_SYNC_EVENT,
33
+ label: 'Notification Type Registry Sync',
34
+ entity: 'notification_type',
35
+ category: 'system',
36
+ excludeFromTriggers: true,
37
+ },
38
+ {
39
+ id: PREFERENCE_UPDATED_EVENT,
40
+ label: 'Notification Preference Updated',
41
+ entity: 'notification_preference',
42
+ category: 'crud',
43
+ },
21
44
  ] as const
22
45
 
23
46
  export const eventsConfig = createModuleEvents({
@@ -0,0 +1,287 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { Switch } from '@open-mercato/ui/primitives/switch'
6
+ import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
7
+ import { FilterBar, type FilterDef, type FilterValues } from '@open-mercato/ui/backend/FilterBar'
8
+ import { FilteredEmptyResults } from '@open-mercato/ui/backend/filters/FilteredEmptyResults'
9
+
10
+ export type NotificationTypeItem = {
11
+ id: string
12
+ labelKey: string
13
+ descriptionKey?: string | null
14
+ // Stable grouping key from /api/notifications/types (the declared category, else the type-id
15
+ // prefix). Filter on this — never on `categoryLabel`, which changes with the locale.
16
+ category?: string | null
17
+ // Server-resolved heading for `category`. Equals `category` when the owning module ships no
18
+ // `notifications.categories.<key>` translation.
19
+ categoryLabel?: string | null
20
+ // When true the type cannot be opted out of; the matrix locks its cells ON (the server drops
21
+ // opt-out writes for these, so a toggleable switch would silently lie on reload).
22
+ nonOptOut?: boolean
23
+ // Effective channel eligibility from /api/notifications/types (operator override, else the
24
+ // code-declared set; null/absent = every channel). A channel outside the set renders locked
25
+ // OFF and toggles are refused server-side too, so the lock is enforcement, not presentation.
26
+ channels?: string[] | null
27
+ }
28
+ export type PreferenceItem = { notificationTypeId: string; channel: string; enabled: boolean }
29
+
30
+ export type ChannelDef = { key: string; labelKey: string; labelFallback: string; hintKey: string; hintFallback: string }
31
+
32
+ /**
33
+ * Resilient fallback channel list used only while the `/api/notifications/channels` catalogue is
34
+ * loading or when the fetch fails. The authoritative source is the module-registered channel
35
+ * catalogue (see `notification-channels.ts`); callers pass the fetched channels into the helpers
36
+ * and the component. Keep this list minimal — it is a safety net, not the source of truth.
37
+ */
38
+ export const PREFERENCE_CHANNELS: ChannelDef[] = [
39
+ {
40
+ key: 'in_app',
41
+ labelKey: 'notifications.preferences.channels.inApp',
42
+ labelFallback: 'In-app',
43
+ hintKey: 'notifications.preferences.channels.inAppHint',
44
+ hintFallback: 'Notification center and bell.',
45
+ },
46
+ {
47
+ key: 'email',
48
+ labelKey: 'notifications.preferences.channels.email',
49
+ labelFallback: 'Email',
50
+ hintKey: 'notifications.preferences.channels.emailHint',
51
+ hintFallback: 'Sent to your account email address.',
52
+ },
53
+ {
54
+ key: 'push',
55
+ labelKey: 'notifications.preferences.channels.push',
56
+ labelFallback: 'Push',
57
+ hintKey: 'notifications.preferences.channels.pushHint',
58
+ hintFallback: 'Mobile push (active once a push channel is connected).',
59
+ },
60
+ ]
61
+
62
+ /** Map a `/api/notifications/channels` item to the UI channel shape. */
63
+ export function toChannelDef(item: { id: string; labelKey: string; descriptionKey?: string | null }): ChannelDef {
64
+ return {
65
+ key: item.id,
66
+ labelKey: item.labelKey,
67
+ labelFallback: item.id,
68
+ hintKey: item.descriptionKey ?? '',
69
+ hintFallback: '',
70
+ }
71
+ }
72
+
73
+ export function preferenceKey(typeId: string, channel: string): string {
74
+ return `${typeId}::${channel}`
75
+ }
76
+
77
+ /** Whether the channel is outside the type's effective eligibility (cell locked off, tenant-wide). */
78
+ export function isChannelDisabledForType(type: NotificationTypeItem, channel: string): boolean {
79
+ return Array.isArray(type.channels) && !type.channels.includes(channel)
80
+ }
81
+
82
+ /** Build the default-on preference map for a catalogue + stored rows; ineligible cells are forced off. */
83
+ export function buildPreferenceMap(
84
+ types: NotificationTypeItem[],
85
+ stored: PreferenceItem[],
86
+ channels: ChannelDef[] = PREFERENCE_CHANNELS,
87
+ ): Record<string, boolean> {
88
+ const storedMap = new Map(stored.map((item) => [preferenceKey(item.notificationTypeId, item.channel), item.enabled]))
89
+ const next: Record<string, boolean> = {}
90
+ for (const type of types) {
91
+ for (const channel of channels) {
92
+ const key = preferenceKey(type.id, channel.key)
93
+ next[key] = isChannelDisabledForType(type, channel.key) ? false : storedMap.get(key) ?? true
94
+ }
95
+ }
96
+ return next
97
+ }
98
+
99
+ /**
100
+ * Return only the entries whose value differs from the originally-loaded state. The server upserts
101
+ * exactly what it receives (last-write-wins), so sending the diff keeps the payload bounded by the
102
+ * number of toggles — not the catalogue size — and avoids materializing redundant default-on rows.
103
+ */
104
+ export function diffPreferenceItems(
105
+ types: NotificationTypeItem[],
106
+ initial: Record<string, boolean>,
107
+ current: Record<string, boolean>,
108
+ channels: ChannelDef[] = PREFERENCE_CHANNELS,
109
+ ): PreferenceItem[] {
110
+ const items: PreferenceItem[] = []
111
+ for (const type of types) {
112
+ for (const channel of channels) {
113
+ if (isChannelDisabledForType(type, channel.key)) continue
114
+ const key = preferenceKey(type.id, channel.key)
115
+ const before = initial[key] ?? true
116
+ const after = current[key] ?? true
117
+ if (before !== after) {
118
+ items.push({ notificationTypeId: type.id, channel: channel.key, enabled: after })
119
+ }
120
+ }
121
+ }
122
+ return items
123
+ }
124
+
125
+ export type NotificationPreferenceMatrixProps = {
126
+ types: NotificationTypeItem[]
127
+ prefs: Record<string, boolean>
128
+ onToggle: (typeId: string, channel: string, enabled: boolean) => void
129
+ disabled?: boolean
130
+ channels?: ChannelDef[]
131
+ }
132
+
133
+ export function NotificationPreferenceMatrix({
134
+ types,
135
+ prefs,
136
+ onToggle,
137
+ disabled,
138
+ channels = PREFERENCE_CHANNELS,
139
+ }: NotificationPreferenceMatrixProps) {
140
+ const t = useT()
141
+ const [filterValues, setFilterValues] = React.useState<FilterValues>({})
142
+
143
+ // Options come from the catalogue itself, so a module introducing a new category shows up
144
+ // without touching this component. Sorted by the localized label, keyed by the stable slug.
145
+ const categoryOptions = React.useMemo(() => {
146
+ const byCategory = new Map<string, string>()
147
+ for (const type of types) {
148
+ if (!type.category) continue
149
+ if (!byCategory.has(type.category)) byCategory.set(type.category, type.categoryLabel ?? type.category)
150
+ }
151
+ return Array.from(byCategory, ([value, label]) => ({ value, label })).sort((left, right) =>
152
+ left.label.localeCompare(right.label),
153
+ )
154
+ }, [types])
155
+
156
+ const filters = React.useMemo<FilterDef[]>(
157
+ () =>
158
+ categoryOptions.length > 1
159
+ ? [
160
+ {
161
+ id: 'category',
162
+ label: t('notifications.preferences.filters.category', 'Category'),
163
+ // `tags` (searchable chips), not a `select` checkbox list: the catalogue grows a
164
+ // category per module, so the list gets long enough that scanning checkboxes stops
165
+ // scaling. Mirrors the customer/tag filters in SalesDocumentsTable.
166
+ type: 'tags',
167
+ options: categoryOptions,
168
+ placeholder: t('notifications.preferences.filters.categoryPlaceholder', 'Search categories'),
169
+ formatValue: (value: string) =>
170
+ categoryOptions.find((option) => option.value === value)?.label ?? value,
171
+ },
172
+ ]
173
+ : [],
174
+ [categoryOptions, t],
175
+ )
176
+
177
+ const selectedCategories = React.useMemo(() => {
178
+ const raw = filterValues.category
179
+ if (Array.isArray(raw)) return raw.filter((value): value is string => typeof value === 'string' && value !== '')
180
+ return typeof raw === 'string' && raw !== '' ? [raw] : []
181
+ }, [filterValues])
182
+
183
+ // Filtering happens at render only. The parent keeps the full `types` array, which
184
+ // `diffPreferenceItems` walks on save — narrowing it upstream would silently drop pending
185
+ // toggles for rows the filter hides.
186
+ const visibleTypes = React.useMemo(
187
+ () =>
188
+ selectedCategories.length === 0
189
+ ? types
190
+ : types.filter((type) => type.category != null && selectedCategories.includes(type.category)),
191
+ [types, selectedCategories],
192
+ )
193
+
194
+ if (types.length === 0) {
195
+ return (
196
+ <p className="text-sm text-muted-foreground">
197
+ {t('notifications.preferences.empty', 'No notification types are registered yet.')}
198
+ </p>
199
+ )
200
+ }
201
+
202
+ return (
203
+ <div className="flex flex-col gap-3">
204
+ {filters.length > 0 ? (
205
+ <FilterBar
206
+ filters={filters}
207
+ values={filterValues}
208
+ onApply={setFilterValues}
209
+ onClear={() => setFilterValues({})}
210
+ />
211
+ ) : null}
212
+ {visibleTypes.length === 0 ? (
213
+ <FilteredEmptyResults
214
+ entityNamePlural={t('notifications.preferences.entityNamePlural', 'notification types')}
215
+ canRemoveLast={selectedCategories.length > 0}
216
+ onClearAll={() => setFilterValues({})}
217
+ onRemoveLast={() => setFilterValues({})}
218
+ />
219
+ ) : (
220
+ <div className="overflow-x-auto rounded-lg border border-border">
221
+ <table className="w-full text-sm">
222
+ <thead>
223
+ <tr className="border-b border-border bg-muted/50 text-left">
224
+ <th className="px-4 py-3 font-medium">{t('notifications.preferences.columns.type', 'Notification type')}</th>
225
+ {channels.map((channel) => (
226
+ <th key={channel.key} className="px-4 py-3 font-medium">
227
+ <div>{t(channel.labelKey, channel.labelFallback)}</div>
228
+ <div className="text-xs font-normal text-muted-foreground">{t(channel.hintKey, channel.hintFallback)}</div>
229
+ </th>
230
+ ))}
231
+ </tr>
232
+ </thead>
233
+ <tbody>
234
+ {visibleTypes.map((type) => (
235
+ <tr key={type.id} className="border-b border-border last:border-0">
236
+ <td className="px-4 py-3">
237
+ <div className="font-medium">{t(type.labelKey, type.id)}</div>
238
+ {type.descriptionKey ? (
239
+ <div className="text-xs text-muted-foreground">{t(type.descriptionKey, '')}</div>
240
+ ) : null}
241
+ </td>
242
+ {channels.map((channel) => {
243
+ // Operator hard-off wins over the nonOptOut forced-on lock — mirrors the gate,
244
+ // where the tenant-wide channel block runs before the nonOptOut bypass.
245
+ const channelDisabled = isChannelDisabledForType(type, channel.key)
246
+ const locked = !channelDisabled && type.nonOptOut === true
247
+ const cellLabel = `${t(type.labelKey, type.id)} – ${t(channel.labelKey, channel.labelFallback)}`
248
+ const requiredHint = t(
249
+ 'notifications.preferences.requiredHint',
250
+ 'This notification is required and cannot be turned off.',
251
+ )
252
+ const channelDisabledHint = t(
253
+ 'notifications.preferences.channelDisabledHint',
254
+ 'This channel is turned off for this notification by your administrator.',
255
+ )
256
+ const lockHint = channelDisabled ? channelDisabledHint : requiredHint
257
+ const switchEl = (
258
+ <Switch
259
+ checked={channelDisabled ? false : locked ? true : prefs[preferenceKey(type.id, channel.key)] ?? true}
260
+ disabled={disabled || locked || channelDisabled}
261
+ onCheckedChange={(checked) => onToggle(type.id, channel.key, checked)}
262
+ aria-label={locked || channelDisabled ? `${cellLabel} (${lockHint})` : cellLabel}
263
+ />
264
+ )
265
+ return (
266
+ <td key={channel.key} className="px-4 py-3">
267
+ {locked || channelDisabled ? (
268
+ <SimpleTooltip content={lockHint}>
269
+ <span className="inline-flex">{switchEl}</span>
270
+ </SimpleTooltip>
271
+ ) : (
272
+ switchEl
273
+ )}
274
+ </td>
275
+ )
276
+ })}
277
+ </tr>
278
+ ))}
279
+ </tbody>
280
+ </table>
281
+ </div>
282
+ )}
283
+ </div>
284
+ )
285
+ }
286
+
287
+ export default NotificationPreferenceMatrix