@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.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/notification/index.js +6 -0
- package/dist/generated/entities/notification/index.js.map +2 -2
- package/dist/generated/entities/notification_preference/index.js +19 -0
- package/dist/generated/entities/notification_preference/index.js.map +7 -0
- package/dist/generated/entities/notification_type/index.js +21 -0
- package/dist/generated/entities/notification_type/index.js.map +7 -0
- package/dist/generated/entities/notification_type_override/index.js +17 -0
- package/dist/generated/entities/notification_type_override/index.js.map +7 -0
- package/dist/generated/entities/push_notification_delivery/index.js +41 -0
- package/dist/generated/entities/push_notification_delivery/index.js.map +7 -0
- package/dist/generated/entities/user_device/index.js +35 -0
- package/dist/generated/entities/user_device/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +12 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +72 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/api.js +2 -1
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/appRoot.js +9 -0
- package/dist/helpers/integration/appRoot.js.map +7 -0
- package/dist/helpers/integration/dbFixtures.js +1 -4
- package/dist/helpers/integration/dbFixtures.js.map +2 -2
- package/dist/helpers/integration/pushFake.js +126 -0
- package/dist/helpers/integration/pushFake.js.map +7 -0
- package/dist/modules/auth/notifications.js +16 -0
- package/dist/modules/auth/notifications.js.map +2 -2
- package/dist/modules/business_rules/notifications.js +2 -0
- package/dist/modules/business_rules/notifications.js.map +2 -2
- package/dist/modules/catalog/notifications.js +2 -0
- package/dist/modules/catalog/notifications.js.map +2 -2
- package/dist/modules/communication_channels/acl.js +9 -0
- package/dist/modules/communication_channels/acl.js.map +2 -2
- package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js +133 -0
- package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +6 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +2 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js +6 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/get/channels/route.js +2 -1
- package/dist/modules/communication_channels/api/get/channels/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +10 -5
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +29 -1
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +157 -0
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +8 -1
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/admin-delete-channel.js +141 -0
- package/dist/modules/communication_channels/commands/admin-delete-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js +25 -8
- package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/commands/push-register.js +1 -1
- package/dist/modules/communication_channels/commands/push-register.js.map +2 -2
- package/dist/modules/communication_channels/commands/push-unregister.js +4 -3
- package/dist/modules/communication_channels/commands/push-unregister.js.map +2 -2
- package/dist/modules/communication_channels/data/entities.js +4 -0
- package/dist/modules/communication_channels/data/entities.js.map +2 -2
- package/dist/modules/communication_channels/data/validators.js +2 -1
- package/dist/modules/communication_channels/data/validators.js.map +2 -2
- package/dist/modules/communication_channels/lib/access-control.js +11 -1
- package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
- package/dist/modules/communication_channels/lib/connect-channel.js +20 -12
- package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
- package/dist/modules/communication_channels/lib/push-adapter.js +68 -0
- package/dist/modules/communication_channels/lib/push-adapter.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-capabilities.js +31 -0
- package/dist/modules/communication_channels/lib/push-capabilities.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-connect-error.js +20 -0
- package/dist/modules/communication_channels/lib/push-connect-error.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-credential-errors.js +41 -0
- package/dist/modules/communication_channels/lib/push-credential-errors.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-envelope.js +46 -0
- package/dist/modules/communication_channels/lib/push-envelope.js.map +7 -0
- package/dist/modules/communication_channels/lib/refcounted-client-cache.js +64 -0
- package/dist/modules/communication_channels/lib/refcounted-client-cache.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js +13 -0
- package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/notifications.js +4 -0
- package/dist/modules/communication_channels/notifications.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +2 -0
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +21 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/notifications.js +12 -0
- package/dist/modules/customer_accounts/notifications.js.map +2 -2
- package/dist/modules/customers/notifications.js +4 -0
- package/dist/modules/customers/notifications.js.map +2 -2
- package/dist/modules/devices/acl.js +11 -0
- package/dist/modules/devices/acl.js.map +7 -0
- package/dist/modules/devices/api/[id]/route.js +167 -0
- package/dist/modules/devices/api/[id]/route.js.map +7 -0
- package/dist/modules/devices/api/admin/devices/[id]/route.js +219 -0
- package/dist/modules/devices/api/admin/devices/[id]/route.js.map +7 -0
- package/dist/modules/devices/api/admin/devices/route.js +140 -0
- package/dist/modules/devices/api/admin/devices/route.js.map +7 -0
- package/dist/modules/devices/api/auth.js +9 -0
- package/dist/modules/devices/api/auth.js.map +7 -0
- package/dist/modules/devices/api/deviceList.js +53 -0
- package/dist/modules/devices/api/deviceList.js.map +7 -0
- package/dist/modules/devices/api/deviceOps.js +118 -0
- package/dist/modules/devices/api/deviceOps.js.map +7 -0
- package/dist/modules/devices/api/openapi.js +19 -0
- package/dist/modules/devices/api/openapi.js.map +7 -0
- package/dist/modules/devices/api/route.js +143 -0
- package/dist/modules/devices/api/route.js.map +7 -0
- package/dist/modules/devices/backend/devices/[id]/page.js +135 -0
- package/dist/modules/devices/backend/devices/[id]/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/[id]/page.meta.js +16 -0
- package/dist/modules/devices/backend/devices/[id]/page.meta.js.map +7 -0
- package/dist/modules/devices/backend/devices/create/page.js +68 -0
- package/dist/modules/devices/backend/devices/create/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/create/page.meta.js +16 -0
- package/dist/modules/devices/backend/devices/create/page.meta.js.map +7 -0
- package/dist/modules/devices/backend/devices/page.js +235 -0
- package/dist/modules/devices/backend/devices/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/page.meta.js +23 -0
- package/dist/modules/devices/backend/devices/page.meta.js.map +7 -0
- package/dist/modules/devices/commands/deactivate.js +107 -0
- package/dist/modules/devices/commands/deactivate.js.map +7 -0
- package/dist/modules/devices/commands/index.js +4 -0
- package/dist/modules/devices/commands/index.js.map +7 -0
- package/dist/modules/devices/commands/register.js +183 -0
- package/dist/modules/devices/commands/register.js.map +7 -0
- package/dist/modules/devices/commands/shared.js +74 -0
- package/dist/modules/devices/commands/shared.js.map +7 -0
- package/dist/modules/devices/commands/update.js +126 -0
- package/dist/modules/devices/commands/update.js.map +7 -0
- package/dist/modules/devices/data/entities.js +83 -0
- package/dist/modules/devices/data/entities.js.map +7 -0
- package/dist/modules/devices/data/validators.js +49 -0
- package/dist/modules/devices/data/validators.js.map +7 -0
- package/dist/modules/devices/di.js +11 -0
- package/dist/modules/devices/di.js.map +7 -0
- package/dist/modules/devices/encryption.js +12 -0
- package/dist/modules/devices/encryption.js.map +7 -0
- package/dist/modules/devices/events.js +19 -0
- package/dist/modules/devices/events.js.map +7 -0
- package/dist/modules/devices/index.js +17 -0
- package/dist/modules/devices/index.js.map +7 -0
- package/dist/modules/devices/lib/operationMetadata.js +20 -0
- package/dist/modules/devices/lib/operationMetadata.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260602120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260602120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260630120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260630120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260701120000.js +13 -0
- package/dist/modules/devices/migrations/Migration20260701120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260721120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260721120000.js.map +7 -0
- package/dist/modules/devices/setup.js +13 -0
- package/dist/modules/devices/setup.js.map +7 -0
- package/dist/modules/entities/cli.js +8 -3
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/migrations/Migration20260722120000.js +28 -0
- package/dist/modules/entities/migrations/Migration20260722120000.js.map +7 -0
- package/dist/modules/inbox_ops/notifications.js +2 -0
- package/dist/modules/inbox_ops/notifications.js.map +2 -2
- package/dist/modules/messages/notifications.js +2 -0
- package/dist/modules/messages/notifications.js.map +2 -2
- package/dist/modules/notifications/acl.js +3 -1
- package/dist/modules/notifications/acl.js.map +2 -2
- package/dist/modules/notifications/api/admin/preferences/route.js +167 -0
- package/dist/modules/notifications/api/admin/preferences/route.js.map +7 -0
- package/dist/modules/notifications/api/channels/route.js +54 -0
- package/dist/modules/notifications/api/channels/route.js.map +7 -0
- package/dist/modules/notifications/api/preferences/route.js +126 -0
- package/dist/modules/notifications/api/preferences/route.js.map +7 -0
- package/dist/modules/notifications/api/route.js +8 -1
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js +187 -0
- package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js.map +7 -0
- package/dist/modules/notifications/api/types/route.js +225 -0
- package/dist/modules/notifications/api/types/route.js.map +7 -0
- package/dist/modules/notifications/api/unread-count/route.js +5 -1
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/dist/modules/notifications/backend/notifications/user-preferences/page.js +10 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.js.map +7 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js +28 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js.map +7 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.js +10 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.js.map +7 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +33 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +7 -0
- package/dist/modules/notifications/data/entities.js +128 -1
- package/dist/modules/notifications/data/entities.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +95 -2
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/di.js +7 -0
- package/dist/modules/notifications/di.js.map +2 -2
- package/dist/modules/notifications/events.js +17 -0
- package/dist/modules/notifications/events.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js +190 -0
- package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js +124 -0
- package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +166 -1
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js +172 -0
- package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js.map +7 -0
- package/dist/modules/notifications/generators.js +63 -0
- package/dist/modules/notifications/generators.js.map +7 -0
- package/dist/modules/notifications/lib/delivery-strategies-registry.js +19 -0
- package/dist/modules/notifications/lib/delivery-strategies-registry.js.map +7 -0
- package/dist/modules/notifications/lib/deliveryStrategies.js.map +2 -2
- package/dist/modules/notifications/lib/derive-category.js +8 -0
- package/dist/modules/notifications/lib/derive-category.js.map +7 -0
- package/dist/modules/notifications/lib/notification-channel-registry.js +35 -0
- package/dist/modules/notifications/lib/notification-channel-registry.js.map +7 -0
- package/dist/modules/notifications/lib/notification-type-registry.js +87 -0
- package/dist/modules/notifications/lib/notification-type-registry.js.map +7 -0
- package/dist/modules/notifications/lib/notificationCopy.js +14 -0
- package/dist/modules/notifications/lib/notificationCopy.js.map +7 -0
- package/dist/modules/notifications/lib/notificationFactory.js +4 -1
- package/dist/modules/notifications/lib/notificationFactory.js.map +2 -2
- package/dist/modules/notifications/lib/notificationMapper.js +2 -0
- package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
- package/dist/modules/notifications/lib/notificationPreferenceService.js +83 -0
- package/dist/modules/notifications/lib/notificationPreferenceService.js.map +7 -0
- package/dist/modules/notifications/lib/notificationService.js +83 -28
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/notificationVisibility.js +24 -0
- package/dist/modules/notifications/lib/notificationVisibility.js.map +7 -0
- package/dist/modules/notifications/lib/routeHelpers.js +11 -1
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/notifications/lib/shouldDeliver.js +46 -0
- package/dist/modules/notifications/lib/shouldDeliver.js.map +7 -0
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +54 -0
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +7 -0
- package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js +12 -0
- package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js.map +7 -0
- package/dist/modules/notifications/lib/typeCatalogue.js +71 -0
- package/dist/modules/notifications/lib/typeCatalogue.js.map +7 -0
- package/dist/modules/notifications/lib/typeChannelSettings.js +12 -0
- package/dist/modules/notifications/lib/typeChannelSettings.js.map +7 -0
- package/dist/modules/notifications/lib/typeOverrides.js +19 -0
- package/dist/modules/notifications/lib/typeOverrides.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js +14 -0
- package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js +15 -0
- package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js +13 -0
- package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js +15 -0
- package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js +13 -0
- package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js +14 -0
- package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js.map +7 -0
- package/dist/modules/notifications/notification-channels.js +26 -0
- package/dist/modules/notifications/notification-channels.js.map +7 -0
- package/dist/modules/notifications/notifications.delivery-strategies.js +12 -0
- package/dist/modules/notifications/notifications.delivery-strategies.js.map +7 -0
- package/dist/modules/notifications/setup.js +16 -1
- package/dist/modules/notifications/setup.js.map +2 -2
- package/dist/modules/notifications/subscribers/deliver-notification.js +66 -57
- package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
- package/dist/modules/notifications/subscribers/sync-notification-types.js +16 -0
- package/dist/modules/notifications/subscribers/sync-notification-types.js.map +7 -0
- package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js +23 -0
- package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js.map +7 -0
- package/dist/modules/notifications/widgets/injection-table.js +14 -0
- package/dist/modules/notifications/widgets/injection-table.js.map +7 -0
- package/dist/modules/push_notifications/acl.js +13 -0
- package/dist/modules/push_notifications/acl.js.map +7 -0
- package/dist/modules/push_notifications/api/custom-send/route.js +141 -0
- package/dist/modules/push_notifications/api/custom-send/route.js.map +7 -0
- package/dist/modules/push_notifications/api/deliveries/[id]/route.js +110 -0
- package/dist/modules/push_notifications/api/deliveries/[id]/route.js.map +7 -0
- package/dist/modules/push_notifications/api/deliveries/route.js +56 -0
- package/dist/modules/push_notifications/api/deliveries/route.js.map +7 -0
- package/dist/modules/push_notifications/api/openapi.js +19 -0
- package/dist/modules/push_notifications/api/openapi.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js +103 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js +13 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.js +213 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.meta.js +23 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js +245 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js +26 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/data/entities.js +92 -0
- package/dist/modules/push_notifications/data/entities.js.map +7 -0
- package/dist/modules/push_notifications/data/extensions.js +23 -0
- package/dist/modules/push_notifications/data/extensions.js.map +7 -0
- package/dist/modules/push_notifications/data/validators.js +89 -0
- package/dist/modules/push_notifications/data/validators.js.map +7 -0
- package/dist/modules/push_notifications/di.js +17 -0
- package/dist/modules/push_notifications/di.js.map +7 -0
- package/dist/modules/push_notifications/events.js +24 -0
- package/dist/modules/push_notifications/events.js.map +7 -0
- package/dist/modules/push_notifications/index.js +16 -0
- package/dist/modules/push_notifications/index.js.map +7 -0
- package/dist/modules/push_notifications/lib/fake-provider-recorder.js +80 -0
- package/dist/modules/push_notifications/lib/fake-provider-recorder.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-delivery-strategy.js +54 -0
- package/dist/modules/push_notifications/lib/push-delivery-strategy.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-delivery.js +297 -0
- package/dist/modules/push_notifications/lib/push-delivery.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-fanout.js +138 -0
- package/dist/modules/push_notifications/lib/push-fanout.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-health.js +32 -0
- package/dist/modules/push_notifications/lib/push-health.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-reaper.js +87 -0
- package/dist/modules/push_notifications/lib/push-reaper.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-receipt-reaper.js +136 -0
- package/dist/modules/push_notifications/lib/push-receipt-reaper.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-stub-adapter.js +63 -0
- package/dist/modules/push_notifications/lib/push-stub-adapter.js.map +7 -0
- package/dist/modules/push_notifications/lib/queue.js +53 -0
- package/dist/modules/push_notifications/lib/queue.js.map +7 -0
- package/dist/modules/push_notifications/lib/reclaim-window.js +13 -0
- package/dist/modules/push_notifications/lib/reclaim-window.js.map +7 -0
- package/dist/modules/push_notifications/lib/send-custom-push.js +48 -0
- package/dist/modules/push_notifications/lib/send-custom-push.js.map +7 -0
- package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js +16 -0
- package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js.map +7 -0
- package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js +13 -0
- package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js.map +7 -0
- package/dist/modules/push_notifications/notifications.delivery-strategies.js +8 -0
- package/dist/modules/push_notifications/notifications.delivery-strategies.js.map +7 -0
- package/dist/modules/push_notifications/notifications.js +40 -0
- package/dist/modules/push_notifications/notifications.js.map +7 -0
- package/dist/modules/push_notifications/setup.js +52 -0
- package/dist/modules/push_notifications/setup.js.map +7 -0
- package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js +57 -0
- package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js.map +7 -0
- package/dist/modules/push_notifications/workers/send-push.worker.js +27 -0
- package/dist/modules/push_notifications/workers/send-push.worker.js.map +7 -0
- package/dist/modules/sales/notifications.js +8 -0
- package/dist/modules/sales/notifications.js.map +2 -2
- package/dist/modules/staff/notifications.js +6 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/workflows/notifications.js +2 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/notification/index.ts +3 -0
- package/generated/entities/notification_preference/index.ts +8 -0
- package/generated/entities/notification_type/index.ts +9 -0
- package/generated/entities/notification_type_override/index.ts +7 -0
- package/generated/entities/push_notification_delivery/index.ts +19 -0
- package/generated/entities/user_device/index.ts +16 -0
- package/generated/entities.ids.generated.ts +12 -1
- package/generated/entity-fields-registry.ts +72 -0
- package/package.json +7 -7
- package/src/helpers/integration/api.ts +2 -1
- package/src/helpers/integration/appRoot.ts +13 -0
- package/src/helpers/integration/dbFixtures.ts +1 -5
- package/src/helpers/integration/pushFake.ts +219 -0
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/ko.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/auth/notifications.ts +16 -0
- package/src/modules/business_rules/i18n/de.json +2 -1
- package/src/modules/business_rules/i18n/en.json +2 -1
- package/src/modules/business_rules/i18n/es.json +2 -1
- package/src/modules/business_rules/i18n/ko.json +2 -1
- package/src/modules/business_rules/i18n/pl.json +2 -1
- package/src/modules/business_rules/notifications.ts +2 -0
- package/src/modules/catalog/i18n/de.json +2 -1
- package/src/modules/catalog/i18n/en.json +2 -1
- package/src/modules/catalog/i18n/es.json +2 -1
- package/src/modules/catalog/i18n/ko.json +2 -1
- package/src/modules/catalog/i18n/pl.json +2 -1
- package/src/modules/catalog/notifications.ts +2 -0
- package/src/modules/communication_channels/acl.ts +9 -0
- package/src/modules/communication_channels/api/delete/admin/channels/[id]/route.ts +168 -0
- package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +6 -2
- package/src/modules/communication_channels/api/get/channels/[id]/route.ts +6 -2
- package/src/modules/communication_channels/api/get/channels/route.ts +2 -1
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +16 -5
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +37 -1
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +175 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +11 -1
- package/src/modules/communication_channels/commands/admin-delete-channel.ts +203 -0
- package/src/modules/communication_channels/commands/connect-credential-channel.ts +63 -15
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +10 -2
- package/src/modules/communication_channels/commands/push-register.ts +7 -1
- package/src/modules/communication_channels/commands/push-unregister.ts +14 -3
- package/src/modules/communication_channels/data/entities.ts +10 -0
- package/src/modules/communication_channels/data/validators.ts +1 -0
- package/src/modules/communication_channels/i18n/de.json +35 -1
- package/src/modules/communication_channels/i18n/en.json +35 -1
- package/src/modules/communication_channels/i18n/es.json +35 -1
- package/src/modules/communication_channels/i18n/ko.json +35 -1
- package/src/modules/communication_channels/i18n/pl.json +35 -1
- package/src/modules/communication_channels/lib/access-control.ts +42 -0
- package/src/modules/communication_channels/lib/adapter.ts +25 -0
- package/src/modules/communication_channels/lib/connect-channel.ts +61 -26
- package/src/modules/communication_channels/lib/push-adapter.ts +139 -0
- package/src/modules/communication_channels/lib/push-capabilities.ts +42 -0
- package/src/modules/communication_channels/lib/push-connect-error.ts +52 -0
- package/src/modules/communication_channels/lib/push-credential-errors.ts +58 -0
- package/src/modules/communication_channels/lib/push-envelope.ts +104 -0
- package/src/modules/communication_channels/lib/refcounted-client-cache.ts +123 -0
- package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +9 -0
- package/src/modules/communication_channels/migrations/Migration20260703123630_communication_channels.ts +20 -0
- package/src/modules/communication_channels/notifications.ts +4 -0
- package/src/modules/communication_channels/setup.ts +2 -0
- package/src/modules/configs/i18n/de.json +5 -1
- package/src/modules/configs/i18n/en.json +5 -1
- package/src/modules/configs/i18n/es.json +5 -1
- package/src/modules/configs/i18n/ko.json +5 -1
- package/src/modules/configs/i18n/pl.json +5 -1
- package/src/modules/configs/lib/upgrade-actions.ts +21 -0
- package/src/modules/customer_accounts/i18n/de.json +2 -1
- package/src/modules/customer_accounts/i18n/en.json +2 -1
- package/src/modules/customer_accounts/i18n/es.json +2 -1
- package/src/modules/customer_accounts/i18n/ko.json +2 -1
- package/src/modules/customer_accounts/i18n/pl.json +2 -1
- package/src/modules/customer_accounts/notifications.ts +12 -0
- package/src/modules/customers/i18n/de.json +2 -1
- package/src/modules/customers/i18n/en.json +2 -1
- package/src/modules/customers/i18n/es.json +2 -1
- package/src/modules/customers/i18n/ko.json +2 -1
- package/src/modules/customers/i18n/pl.json +2 -1
- package/src/modules/customers/notifications.ts +4 -0
- package/src/modules/devices/AGENTS.md +101 -0
- package/src/modules/devices/acl.ts +7 -0
- package/src/modules/devices/api/[id]/route.ts +180 -0
- package/src/modules/devices/api/admin/devices/[id]/route.ts +235 -0
- package/src/modules/devices/api/admin/devices/route.ts +160 -0
- package/src/modules/devices/api/auth.ts +13 -0
- package/src/modules/devices/api/deviceList.ts +54 -0
- package/src/modules/devices/api/deviceOps.ts +186 -0
- package/src/modules/devices/api/openapi.ts +23 -0
- package/src/modules/devices/api/route.ts +165 -0
- package/src/modules/devices/backend/devices/[id]/page.meta.ts +12 -0
- package/src/modules/devices/backend/devices/[id]/page.tsx +164 -0
- package/src/modules/devices/backend/devices/create/page.meta.ts +12 -0
- package/src/modules/devices/backend/devices/create/page.tsx +81 -0
- package/src/modules/devices/backend/devices/page.meta.ts +21 -0
- package/src/modules/devices/backend/devices/page.tsx +259 -0
- package/src/modules/devices/commands/deactivate.ts +126 -0
- package/src/modules/devices/commands/index.ts +3 -0
- package/src/modules/devices/commands/register.ts +199 -0
- package/src/modules/devices/commands/shared.ts +119 -0
- package/src/modules/devices/commands/update.ts +142 -0
- package/src/modules/devices/data/entities.ts +79 -0
- package/src/modules/devices/data/validators.ts +65 -0
- package/src/modules/devices/di.ts +9 -0
- package/src/modules/devices/encryption.ts +15 -0
- package/src/modules/devices/events.ts +16 -0
- package/src/modules/devices/i18n/de.json +45 -0
- package/src/modules/devices/i18n/en.json +45 -0
- package/src/modules/devices/i18n/es.json +45 -0
- package/src/modules/devices/i18n/ko.json +45 -0
- package/src/modules/devices/i18n/pl.json +45 -0
- package/src/modules/devices/index.ts +15 -0
- package/src/modules/devices/lib/operationMetadata.ts +35 -0
- package/src/modules/devices/migrations/.snapshot-open-mercato.json +200 -0
- package/src/modules/devices/migrations/Migration20260602120000.ts +15 -0
- package/src/modules/devices/migrations/Migration20260630120000.ts +20 -0
- package/src/modules/devices/migrations/Migration20260701120000.ts +13 -0
- package/src/modules/devices/migrations/Migration20260721120000.ts +18 -0
- package/src/modules/devices/setup.ts +11 -0
- package/src/modules/entities/cli.ts +16 -2
- package/src/modules/entities/migrations/Migration20260722120000.ts +38 -0
- package/src/modules/inbox_ops/i18n/de.json +2 -1
- package/src/modules/inbox_ops/i18n/en.json +2 -1
- package/src/modules/inbox_ops/i18n/es.json +2 -1
- package/src/modules/inbox_ops/i18n/ko.json +2 -1
- package/src/modules/inbox_ops/i18n/pl.json +2 -1
- package/src/modules/inbox_ops/notifications.ts +2 -0
- package/src/modules/messages/i18n/de.json +2 -1
- package/src/modules/messages/i18n/en.json +2 -1
- package/src/modules/messages/i18n/es.json +2 -1
- package/src/modules/messages/i18n/ko.json +2 -1
- package/src/modules/messages/i18n/pl.json +2 -1
- package/src/modules/messages/notifications.ts +2 -0
- package/src/modules/notifications/AGENTS.md +54 -0
- package/src/modules/notifications/acl.ts +2 -0
- package/src/modules/notifications/api/admin/preferences/route.ts +193 -0
- package/src/modules/notifications/api/channels/route.ts +54 -0
- package/src/modules/notifications/api/preferences/route.ts +131 -0
- package/src/modules/notifications/api/route.ts +8 -1
- package/src/modules/notifications/api/types/[id]/channels/[channel]/route.ts +226 -0
- package/src/modules/notifications/api/types/route.ts +239 -0
- package/src/modules/notifications/api/unread-count/route.ts +5 -1
- package/src/modules/notifications/backend/notifications/user-preferences/page.meta.ts +26 -0
- package/src/modules/notifications/backend/notifications/user-preferences/page.tsx +12 -0
- package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +31 -0
- package/src/modules/notifications/backend/profile/notification-preferences/page.tsx +12 -0
- package/src/modules/notifications/data/entities.ts +144 -0
- package/src/modules/notifications/data/validators.ts +116 -0
- package/src/modules/notifications/di.ts +7 -0
- package/src/modules/notifications/events.ts +23 -0
- package/src/modules/notifications/frontend/NotificationPreferenceMatrix.tsx +287 -0
- package/src/modules/notifications/frontend/NotificationPreferencesPageClient.tsx +155 -0
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +273 -2
- package/src/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.tsx +214 -0
- package/src/modules/notifications/generators.ts +82 -0
- package/src/modules/notifications/i18n/de.json +43 -0
- package/src/modules/notifications/i18n/en.json +43 -0
- package/src/modules/notifications/i18n/es.json +43 -0
- package/src/modules/notifications/i18n/ko.json +43 -0
- package/src/modules/notifications/i18n/pl.json +44 -1
- package/src/modules/notifications/lib/delivery-strategies-registry.ts +30 -0
- package/src/modules/notifications/lib/deliveryStrategies.ts +40 -4
- package/src/modules/notifications/lib/derive-category.ts +15 -0
- package/src/modules/notifications/lib/notification-channel-registry.ts +59 -0
- package/src/modules/notifications/lib/notification-type-registry.ts +182 -0
- package/src/modules/notifications/lib/notificationCopy.ts +34 -0
- package/src/modules/notifications/lib/notificationFactory.ts +5 -1
- package/src/modules/notifications/lib/notificationMapper.ts +2 -0
- package/src/modules/notifications/lib/notificationPreferenceService.ts +138 -0
- package/src/modules/notifications/lib/notificationService.ts +129 -25
- package/src/modules/notifications/lib/notificationVisibility.ts +48 -0
- package/src/modules/notifications/lib/routeHelpers.ts +14 -1
- package/src/modules/notifications/lib/shouldDeliver.ts +112 -0
- package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +63 -0
- package/src/modules/notifications/lib/strategies/in-app-delivery-strategy.ts +21 -0
- package/src/modules/notifications/lib/typeCatalogue.ts +141 -0
- package/src/modules/notifications/lib/typeChannelSettings.ts +29 -0
- package/src/modules/notifications/lib/typeOverrides.ts +46 -0
- package/src/modules/notifications/migrations/.snapshot-open-mercato.json +874 -100
- package/src/modules/notifications/migrations/Migration20260625122947_notifications.ts +14 -0
- package/src/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.ts +15 -0
- package/src/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.ts +13 -0
- package/src/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.ts +15 -0
- package/src/modules/notifications/migrations/Migration20260706120000_notifications_channels.ts +13 -0
- package/src/modules/notifications/migrations/Migration20260716123121_notifications.ts +14 -0
- package/src/modules/notifications/notification-channels.ts +30 -0
- package/src/modules/notifications/notifications.delivery-strategies.ts +15 -0
- package/src/modules/notifications/setup.ts +23 -1
- package/src/modules/notifications/subscribers/deliver-notification.ts +105 -71
- package/src/modules/notifications/subscribers/sync-notification-types.ts +18 -0
- package/src/modules/notifications/widgets/injection/profile-preferences-menu/widget.ts +35 -0
- package/src/modules/notifications/widgets/injection-table.ts +18 -0
- package/src/modules/push_notifications/AGENTS.md +92 -0
- package/src/modules/push_notifications/acl.ts +9 -0
- package/src/modules/push_notifications/api/custom-send/route.ts +175 -0
- package/src/modules/push_notifications/api/deliveries/[id]/route.ts +122 -0
- package/src/modules/push_notifications/api/deliveries/route.ts +58 -0
- package/src/modules/push_notifications/api/openapi.ts +22 -0
- package/src/modules/push_notifications/backend/push_notifications/[id]/page.meta.ts +9 -0
- package/src/modules/push_notifications/backend/push_notifications/[id]/page.tsx +149 -0
- package/src/modules/push_notifications/backend/push_notifications/page.meta.ts +21 -0
- package/src/modules/push_notifications/backend/push_notifications/page.tsx +239 -0
- package/src/modules/push_notifications/backend/push_notifications/send/page.meta.ts +24 -0
- package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +327 -0
- package/src/modules/push_notifications/data/entities.ts +111 -0
- package/src/modules/push_notifications/data/extensions.ts +29 -0
- package/src/modules/push_notifications/data/validators.ts +102 -0
- package/src/modules/push_notifications/di.ts +20 -0
- package/src/modules/push_notifications/events.ts +21 -0
- package/src/modules/push_notifications/i18n/de.json +79 -0
- package/src/modules/push_notifications/i18n/en.json +79 -0
- package/src/modules/push_notifications/i18n/es.json +79 -0
- package/src/modules/push_notifications/i18n/ko.json +79 -0
- package/src/modules/push_notifications/i18n/pl.json +79 -0
- package/src/modules/push_notifications/index.ts +15 -0
- package/src/modules/push_notifications/lib/fake-provider-recorder.ts +154 -0
- package/src/modules/push_notifications/lib/push-delivery-strategy.ts +90 -0
- package/src/modules/push_notifications/lib/push-delivery.ts +455 -0
- package/src/modules/push_notifications/lib/push-fanout.ts +237 -0
- package/src/modules/push_notifications/lib/push-health.ts +47 -0
- package/src/modules/push_notifications/lib/push-reaper.ts +140 -0
- package/src/modules/push_notifications/lib/push-receipt-reaper.ts +210 -0
- package/src/modules/push_notifications/lib/push-stub-adapter.ts +80 -0
- package/src/modules/push_notifications/lib/queue.ts +74 -0
- package/src/modules/push_notifications/lib/reclaim-window.ts +16 -0
- package/src/modules/push_notifications/lib/send-custom-push.ts +100 -0
- package/src/modules/push_notifications/migrations/.snapshot-open-mercato.json +386 -0
- package/src/modules/push_notifications/migrations/Migration20260625150049_push_notifications.ts +16 -0
- package/src/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.ts +13 -0
- package/src/modules/push_notifications/notifications.delivery-strategies.ts +8 -0
- package/src/modules/push_notifications/notifications.ts +50 -0
- package/src/modules/push_notifications/setup.ts +92 -0
- package/src/modules/push_notifications/workers/reclaim-stuck.worker.ts +78 -0
- package/src/modules/push_notifications/workers/send-push.worker.ts +34 -0
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/ko.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/sales/notifications.ts +8 -0
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/ko.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/notifications.ts +6 -0
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/ko.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- 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
|