@open-mercato/core 0.6.8-develop.6984.1.f0cf23f0f6 → 0.6.8-develop.6985.1.fb93574faa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -23,6 +23,15 @@ import {
|
|
|
23
23
|
} from './notificationRecipients'
|
|
24
24
|
import { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'
|
|
25
25
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
26
|
+
import { getNotificationType } from './notification-type-registry'
|
|
27
|
+
import { getNotificationTypeOverrides, type NotificationTypeOverrides } from './typeOverrides'
|
|
28
|
+
import { getNotificationDeliveryStrategies } from './deliveryStrategies'
|
|
29
|
+
import { resolveEffectiveChannels } from './shouldDeliver'
|
|
30
|
+
import {
|
|
31
|
+
createNotificationPreferenceService,
|
|
32
|
+
type NotificationPreferenceService,
|
|
33
|
+
} from './notificationPreferenceService'
|
|
34
|
+
import { inAppVisibleFilter, inAppVisibleSql, isInAppVisible } from './notificationVisibility'
|
|
26
35
|
|
|
27
36
|
const logger = createLogger('notifications').child({ component: 'service' })
|
|
28
37
|
|
|
@@ -83,6 +92,7 @@ function applyNotificationContent(
|
|
|
83
92
|
input: NotificationContentInput,
|
|
84
93
|
recipientUserId: string,
|
|
85
94
|
ctx: NotificationTenantContext,
|
|
95
|
+
channels: string[] | null,
|
|
86
96
|
) {
|
|
87
97
|
const actions = sanitizeNotificationActions(input.actions)
|
|
88
98
|
const linkHref = assertSafeNotificationHref(input.linkHref)
|
|
@@ -108,6 +118,7 @@ function applyNotificationContent(
|
|
|
108
118
|
notification.sourceEntityId = input.sourceEntityId
|
|
109
119
|
notification.linkHref = linkHref
|
|
110
120
|
notification.groupKey = input.groupKey
|
|
121
|
+
notification.channels = channels
|
|
111
122
|
notification.expiresAt = input.expiresAt ? new Date(input.expiresAt) : null
|
|
112
123
|
notification.tenantId = ctx.tenantId
|
|
113
124
|
notification.organizationId = normalizeOrgScope(ctx.organizationId)
|
|
@@ -149,16 +160,21 @@ async function emitNotificationSseEvents(
|
|
|
149
160
|
eventBus: { emit: (event: string, payload: unknown) => Promise<void> },
|
|
150
161
|
notifications: Notification[],
|
|
151
162
|
ctx: NotificationServiceContext,
|
|
152
|
-
recipientUserIds: string[],
|
|
153
163
|
): Promise<void> {
|
|
164
|
+
// Live bell updates only for notifications actually delivered to the in-app channel; the others
|
|
165
|
+
// exist as records but must not bump anyone's badge/inbox.
|
|
166
|
+
const visible = notifications.filter((notification) => isInAppVisible(notification.channels))
|
|
167
|
+
if (visible.length === 0) return
|
|
168
|
+
|
|
169
|
+
const visibleRecipientUserIds = Array.from(new Set(visible.map((n) => n.recipientUserId)))
|
|
154
170
|
await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {
|
|
155
171
|
tenantId: ctx.tenantId,
|
|
156
172
|
organizationId: normalizeOrgScope(ctx.organizationId),
|
|
157
|
-
recipientUserIds,
|
|
158
|
-
count:
|
|
173
|
+
recipientUserIds: visibleRecipientUserIds,
|
|
174
|
+
count: visible.length,
|
|
159
175
|
})
|
|
160
176
|
|
|
161
|
-
for (const notification of
|
|
177
|
+
for (const notification of visible) {
|
|
162
178
|
await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {
|
|
163
179
|
tenantId: notification.tenantId,
|
|
164
180
|
organizationId: notification.organizationId ?? null,
|
|
@@ -173,6 +189,7 @@ async function createOrRefreshNotification(
|
|
|
173
189
|
input: NotificationContentInput,
|
|
174
190
|
recipientUserId: string,
|
|
175
191
|
ctx: NotificationTenantContext,
|
|
192
|
+
channels: string[] | null,
|
|
176
193
|
): Promise<Notification> {
|
|
177
194
|
if (input.groupKey && input.groupKey.trim().length > 0) {
|
|
178
195
|
const orgScope = normalizeOrgScope(ctx.organizationId) ?? 'global'
|
|
@@ -196,12 +213,12 @@ async function createOrRefreshNotification(
|
|
|
196
213
|
})
|
|
197
214
|
|
|
198
215
|
if (existing) {
|
|
199
|
-
applyNotificationContent(existing, input, recipientUserId, ctx)
|
|
216
|
+
applyNotificationContent(existing, input, recipientUserId, ctx, channels)
|
|
200
217
|
return existing
|
|
201
218
|
}
|
|
202
219
|
}
|
|
203
220
|
|
|
204
|
-
return buildNotificationEntity(em, input, recipientUserId, ctx)
|
|
221
|
+
return buildNotificationEntity(em, input, recipientUserId, ctx, channels)
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
export interface NotificationServiceContext {
|
|
@@ -258,25 +275,94 @@ export interface NotificationServiceDeps {
|
|
|
258
275
|
export function createNotificationService(deps: NotificationServiceDeps): NotificationService {
|
|
259
276
|
const { em: rootEm, eventBus, commandBus, container } = deps
|
|
260
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Resolves the authoritative delivery-channel set for one recipient at create time — the single
|
|
280
|
+
* gate that folds per-send target, per-type eligibility, registered strategies, and the
|
|
281
|
+
* recipient's per-channel preferences (`nonOptOut` bypasses opt-out). Stored on the row and
|
|
282
|
+
* replayed by the dispatcher; `in_app` membership also drives bell/inbox visibility.
|
|
283
|
+
*
|
|
284
|
+
* Returns `null` (⇒ "all channels", legacy behavior) when no delivery strategies are registered
|
|
285
|
+
* (e.g. a minimal bootstrap/test), so notifications never become silently undeliverable/invisible
|
|
286
|
+
* in an environment that simply hasn't wired the seam.
|
|
287
|
+
*/
|
|
288
|
+
const resolveChannelsFor = async (
|
|
289
|
+
content: NotificationContentInput,
|
|
290
|
+
recipientUserId: string,
|
|
291
|
+
scopeCtx: NotificationServiceContext,
|
|
292
|
+
preferences: NotificationPreferenceService = createNotificationPreferenceService({ em: rootEm.fork() }),
|
|
293
|
+
typeOverrides?: NotificationTypeOverrides | null,
|
|
294
|
+
): Promise<string[] | null> => {
|
|
295
|
+
const registeredChannels = getNotificationDeliveryStrategies().map((strategy) => strategy.id)
|
|
296
|
+
if (registeredChannels.length === 0) return null
|
|
297
|
+
// Treat an empty target as "no restriction" (all channels) rather than "no deliverable channel":
|
|
298
|
+
// a programmatic caller that computed an empty array should not silently black-hole the
|
|
299
|
+
// notification. The HTTP layer rejects an empty `channels` outright (see validators.ts).
|
|
300
|
+
const targetChannels = content.channels && content.channels.length > 0 ? content.channels : null
|
|
301
|
+
const overrides = typeOverrides === undefined
|
|
302
|
+
? (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null
|
|
303
|
+
: typeOverrides
|
|
304
|
+
return resolveEffectiveChannels({
|
|
305
|
+
typeId: content.type,
|
|
306
|
+
type: getNotificationType(content.type),
|
|
307
|
+
scope: { tenantId: scopeCtx.tenantId, userId: recipientUserId },
|
|
308
|
+
targetChannels,
|
|
309
|
+
registeredChannels,
|
|
310
|
+
preferences,
|
|
311
|
+
channelsOverride: overrides?.channels ?? null,
|
|
312
|
+
nonOptOutOverride: overrides?.nonOptOut ?? null,
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Broadcast counterpart of {@link resolveChannelsFor}: resolves the channel set for every recipient
|
|
318
|
+
* up front, BEFORE the write transaction opens, reusing a single forked EM / preference service
|
|
319
|
+
* across the whole set. This keeps the preference reads out of the write transaction (matching
|
|
320
|
+
* `create`, which resolves before its transaction) and avoids one EM fork per recipient on large
|
|
321
|
+
* role/feature broadcasts.
|
|
322
|
+
*/
|
|
323
|
+
const resolveChannelsForRecipients = async (
|
|
324
|
+
content: NotificationContentInput,
|
|
325
|
+
recipientUserIds: string[],
|
|
326
|
+
scopeCtx: NotificationServiceContext,
|
|
327
|
+
): Promise<Array<{ recipientUserId: string; channels: string[] | null }>> => {
|
|
328
|
+
const preferences = createNotificationPreferenceService({ em: rootEm.fork() })
|
|
329
|
+
// Stored overrides are per-type (not per-recipient) — read once for the whole broadcast.
|
|
330
|
+
const typeOverrides =
|
|
331
|
+
(await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null
|
|
332
|
+
const resolved: Array<{ recipientUserId: string; channels: string[] | null }> = []
|
|
333
|
+
for (const recipientUserId of recipientUserIds) {
|
|
334
|
+
resolved.push({
|
|
335
|
+
recipientUserId,
|
|
336
|
+
channels: await resolveChannelsFor(content, recipientUserId, scopeCtx, preferences, typeOverrides),
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
return resolved
|
|
340
|
+
}
|
|
341
|
+
|
|
261
342
|
return {
|
|
262
343
|
async create(input, ctx) {
|
|
263
344
|
const { recipientUserId, ...content } = input
|
|
345
|
+
const channels = await resolveChannelsFor(content, recipientUserId, ctx)
|
|
264
346
|
const writeEm = rootEm.fork()
|
|
265
347
|
const notification = await writeEm.transactional(async (tx) => {
|
|
266
348
|
await assertNotificationRecipientsInScope(tx, [recipientUserId], ctx)
|
|
267
|
-
const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
|
|
349
|
+
const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
|
|
268
350
|
await tx.flush()
|
|
269
351
|
return entity
|
|
270
352
|
})
|
|
271
353
|
|
|
272
354
|
await invalidateNotificationCache(container, ctx, 'created')
|
|
355
|
+
// Always emit the domain event (drives the deliver subscriber → push/email/…). Only bump the
|
|
356
|
+
// live in-app bell when this notification is actually visible in-app.
|
|
273
357
|
await emitNotificationCreated(eventBus, notification, ctx)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
358
|
+
if (isInAppVisible(notification.channels)) {
|
|
359
|
+
await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {
|
|
360
|
+
tenantId: notification.tenantId,
|
|
361
|
+
organizationId: notification.organizationId ?? null,
|
|
362
|
+
recipientUserId: notification.recipientUserId,
|
|
363
|
+
notification: toNotificationDto(notification),
|
|
364
|
+
})
|
|
365
|
+
}
|
|
280
366
|
|
|
281
367
|
return notification
|
|
282
368
|
},
|
|
@@ -285,12 +371,13 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
285
371
|
const recipientUserIds = Array.from(new Set(input.recipientUserIds))
|
|
286
372
|
const { recipientUserIds: _recipientUserIds, ...content } = input
|
|
287
373
|
const notifications: Notification[] = []
|
|
374
|
+
const resolved = await resolveChannelsForRecipients(content, recipientUserIds, ctx)
|
|
288
375
|
const writeEm = rootEm.fork()
|
|
289
376
|
|
|
290
377
|
await writeEm.transactional(async (tx) => {
|
|
291
378
|
await assertNotificationRecipientsInScope(tx, recipientUserIds, ctx)
|
|
292
|
-
for (const recipientUserId of
|
|
293
|
-
const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
|
|
379
|
+
for (const { recipientUserId, channels } of resolved) {
|
|
380
|
+
const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
|
|
294
381
|
notifications.push(notification)
|
|
295
382
|
}
|
|
296
383
|
await tx.flush()
|
|
@@ -298,7 +385,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
298
385
|
|
|
299
386
|
await invalidateNotificationCache(container, ctx, 'created')
|
|
300
387
|
await emitNotificationCreatedBatch(eventBus, notifications, ctx)
|
|
301
|
-
await emitNotificationSseEvents(eventBus, notifications, ctx
|
|
388
|
+
await emitNotificationSseEvents(eventBus, notifications, ctx)
|
|
302
389
|
|
|
303
390
|
return notifications
|
|
304
391
|
},
|
|
@@ -315,11 +402,12 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
315
402
|
const { roleId: _roleId, ...content } = input
|
|
316
403
|
const notifications: Notification[] = []
|
|
317
404
|
const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))
|
|
405
|
+
const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)
|
|
318
406
|
const writeEm = rootEm.fork()
|
|
319
407
|
|
|
320
408
|
await writeEm.transactional(async (tx) => {
|
|
321
|
-
for (const recipientUserId of
|
|
322
|
-
const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
|
|
409
|
+
for (const { recipientUserId, channels } of resolved) {
|
|
410
|
+
const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
|
|
323
411
|
notifications.push(notification)
|
|
324
412
|
}
|
|
325
413
|
await tx.flush()
|
|
@@ -327,7 +415,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
327
415
|
|
|
328
416
|
await invalidateNotificationCache(container, ctx, 'created')
|
|
329
417
|
await emitNotificationCreatedBatch(eventBus, notifications, ctx)
|
|
330
|
-
await emitNotificationSseEvents(eventBus, notifications, ctx
|
|
418
|
+
await emitNotificationSseEvents(eventBus, notifications, ctx)
|
|
331
419
|
|
|
332
420
|
return notifications
|
|
333
421
|
},
|
|
@@ -422,11 +510,12 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
422
510
|
} = input
|
|
423
511
|
const notifications: Notification[] = []
|
|
424
512
|
const uniqueRecipientUserIds = Array.from(new Set(authorizedRecipientUserIds))
|
|
513
|
+
const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)
|
|
425
514
|
const writeEm = rootEm.fork()
|
|
426
515
|
|
|
427
516
|
await writeEm.transactional(async (tx) => {
|
|
428
|
-
for (const recipientUserId of
|
|
429
|
-
const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx)
|
|
517
|
+
for (const { recipientUserId, channels } of resolved) {
|
|
518
|
+
const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)
|
|
430
519
|
notifications.push(notification)
|
|
431
520
|
}
|
|
432
521
|
await tx.flush()
|
|
@@ -434,7 +523,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
434
523
|
|
|
435
524
|
await invalidateNotificationCache(container, ctx, 'created')
|
|
436
525
|
await emitNotificationCreatedBatch(eventBus, notifications, ctx)
|
|
437
|
-
await emitNotificationSseEvents(eventBus, notifications, ctx
|
|
526
|
+
await emitNotificationSseEvents(eventBus, notifications, ctx)
|
|
438
527
|
|
|
439
528
|
return notifications
|
|
440
529
|
},
|
|
@@ -467,6 +556,12 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
467
556
|
.where('recipient_user_id' as any, '=', ctx.userId as any)
|
|
468
557
|
.where('tenant_id' as any, '=', ctx.tenantId)
|
|
469
558
|
.where('status' as any, '=', 'unread')
|
|
559
|
+
// Only in-app-visible rows count toward the badge (see getUnreadCount),
|
|
560
|
+
// so "mark all as read" must scope to the SAME set — otherwise it flips
|
|
561
|
+
// push/email-only rows the user never saw and SSE-broadcasts a read for
|
|
562
|
+
// notifications that were never in the bell, inflating the returned
|
|
563
|
+
// count past what the badge showed.
|
|
564
|
+
.where(inAppVisibleSql() as any)
|
|
470
565
|
if (ctx.organizationId) {
|
|
471
566
|
chain = chain.where('organization_id' as any, '=', ctx.organizationId)
|
|
472
567
|
}
|
|
@@ -707,7 +802,10 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
707
802
|
recipientUserId: ctx.userId,
|
|
708
803
|
tenantId: ctx.tenantId,
|
|
709
804
|
status: 'unread',
|
|
710
|
-
|
|
805
|
+
$and: [
|
|
806
|
+
buildNotificationReadScopeWhere(ctx),
|
|
807
|
+
inAppVisibleFilter(),
|
|
808
|
+
],
|
|
711
809
|
})
|
|
712
810
|
},
|
|
713
811
|
|
|
@@ -716,7 +814,10 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
716
814
|
const filters: Record<string, unknown> = {
|
|
717
815
|
recipientUserId: ctx.userId,
|
|
718
816
|
tenantId: ctx.tenantId,
|
|
719
|
-
|
|
817
|
+
$and: [
|
|
818
|
+
buildNotificationReadScopeWhere(ctx),
|
|
819
|
+
inAppVisibleFilter(),
|
|
820
|
+
],
|
|
720
821
|
}
|
|
721
822
|
|
|
722
823
|
if (since) {
|
|
@@ -732,7 +833,10 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
732
833
|
recipientUserId: ctx.userId,
|
|
733
834
|
tenantId: ctx.tenantId,
|
|
734
835
|
status: 'unread',
|
|
735
|
-
|
|
836
|
+
$and: [
|
|
837
|
+
buildNotificationReadScopeWhere(ctx),
|
|
838
|
+
inAppVisibleFilter(),
|
|
839
|
+
],
|
|
736
840
|
}),
|
|
737
841
|
])
|
|
738
842
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { sql, type RawBuilder } from 'kysely'
|
|
2
|
+
import { IN_APP_CHANNEL } from './strategies/in-app-delivery-strategy'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MikroORM filter fragment selecting notifications that should be VISIBLE in the in-app surfaces
|
|
6
|
+
* (bell, inbox list, unread count, poll). A row is visible when the `in_app` channel survived the
|
|
7
|
+
* create-time delivery gate — i.e. `in_app ∈ channels` — or when `channels` is NULL (legacy rows
|
|
8
|
+
* and any environment without the seam wired, which behave as "all channels" for backward
|
|
9
|
+
* compatibility). Rows targeted away from in-app (e.g. a push-only send, or a type/preference that
|
|
10
|
+
* excluded in_app) still exist as durable records but are hidden here.
|
|
11
|
+
*
|
|
12
|
+
* AND-composed with the caller's other conditions when merged into a filter object.
|
|
13
|
+
*
|
|
14
|
+
* `channels` is a JSONB column (`data/entities.ts`); MikroORM's `$contains` maps to the Postgres
|
|
15
|
+
* `@>` containment operator with a JSON-encoded operand — i.e. `channels @> '["in_app"]'` — so a row
|
|
16
|
+
* with `["in_app","email"]` matches and one with `["push"]` does not. Same operator MikroORM emits for
|
|
17
|
+
* the JSONB `$contains` filters already in production on `staff.role_ids` and `workflows.metadata`.
|
|
18
|
+
*/
|
|
19
|
+
export function inAppVisibleFilter(): { $or: Array<Record<string, unknown>> } {
|
|
20
|
+
return {
|
|
21
|
+
$or: [
|
|
22
|
+
{ channels: null },
|
|
23
|
+
{ channels: { $contains: [IN_APP_CHANNEL] } },
|
|
24
|
+
],
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* In-memory counterpart of {@link inAppVisibleFilter} for a single row's resolved channel set. Used
|
|
30
|
+
* to gate the live bell SSE emit so a notification suppressed from the in-app channel does not push a
|
|
31
|
+
* real-time badge/inbox update. `null`/`undefined` channels ⇒ visible (legacy / all-channels).
|
|
32
|
+
*/
|
|
33
|
+
export function isInAppVisible(channels: string[] | null | undefined): boolean {
|
|
34
|
+
return channels == null || channels.includes(IN_APP_CHANNEL)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Raw-SQL counterpart of {@link inAppVisibleFilter} for queries built with Kysely
|
|
39
|
+
* (which cannot consume MikroORM's `$contains` fragment). Emits
|
|
40
|
+
* `(<column> IS NULL OR <column> @> '["in_app"]'::jsonb)` — the exact predicate
|
|
41
|
+
* MikroORM produces — so raw and ORM paths stay in lock-step. `columnRef` MUST be
|
|
42
|
+
* a trusted, code-controlled column identifier (never user input).
|
|
43
|
+
*/
|
|
44
|
+
export function inAppVisibleSql(columnRef = 'channels'): RawBuilder<boolean> {
|
|
45
|
+
return sql<boolean>`(${sql.ref(columnRef)} is null or ${sql.ref(columnRef)} @> ${sql.val(
|
|
46
|
+
JSON.stringify([IN_APP_CHANNEL]),
|
|
47
|
+
)}::jsonb)`
|
|
48
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import type { AwilixContainer } from 'awilix'
|
|
3
3
|
import { resolveRequestContext } from '@open-mercato/shared/lib/api/context'
|
|
4
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
4
5
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
6
|
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
6
7
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
7
|
-
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
8
8
|
import { resolveNotificationService, type NotificationService } from './notificationService'
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -17,6 +17,11 @@ export const NOTIFICATION_RESOURCE_KIND = 'notifications.notification'
|
|
|
17
17
|
*/
|
|
18
18
|
export const NOTIFICATION_SETTINGS_RESOURCE_KIND = 'notifications.settings'
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Mutation-guard resource kind for a user's channel preferences.
|
|
22
|
+
*/
|
|
23
|
+
export const NOTIFICATION_PREFERENCE_RESOURCE_KIND = 'notifications.preference'
|
|
24
|
+
|
|
20
25
|
/**
|
|
21
26
|
* Notification scope context for service calls
|
|
22
27
|
*/
|
|
@@ -120,6 +125,14 @@ export async function resolveNotificationContext(req: Request): Promise<Notifica
|
|
|
120
125
|
service: resolveNotificationService(ctx.container),
|
|
121
126
|
scope: {
|
|
122
127
|
tenantId,
|
|
128
|
+
// The *creator's* org (the shared resolver already falls back to the caller's own org when
|
|
129
|
+
// nothing is selected), and the push strategy later scopes the recipient's device lookup to
|
|
130
|
+
// it. Same-org and self-notify (the common paths) match; a cross-org notify — an admin in
|
|
131
|
+
// org A notifying a user whose devices live in org B — won't match the recipient's devices,
|
|
132
|
+
// so push is silently dropped for that case. Same for a deliberate all-organizations
|
|
133
|
+
// selection, which stays tenant-level (org=null) and so only reaches null-org devices.
|
|
134
|
+
// In-app delivery is unaffected. If cross-org push is ever required, scope the device
|
|
135
|
+
// lookup by the recipient's org instead.
|
|
123
136
|
organizationId,
|
|
124
137
|
organizationIds,
|
|
125
138
|
userId: ctx.auth?.sub ?? null,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
2
|
+
import type { NotificationTypeDefinition } from '@open-mercato/shared/modules/notifications/types'
|
|
3
|
+
import type { NotificationPreferenceScope } from './notificationPreferenceService'
|
|
4
|
+
|
|
5
|
+
const logger = createLogger('notifications').child({ component: 'delivery-gate' })
|
|
6
|
+
|
|
7
|
+
// One warning per unregistered type id per process — unregistered ids are rare (a renamed/removed type
|
|
8
|
+
// still referenced by an in-flight notification), so the set stays tiny and the log never floods.
|
|
9
|
+
const warnedUnregisteredTypes = new Set<string>()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The single per-channel delivery gate. Every channel (`in_app`, `email`, `push`, custom) is
|
|
13
|
+
* governed by this one function so opt-out / eligibility / targeting enforcement can never be
|
|
14
|
+
* bypassed or re-implemented inconsistently per strategy.
|
|
15
|
+
*
|
|
16
|
+
* A channel is delivered when ALL hold:
|
|
17
|
+
* 1. it is a registered strategy id (`registeredChannels`),
|
|
18
|
+
* 2. the type is eligible for it — the operator's stored override
|
|
19
|
+
* (`notification_types.channels`, when set) or the code-declared `type.channels`;
|
|
20
|
+
* this runs BEFORE the `nonOptOut` bypass and before user preferences, so a channel
|
|
21
|
+
* outside the effective set is completely off for the type,
|
|
22
|
+
* 3. it is in the per-send target (`targetChannels`, when provided),
|
|
23
|
+
* 4. either the type is effectively `nonOptOut` (operator override ?? code flag), or the
|
|
24
|
+
* recipient has not disabled it (`isChannelEnabled`).
|
|
25
|
+
*
|
|
26
|
+
* `silent` is intentionally NOT consulted here — it selects push delivery STYLE, not whether a
|
|
27
|
+
* channel delivers at all. Absent `type.channels` and absent `targetChannels` both mean "no
|
|
28
|
+
* restriction", so a fully-default call resolves to every registered channel (pre-Phase-7 behavior).
|
|
29
|
+
*/
|
|
30
|
+
export type ChannelPreferenceReader = {
|
|
31
|
+
isChannelEnabled(scope: NotificationPreferenceScope, typeId: string, channel: string): Promise<boolean>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The effective eligibility set for a type: the operator's stored override replaces the
|
|
36
|
+
* code-declared `type.channels`; `null`/absent on both ⇒ no restriction (every channel).
|
|
37
|
+
*/
|
|
38
|
+
export function resolveEligibleChannels(
|
|
39
|
+
type: NotificationTypeDefinition | undefined,
|
|
40
|
+
channelsOverride: string[] | null | undefined,
|
|
41
|
+
): string[] | null {
|
|
42
|
+
return channelsOverride ?? type?.channels ?? null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ShouldDeliverParams = {
|
|
46
|
+
/** The notification's `type` string (used for the preference lookup even when unregistered). */
|
|
47
|
+
typeId: string
|
|
48
|
+
/** The registered type definition, when the type id is known. Supplies eligibility + nonOptOut. */
|
|
49
|
+
type: NotificationTypeDefinition | undefined
|
|
50
|
+
channel: string
|
|
51
|
+
scope: NotificationPreferenceScope
|
|
52
|
+
/** Per-send channel target. `null`/`undefined` → no target restriction. */
|
|
53
|
+
targetChannels?: string[] | null
|
|
54
|
+
/** Ids of the currently registered delivery strategies. */
|
|
55
|
+
registeredChannels: string[]
|
|
56
|
+
preferences: ChannelPreferenceReader
|
|
57
|
+
/**
|
|
58
|
+
* Operator override of the type's eligibility from `notification_types.channels`.
|
|
59
|
+
* `undefined`/`null` ⇒ no override; the code-declared `type.channels` applies.
|
|
60
|
+
*/
|
|
61
|
+
channelsOverride?: string[] | null
|
|
62
|
+
/**
|
|
63
|
+
* Operator override of the type's `nonOptOut` flag from `notification_types.non_opt_out`.
|
|
64
|
+
* `undefined`/`null` ⇒ no override; the code-declared flag applies.
|
|
65
|
+
*/
|
|
66
|
+
nonOptOutOverride?: boolean | null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function shouldDeliver(params: ShouldDeliverParams): Promise<boolean> {
|
|
70
|
+
const {
|
|
71
|
+
typeId, type, channel, scope, targetChannels, registeredChannels, preferences,
|
|
72
|
+
channelsOverride, nonOptOutOverride,
|
|
73
|
+
} = params
|
|
74
|
+
|
|
75
|
+
if (!registeredChannels.includes(channel)) return false
|
|
76
|
+
const eligible = resolveEligibleChannels(type, channelsOverride)
|
|
77
|
+
if (eligible && !eligible.includes(channel)) return false
|
|
78
|
+
if (targetChannels && !targetChannels.includes(channel)) return false
|
|
79
|
+
if ((nonOptOutOverride ?? type?.nonOptOut) === true) return true
|
|
80
|
+
|
|
81
|
+
// The type id resolved to no registered definition (renamed/removed type), so its code-declared
|
|
82
|
+
// `nonOptOut` and eligibility cannot be enforced here — a security type that was `nonOptOut` becomes
|
|
83
|
+
// silently user-suppressible. Surface that once so it is not invisible (there is no operator override
|
|
84
|
+
// in play either, else the branch above would have returned).
|
|
85
|
+
if (!type && nonOptOutOverride == null && !warnedUnregisteredTypes.has(typeId)) {
|
|
86
|
+
warnedUnregisteredTypes.add(typeId)
|
|
87
|
+
logger.warn(
|
|
88
|
+
'Delivery gate evaluated an unregistered notification type; its code-declared nonOptOut/eligibility cannot be enforced and it is subject to user opt-out',
|
|
89
|
+
{ typeId },
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return preferences.isChannelEnabled(scope, typeId, channel)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type ResolveEffectiveChannelsParams = Omit<ShouldDeliverParams, 'channel'>
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolves the authoritative channel set for a notification at create time: every registered
|
|
100
|
+
* channel that passes {@link shouldDeliver}. Stored on `Notification.channels` and looped by the
|
|
101
|
+
* dispatcher; `in_app` membership also gates bell/inbox visibility.
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveEffectiveChannels(
|
|
104
|
+
params: ResolveEffectiveChannelsParams,
|
|
105
|
+
): Promise<string[]> {
|
|
106
|
+
const results = await Promise.all(
|
|
107
|
+
params.registeredChannels.map(async (channel) =>
|
|
108
|
+
(await shouldDeliver({ ...params, channel })) ? channel : null,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
return results.filter((channel): channel is string => channel !== null)
|
|
112
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { sendEmail } from '@open-mercato/shared/lib/email/send'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
import NotificationEmail from '../../emails/NotificationEmail'
|
|
4
|
+
import type { NotificationDeliveryContext, NotificationDeliveryStrategy } from '../deliveryStrategies'
|
|
5
|
+
|
|
6
|
+
export const EMAIL_CHANNEL = 'email'
|
|
7
|
+
|
|
8
|
+
const logger = createLogger('notifications')
|
|
9
|
+
|
|
10
|
+
const DEBUG = process.env.NOTIFICATIONS_DEBUG === 'true'
|
|
11
|
+
|
|
12
|
+
function debug(msg: string, fields?: Record<string, unknown>): void {
|
|
13
|
+
if (DEBUG) logger.debug(msg, fields)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Email delivery as a first-class strategy on the seam (previously hard-coded inline in the dispatch
|
|
18
|
+
* subscriber). Per-user opt-out and `nonOptOut`/`silent` are enforced upstream at create time via
|
|
19
|
+
* `shouldDeliver`; this strategy only owns technical deliverability: it runs when email is enabled by
|
|
20
|
+
* tenant config (`isConfigured`) and there is both a recipient address and a panel deep link.
|
|
21
|
+
*/
|
|
22
|
+
export const emailDeliveryStrategy: NotificationDeliveryStrategy = {
|
|
23
|
+
id: EMAIL_CHANNEL,
|
|
24
|
+
defaultEnabled: true,
|
|
25
|
+
isConfigured: (ctx: NotificationDeliveryContext) => ctx.deliveryConfig.strategies.email.enabled === true,
|
|
26
|
+
async deliver(ctx: NotificationDeliveryContext) {
|
|
27
|
+
const { recipient, panelLink, title, body, actionLinks, deliveryConfig, t } = ctx
|
|
28
|
+
if (!recipient?.email || !panelLink) {
|
|
29
|
+
debug('email skipped: missing recipient email or panelLink')
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const subjectPrefix = deliveryConfig.strategies.email.subjectPrefix?.trim()
|
|
34
|
+
const subject = subjectPrefix ? `${subjectPrefix} ${title}` : title
|
|
35
|
+
const copy = {
|
|
36
|
+
preview: t('notifications.delivery.email.preview', 'New notification'),
|
|
37
|
+
heading: t('notifications.delivery.email.heading', 'You have a new notification'),
|
|
38
|
+
bodyIntro: t('notifications.delivery.email.bodyIntro', 'Review the notification details and take any required actions.'),
|
|
39
|
+
actionNotice: t('notifications.delivery.email.actionNotice', 'Actions are available in Open Mercato and are read-only in this email.'),
|
|
40
|
+
openCta: t('notifications.delivery.email.openCta', 'Open notification center'),
|
|
41
|
+
footer: t('notifications.delivery.email.footer', 'Open Mercato notifications'),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
debug('sending email', { to: recipient.email, from: deliveryConfig.strategies.email.from, subject })
|
|
46
|
+
await sendEmail({
|
|
47
|
+
to: recipient.email,
|
|
48
|
+
subject,
|
|
49
|
+
from: deliveryConfig.strategies.email.from,
|
|
50
|
+
replyTo: deliveryConfig.strategies.email.replyTo,
|
|
51
|
+
react: NotificationEmail({
|
|
52
|
+
title,
|
|
53
|
+
body,
|
|
54
|
+
actions: actionLinks,
|
|
55
|
+
panelUrl: panelLink,
|
|
56
|
+
copy,
|
|
57
|
+
}),
|
|
58
|
+
})
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.error('email delivery failed', { error })
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NotificationDeliveryStrategy } from '../deliveryStrategies'
|
|
2
|
+
|
|
3
|
+
export const IN_APP_CHANNEL = 'in_app'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-app is a first-class channel of the delivery seam, but its "delivery" is the `Notification` row
|
|
7
|
+
* that `notificationService.create()` writes synchronously (the durable record that push/email
|
|
8
|
+
* reference and that the bell renders). There is nothing to send here.
|
|
9
|
+
*
|
|
10
|
+
* Whether the row is *visible* in the bell/inbox is governed by `in_app ∈ notification.channels`
|
|
11
|
+
* (resolved at create time by `shouldDeliver`) at the read layer — not by this strategy. Registering
|
|
12
|
+
* it makes in_app appear in the strategy registry so the create-time gate and the dispatcher treat it
|
|
13
|
+
* exactly like every other channel, with no channel-specific branch in the dispatcher.
|
|
14
|
+
*/
|
|
15
|
+
export const inAppDeliveryStrategy: NotificationDeliveryStrategy = {
|
|
16
|
+
id: IN_APP_CHANNEL,
|
|
17
|
+
defaultEnabled: true,
|
|
18
|
+
deliver: () => {
|
|
19
|
+
/* no-op: the row IS the in-app delivery; visibility is a read-layer concern */
|
|
20
|
+
},
|
|
21
|
+
}
|