diff --git a/internal/api/handler/notifications.go b/internal/api/handler/notifications.go
index b07c5507..af7d88b5 100644
--- a/internal/api/handler/notifications.go
+++ b/internal/api/handler/notifications.go
@@ -181,7 +181,7 @@ func (h *NotificationsHandler) ListSystemNotifications(ctx echo.Context) error {
seenDestinations := make(map[string]struct{})
for i := range rows {
- name, ok := notification.NormalizeNotificationType(rows[i].NotificationType)
+ name, ok := notification.NormalizeSystemNotificationName(rows[i].NotificationType)
if !ok {
err := fmt.Errorf("unsupported notification type %q", rows[i].NotificationType)
h.sugar.Errorw("Invalid configured system notification type", "error", err, "notificationType", rows[i].NotificationType)
@@ -251,7 +251,7 @@ func (h *NotificationsHandler) ListSystemNotifications(ctx echo.Context) error {
// @Router /admin/notifications/{notificationName}/destinations [post]
func (h *NotificationsHandler) CreateSystemNotificationDestination(ctx echo.Context) error {
notificationName := ctx.Param("notificationName")
- canonicalType, ok := notification.NormalizeNotificationType(notificationName)
+ canonicalType, ok := notification.NormalizeSystemNotificationName(notificationName)
if !ok {
err := fmt.Errorf("unsupported notification type %q", notificationName)
h.sugar.Warnw("Invalid system notification type", "error", err, "notificationName", notificationName)
@@ -351,7 +351,7 @@ func (h *NotificationsHandler) CreateSystemNotificationDestination(ctx echo.Cont
// @Router /admin/notifications/{notificationName}/destinations [delete]
func (h *NotificationsHandler) DeleteSystemNotificationDestination(ctx echo.Context) error {
notificationName := ctx.Param("notificationName")
- canonicalType, ok := notification.NormalizeNotificationType(notificationName)
+ canonicalType, ok := notification.NormalizeSystemNotificationName(notificationName)
if !ok {
err := fmt.Errorf("unsupported notification type %q", notificationName)
h.sugar.Warnw("Invalid system notification type", "error", err, "notificationName", notificationName)
diff --git a/internal/api/handler/notifications_integration_test.go b/internal/api/handler/notifications_integration_test.go
index f57b1ebe..63ce2ec4 100644
--- a/internal/api/handler/notifications_integration_test.go
+++ b/internal/api/handler/notifications_integration_test.go
@@ -148,7 +148,7 @@ func (suite *NotificationsApiIntegrationSuite) TestListNotificationProviderStatu
func (suite *NotificationsApiIntegrationSuite) TestListSystemNotifications() {
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -159,7 +159,7 @@ func (suite *NotificationsApiIntegrationSuite) TestListSystemNotifications() {
}).Error)
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelEmail,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -169,7 +169,7 @@ func (suite *NotificationsApiIntegrationSuite) TestListSystemNotifications() {
}).Error)
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -205,7 +205,7 @@ func (suite *NotificationsApiIntegrationSuite) TestListSystemNotifications() {
func (suite *NotificationsApiIntegrationSuite) TestListSystemNotificationsDeduplicatesEquivalentDestinations() {
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -215,7 +215,7 @@ func (suite *NotificationsApiIntegrationSuite) TestListSystemNotificationsDedupl
}),
}).Error)
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -242,7 +242,7 @@ func (suite *NotificationsApiIntegrationSuite) TestListSystemNotificationsDedupl
func (suite *NotificationsApiIntegrationSuite) TestListSystemNotificationsIncludesConfiguredSupportedTypesOutsideDefaultBaseline() {
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Provider: notification.DeliveryChannelEmail,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -267,6 +267,33 @@ func (suite *NotificationsApiIntegrationSuite) TestListSystemNotificationsInclud
}, response.Data[0].ConfiguredDestinations)
}
+func (suite *NotificationsApiIntegrationSuite) TestListSystemNotificationsIncludesWorkflowExecutionFailed() {
+ suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
+ NotificationType: notification.SystemNotificationNameWorkflowExecutionFailed,
+ Provider: notification.DeliveryChannelEmail,
+ Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
+ Address: map[string]string{
+ emailprovider.AddressKeyEmail: "alerts@example.com",
+ },
+ }),
+ }).Error)
+
+ rec, req := suite.authedRequest(http.MethodGet, "/api/admin/notifications")
+
+ suite.server.E().ServeHTTP(rec, req)
+ suite.Equal(200, rec.Code, "Expected OK response for ListSystemNotifications")
+
+ var response GenericDataListResponse[systemNotificationResponse]
+ err := json.Unmarshal(rec.Body.Bytes(), &response)
+ suite.Require().NoError(err, "Failed to unmarshal notifications response")
+ suite.Require().Len(response.Data, 1)
+
+ suite.Equal("WORKFLOW_EXECUTION_FAILED", response.Data[0].Name)
+ suite.Equal([]configuredSystemDestinationResponse{
+ {ProviderType: "email", DestinationTarget: "alerts@example.com"},
+ }, response.Data[0].ConfiguredDestinations)
+}
+
func (suite *NotificationsApiIntegrationSuite) TestListSystemNotificationsReturnsEmptyDataWhenNoConfigurationsExist() {
rec, req := suite.authedRequest(http.MethodGet, "/api/admin/notifications")
@@ -299,7 +326,7 @@ func (suite *NotificationsApiIntegrationSuite) TestCreateSystemNotificationDesti
var rows []relational.SystemNotificationDestination
suite.Require().NoError(suite.DB.Find(&rows).Error)
suite.Require().Len(rows, 1)
- suite.Equal(notification.NotificationTypeEvidenceDigest, rows[0].NotificationType)
+ suite.Equal(notification.SubscriptionGateEvidenceDigest, rows[0].NotificationType)
suite.Equal(notification.DeliveryChannelEmail, rows[0].Provider)
suite.Equal("alerts@example.com", rows[0].Target.Data().Address[emailprovider.AddressKeyEmail])
}
@@ -334,15 +361,32 @@ func (suite *NotificationsApiIntegrationSuite) TestCreateSystemNotificationDesti
var rows []relational.SystemNotificationDestination
suite.Require().NoError(suite.DB.Find(&rows).Error)
suite.Require().Len(rows, 1)
- suite.Equal(notification.NotificationTypeTaskAvailable, rows[0].NotificationType)
+ suite.Equal(notification.SubscriptionGateTaskAvailable, rows[0].NotificationType)
suite.Equal(notification.DeliveryChannelSlack, rows[0].Provider)
suite.Equal("ccf-slack-int", rows[0].Target.Data().Address[slackprovider.AddressKeyChannel])
suite.Equal(slackprovider.TargetTypeChannel, rows[0].Target.Data().Address[slackprovider.AddressKeyTargetType])
}
+func (suite *NotificationsApiIntegrationSuite) TestCreateSystemNotificationDestinationWorkflowExecutionFailed() {
+ rec, req := suite.authedJSONRequest(http.MethodPost, "/api/admin/notifications/WORKFLOW_EXECUTION_FAILED/destinations", map[string]string{
+ "providerType": "email",
+ "destinationTarget": "alerts@example.com",
+ })
+
+ suite.server.E().ServeHTTP(rec, req)
+ suite.Equal(http.StatusCreated, rec.Code, "Expected Created response for workflow-execution-failed destination")
+
+ var rows []relational.SystemNotificationDestination
+ suite.Require().NoError(suite.DB.Find(&rows).Error)
+ suite.Require().Len(rows, 1)
+ suite.Equal(notification.SystemNotificationNameWorkflowExecutionFailed, rows[0].NotificationType)
+ suite.Equal(notification.DeliveryChannelEmail, rows[0].Provider)
+ suite.Equal("alerts@example.com", rows[0].Target.Data().Address[emailprovider.AddressKeyEmail])
+}
+
func (suite *NotificationsApiIntegrationSuite) TestCreateSystemNotificationDestinationRejectsDuplicateDestination() {
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelEmail,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -398,7 +442,7 @@ func (suite *NotificationsApiIntegrationSuite) TestCreateSystemNotificationDesti
func (suite *NotificationsApiIntegrationSuite) TestDeleteSystemNotificationDestination() {
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelEmail,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -422,7 +466,7 @@ func (suite *NotificationsApiIntegrationSuite) TestDeleteSystemNotificationDesti
func (suite *NotificationsApiIntegrationSuite) TestDeleteSystemNotificationDestinationAcceptsKebabCasePayload() {
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelEmail,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -442,7 +486,7 @@ func (suite *NotificationsApiIntegrationSuite) TestDeleteSystemNotificationDesti
func (suite *NotificationsApiIntegrationSuite) TestDeleteSystemNotificationDestinationRemovesDuplicateRows() {
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -452,7 +496,7 @@ func (suite *NotificationsApiIntegrationSuite) TestDeleteSystemNotificationDesti
}),
}).Error)
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -462,7 +506,7 @@ func (suite *NotificationsApiIntegrationSuite) TestDeleteSystemNotificationDesti
}),
}).Error)
suite.Require().NoError(suite.DB.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
diff --git a/internal/api/handler/users.go b/internal/api/handler/users.go
index acc8f664..5acd25ef 100644
--- a/internal/api/handler/users.go
+++ b/internal/api/handler/users.go
@@ -693,7 +693,7 @@ func (h *UserHandler) loadUserNotificationSubscriptions(ctx context.Context, use
for i := range rows {
channels := make([]string, len(rows[i].Channels))
copy(channels, rows[i].Channels)
- wireType, ok := notification.WireNotificationType(rows[i].NotificationType)
+ wireType, ok := notification.WireSubscriptionGate(rows[i].NotificationType)
if !ok {
wireType = rows[i].NotificationType
}
@@ -710,7 +710,7 @@ func normalizeNotificationSubscriptions(notifications map[string][]string) (map[
channelSets := make(map[string]map[string]struct{}, len(notifications))
for notificationType, channels := range notifications {
- normalizedType, ok := notification.NormalizeNotificationType(notificationType)
+ normalizedType, ok := notification.NormalizeSubscriptionGate(notificationType)
if !ok {
invalidType := strings.ToLower(strings.TrimSpace(notificationType))
return nil, fmt.Errorf("%w: %q", errInvalidNotificationTypes, invalidType)
diff --git a/internal/api/handler/users_integration_test.go b/internal/api/handler/users_integration_test.go
index 40809fb9..b6b4a770 100644
--- a/internal/api/handler/users_integration_test.go
+++ b/internal/api/handler/users_integration_test.go
@@ -649,10 +649,10 @@ func (suite *UserApiIntegrationSuite) TestSubscriptions() {
// Test subscribing to digest notifications.
payload := map[string]interface{}{
"notifications": map[string][]string{
- notification.NotificationTypeTaskAvailableWire: {"email", "slack", "email"},
- notification.NotificationTypeEvidenceDigestWire: {"email", "email"},
- notification.NotificationTypeTaskDailyDigestWire: {"email", "email"},
- notification.NotificationTypeRiskNotifications: {"email", "slack", "email"},
+ notification.SubscriptionGateTaskAvailableWire: {"email", "slack", "email"},
+ notification.SubscriptionGateEvidenceDigestWire: {"email", "email"},
+ notification.SubscriptionGateTaskDailyDigestWire: {"email", "email"},
+ notification.SubscriptionGateRiskNotifications: {"email", "slack", "email"},
},
}
payloadJSON, err := json.Marshal(payload)
@@ -674,16 +674,16 @@ func (suite *UserApiIntegrationSuite) TestSubscriptions() {
err = json.Unmarshal(rec.Body.Bytes(), &response)
suite.Require().NoError(err, "Failed to unmarshal UpdateSubscriptions response")
- suite.Equal([]string{"email", "slack"}, response.Data.Notifications[notification.NotificationTypeTaskAvailableWire], "Expected notifications to be normalized and persisted")
- suite.Equal([]string{"email"}, response.Data.Notifications[notification.NotificationTypeEvidenceDigestWire], "Expected evidence digest notifications to be normalized and persisted")
- suite.Equal([]string{"email"}, response.Data.Notifications[notification.NotificationTypeTaskDailyDigestWire], "Expected task daily digest notifications to be normalized and persisted")
- suite.Equal([]string{"email", "slack"}, response.Data.Notifications[notification.NotificationTypeRiskNotificationsWire], "Expected risk notifications to be normalized and persisted")
+ suite.Equal([]string{"email", "slack"}, response.Data.Notifications[notification.SubscriptionGateTaskAvailableWire], "Expected notifications to be normalized and persisted")
+ suite.Equal([]string{"email"}, response.Data.Notifications[notification.SubscriptionGateEvidenceDigestWire], "Expected evidence digest notifications to be normalized and persisted")
+ suite.Equal([]string{"email"}, response.Data.Notifications[notification.SubscriptionGateTaskDailyDigestWire], "Expected task daily digest notifications to be normalized and persisted")
+ suite.Equal([]string{"email", "slack"}, response.Data.Notifications[notification.SubscriptionGateRiskNotificationsWire], "Expected risk notifications to be normalized and persisted")
suite.Equal(int64(4), countStoredSubscriptions(), "Expected exactly one stored row per active notification type")
// Test unsubscribing by omitting previously-configured notification types from the map.
payload = map[string]interface{}{
"notifications": map[string][]string{
- notification.NotificationTypeTaskAvailableWire: {"email", "slack"},
+ notification.SubscriptionGateTaskAvailableWire: {"email", "slack"},
},
}
payloadJSON, err = json.Marshal(payload)
@@ -705,12 +705,12 @@ func (suite *UserApiIntegrationSuite) TestSubscriptions() {
err = json.Unmarshal(rec.Body.Bytes(), &response)
suite.Require().NoError(err, "Failed to unmarshal unsubscribe response")
- suite.Equal([]string{"email", "slack"}, response.Data.Notifications[notification.NotificationTypeTaskAvailableWire], "Expected task-available notifications to remain configured")
- _, hasDigestSubscription := response.Data.Notifications[notification.NotificationTypeEvidenceDigestWire]
+ suite.Equal([]string{"email", "slack"}, response.Data.Notifications[notification.SubscriptionGateTaskAvailableWire], "Expected task-available notifications to remain configured")
+ _, hasDigestSubscription := response.Data.Notifications[notification.SubscriptionGateEvidenceDigestWire]
suite.False(hasDigestSubscription, "Expected digest subscription to be removed when evidence_digest is omitted")
- _, hasTaskDailyDigestSubscription := response.Data.Notifications[notification.NotificationTypeTaskDailyDigestWire]
+ _, hasTaskDailyDigestSubscription := response.Data.Notifications[notification.SubscriptionGateTaskDailyDigestWire]
suite.False(hasTaskDailyDigestSubscription, "Expected task daily digest subscription to be removed when taskDailyDigest is omitted")
- _, hasRiskNotificationSubscription := response.Data.Notifications[notification.NotificationTypeRiskNotificationsWire]
+ _, hasRiskNotificationSubscription := response.Data.Notifications[notification.SubscriptionGateRiskNotificationsWire]
suite.False(hasRiskNotificationSubscription, "Expected risk notification subscription to be removed when risk_notifications is omitted")
suite.Equal(int64(1), countStoredSubscriptions(), "Expected old notification rows to be replaced rather than soft-deleted")
@@ -737,8 +737,8 @@ func (suite *UserApiIntegrationSuite) TestSubscriptions() {
err = json.Unmarshal(rec.Body.Bytes(), &response)
suite.Require().NoError(err, "Failed to unmarshal response for request without notifications")
- suite.Equal([]string{"email", "slack"}, response.Data.Notifications[notification.NotificationTypeTaskAvailableWire], "Expected notifications to remain unchanged when omitted")
- _, hasDigestSubscription = response.Data.Notifications[notification.NotificationTypeEvidenceDigestWire]
+ suite.Equal([]string{"email", "slack"}, response.Data.Notifications[notification.SubscriptionGateTaskAvailableWire], "Expected notifications to remain unchanged when omitted")
+ _, hasDigestSubscription = response.Data.Notifications[notification.SubscriptionGateEvidenceDigestWire]
suite.False(hasDigestSubscription, "Expected digest notification subscription to remain unchanged when notifications are omitted")
suite.Equal(int64(1), countStoredSubscriptions(), "Expected notification row count to remain stable when notifications are omitted")
})
@@ -747,7 +747,7 @@ func (suite *UserApiIntegrationSuite) TestSubscriptions() {
// Test with malformed notifications payload
payload := map[string]interface{}{
"notifications": map[string]interface{}{
- notification.NotificationTypeRiskNotifications: "invalid",
+ notification.SubscriptionGateRiskNotifications: "invalid",
},
}
payloadJSON, err := json.Marshal(payload)
@@ -764,7 +764,7 @@ func (suite *UserApiIntegrationSuite) TestSubscriptions() {
// Test with unsupported notification channel
payload2 := map[string]interface{}{
"notifications": map[string][]string{
- notification.NotificationTypeTaskAvailable: {"email", "pagerduty"},
+ notification.SubscriptionGateTaskAvailable: {"email", "pagerduty"},
},
}
payloadJSON2, err := json.Marshal(payload2)
diff --git a/internal/service/digest/notifications.go b/internal/service/digest/notifications.go
index 409f7c7e..2741bffe 100644
--- a/internal/service/digest/notifications.go
+++ b/internal/service/digest/notifications.go
@@ -16,7 +16,7 @@ import (
"gorm.io/gorm"
)
-const evidenceDigestKind = notification.Kind(notification.NotificationTypeEvidenceDigest)
+const evidenceDigestKind = notification.NotificationKindEvidenceDigest
type evidenceDigestNotificationModel struct {
UserName string
@@ -139,7 +139,7 @@ func NewNotificationService(
notification.NewGORMUserRepository(db),
notification.NewDefinition(
evidenceDigestKind,
- notification.NotificationTypeEvidenceDigest,
+ notification.SubscriptionGateEvidenceDigest,
emailprovider.TemplateChannel(func(ctx context.Context, model any) (emailprovider.TemplateContent, error) {
return renderEvidenceDigestEmail(ctx, model)
}),
@@ -269,7 +269,7 @@ func (s *Service) configuredDigestTargets(ctx context.Context) ([]notification.T
}
return notification.NewGORMSystemDestinationRepository(s.db, notificationproviders.NewLookup()).
- ListTargetsByNotificationType(ctx, notification.NotificationTypeEvidenceDigest)
+ ListTargetsBySubscriptionGate(ctx, notification.SubscriptionGateEvidenceDigest)
}
func audiencesForTargets(targets []notification.Target) []notification.Audience {
diff --git a/internal/service/digest/service.go b/internal/service/digest/service.go
index 066e5677..73e3fab9 100644
--- a/internal/service/digest/service.go
+++ b/internal/service/digest/service.go
@@ -175,7 +175,7 @@ func (s *Service) convertToEvidenceItems(evidences []relational.Evidence) []Evid
func (s *Service) GetDigestRecipients(ctx context.Context) ([]DigestRecipient, error) {
var subscriptions []relational.UserNotificationSubscription
if err := s.db.WithContext(ctx).
- Where("notification_type = ?", notification.NotificationTypeEvidenceDigest).
+ Where("notification_type = ?", notification.SubscriptionGateEvidenceDigest).
Find(&subscriptions).Error; err != nil {
return nil, fmt.Errorf("failed to fetch evidence digest subscriptions: %w", err)
}
diff --git a/internal/service/digest/service_test.go b/internal/service/digest/service_test.go
index 918dec11..940f9685 100644
--- a/internal/service/digest/service_test.go
+++ b/internal/service/digest/service_test.go
@@ -122,7 +122,7 @@ func TestHasGlobalDigestDestinations_RequiresConfiguredDestination(t *testing.T)
assert.False(t, service.hasGlobalDigestDestinations(context.Background()))
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelEmail,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -140,7 +140,7 @@ func TestDispatchEvidenceDigestNotificationsSupportsMultipleConfiguredDestinatio
require.NoError(t, db.AutoMigrate(&relational.SystemNotificationDestination{}))
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -150,7 +150,7 @@ func TestDispatchEvidenceDigestNotificationsSupportsMultipleConfiguredDestinatio
}),
}).Error)
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelEmail,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -161,7 +161,7 @@ func TestDispatchEvidenceDigestNotificationsSupportsMultipleConfiguredDestinatio
registry := notification.MustNewRegistry(notification.NewDefinition(
evidenceDigestKind,
- notification.NotificationTypeEvidenceDigest,
+ notification.SubscriptionGateEvidenceDigest,
notification.BindRenderer(notification.DeliveryChannelEmail, notification.ProviderRenderer(notification.DeliveryChannelEmail, func(context.Context, any) (any, error) {
return emailprovider.Content{From: "from@example.com", Subject: "Digest", TextBody: "body"}, nil
})),
diff --git a/internal/service/email/templates/service_test.go b/internal/service/email/templates/service_test.go
index ced383ce..9abc14ac 100644
--- a/internal/service/email/templates/service_test.go
+++ b/internal/service/email/templates/service_test.go
@@ -199,6 +199,8 @@ func TestTemplateService_WorkflowExecutionFailed_WithData(t *testing.T) {
"CompletedSteps": 3,
"TotalSteps": 5,
"WorkflowURL": "https://app.example.com/workflows/abc",
+ "MyTasksURL": "https://app.example.com/my-tasks",
+ "IsSystemAudience": false,
}
html, text, err := service.Use("workflow-execution-failed", data)
@@ -210,6 +212,7 @@ func TestTemplateService_WorkflowExecutionFailed_WithData(t *testing.T) {
require.Contains(t, html, "SOC2 2026")
require.Contains(t, html, "2 of 5 steps failed")
require.Contains(t, html, "exec-abc-123")
+ require.Contains(t, html, "View all my tasks")
require.Contains(t, text, "Alice Smith")
require.Contains(t, text, "SOC2 Audit")
require.Contains(t, text, "2 of 5 steps failed")
@@ -241,6 +244,38 @@ func TestTemplateService_WorkflowExecutionFailed_NoURL(t *testing.T) {
require.NotContains(t, html, "View Workflow Instance")
}
+func TestTemplateService_WorkflowExecutionFailed_SystemAudience(t *testing.T) {
+ service, err := NewTemplateService()
+ require.NoError(t, err)
+
+ data := TemplateData{
+ "RecipientName": "",
+ "WorkflowTitle": "Annual Audit",
+ "WorkflowInstanceName": "Audit 2026",
+ "ExecutionID": "exec-system-123",
+ "FailureReason": "step execution failed",
+ "FailedAt": "Wed, 19 Feb 2026 09:00:00 UTC",
+ "FailedSteps": 1,
+ "CompletedSteps": 2,
+ "TotalSteps": 3,
+ "WorkflowURL": "https://app.example.com/workflow-executions/exec-system-123",
+ "MyTasksURL": "",
+ "IsSystemAudience": true,
+ }
+
+ html, text, err := service.Use("workflow-execution-failed", data)
+ require.NoError(t, err)
+ require.NotEmpty(t, html)
+ require.NotEmpty(t, text)
+ require.Contains(t, html, "A workflow execution has failed and may require your attention.")
+ require.Contains(t, html, "configured for workflow failure alerts")
+ require.NotContains(t, html, "Hi ,")
+ require.NotContains(t, html, "View all my tasks")
+ require.Contains(t, text, "A workflow execution has failed and may require your attention.")
+ require.Contains(t, text, "configured for workflow failure alerts")
+ require.NotContains(t, text, "Hi ,")
+}
+
func TestTemplateService_WorkflowTaskDigest_WithTasks(t *testing.T) {
service, err := NewTemplateService()
require.NoError(t, err)
diff --git a/internal/service/email/templates/templates/workflow-execution-failed.html b/internal/service/email/templates/templates/workflow-execution-failed.html
index 4dd5c06e..a632b69a 100644
--- a/internal/service/email/templates/templates/workflow-execution-failed.html
+++ b/internal/service/email/templates/templates/workflow-execution-failed.html
@@ -141,7 +141,11 @@
⚠ Workflow Execution Failed
+ {{if .RecipientName}}
Hi {{.RecipientName}},
+ {{else}}
+
A workflow execution has failed and may require your attention.
+ {{end}}
{{.WorkflowInstanceName}} — execution failed
@@ -182,13 +186,17 @@
⚠ Workflow Execution Failed
{{if .WorkflowURL}}
View Workflow Instance{{end}}
-
{{end}}
diff --git a/internal/service/email/templates/templates/workflow-execution-failed.txt b/internal/service/email/templates/templates/workflow-execution-failed.txt
index 1f7cfa72..4b96d43a 100644
--- a/internal/service/email/templates/templates/workflow-execution-failed.txt
+++ b/internal/service/email/templates/templates/workflow-execution-failed.txt
@@ -1,8 +1,9 @@
WORKFLOW EXECUTION FAILED
=========================
-Hi {{.RecipientName}},
+{{if .RecipientName}}Hi {{.RecipientName}},
+{{end}}
A workflow execution has failed and may require your attention.
WORKFLOW: {{.WorkflowTitle}}
@@ -28,5 +29,7 @@ VIEW WORKFLOW INSTANCE
{{.WorkflowURL}}
{{end}}
---
-You are receiving this because you are the owner of this workflow instance.
+{{if .IsSystemAudience}}You are receiving this because this destination is configured for workflow failure alerts.
+{{else}}You are receiving this because you are the owner of this workflow instance.
+{{end}}
Compliance Framework
diff --git a/internal/service/migrator.go b/internal/service/migrator.go
index 87dc16a5..35cca526 100644
--- a/internal/service/migrator.go
+++ b/internal/service/migrator.go
@@ -265,14 +265,14 @@ func migrateLegacySystemNotificationDestinations(db *gorm.DB, cfg *config.Config
var existing relational.SystemNotificationDestination
if err := db.
- Where("notification_type = ? AND provider = ?", notification.NotificationTypeEvidenceDigest, notification.DeliveryChannelSlack).
+ Where("notification_type = ? AND provider = ?", notification.SubscriptionGateEvidenceDigest, notification.DeliveryChannelSlack).
First(&existing).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to query existing system notification destination %q: %w", slackprovider.ConfiguredDestinationDigestChan, err)
}
row := relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -322,7 +322,7 @@ func migrateLegacyTaskAvailableEmailSubscriptions(db *gorm.DB) error {
if err := backfillLegacyNotificationSubscriptions(
db,
"task_available_email_subscribed",
- notification.NotificationTypeTaskAvailable,
+ notification.SubscriptionGateTaskAvailable,
"task-available email",
); err != nil {
return err
@@ -344,7 +344,7 @@ func migrateLegacyDigestSubscriptions(db *gorm.DB) error {
if err := backfillLegacyNotificationSubscriptions(
db,
"digest_subscribed",
- notification.NotificationTypeEvidenceDigest,
+ notification.SubscriptionGateEvidenceDigest,
"evidence digest",
); err != nil {
return err
@@ -366,7 +366,7 @@ func migrateLegacyTaskDailyDigestSubscriptions(db *gorm.DB) error {
if err := backfillLegacyNotificationSubscriptions(
db,
"task_daily_digest_subscribed",
- notification.NotificationTypeTaskDailyDigest,
+ notification.SubscriptionGateTaskDailyDigest,
"task daily digest",
); err != nil {
return err
@@ -388,7 +388,7 @@ func migrateLegacyRiskNotificationSubscriptions(db *gorm.DB) error {
if err := backfillLegacyNotificationSubscriptions(
db,
"risk_notifications_subscribed",
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
"risk notifications",
); err != nil {
return err
diff --git a/internal/service/migrator_test.go b/internal/service/migrator_test.go
index 285ee4ee..e0cd0b9e 100644
--- a/internal/service/migrator_test.go
+++ b/internal/service/migrator_test.go
@@ -52,7 +52,7 @@ func TestBackfillLegacyNotificationSubscriptions(t *testing.T) {
existing := relational.UserNotificationSubscription{
UserID: userWithExistingSubscription.ID.String(),
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: []string{notification.DeliveryChannelEmail},
}
require.NoError(t, db.Create(&existing).Error)
@@ -61,7 +61,7 @@ func TestBackfillLegacyNotificationSubscriptions(t *testing.T) {
backfillLegacyNotificationSubscriptions(
db,
"task_available_email_subscribed",
- notification.NotificationTypeTaskAvailable,
+ notification.SubscriptionGateTaskAvailable,
"task-available email",
),
)
@@ -75,17 +75,17 @@ func TestBackfillLegacyNotificationSubscriptions(t *testing.T) {
rowsByUserID[row.UserID] = row
}
- assert.Equal(t, notification.NotificationTypeTaskAvailable, rowsByUserID[userWithLegacySubscription.ID.String()].NotificationType)
+ assert.Equal(t, notification.SubscriptionGateTaskAvailable, rowsByUserID[userWithLegacySubscription.ID.String()].NotificationType)
assert.Equal(t, []string{notification.DeliveryChannelEmail}, []string(rowsByUserID[userWithLegacySubscription.ID.String()].Channels))
- assert.Equal(t, notification.NotificationTypeTaskAvailable, rowsByUserID[userWithExistingSubscription.ID.String()].NotificationType)
+ assert.Equal(t, notification.SubscriptionGateTaskAvailable, rowsByUserID[userWithExistingSubscription.ID.String()].NotificationType)
assert.Equal(t, []string{notification.DeliveryChannelEmail}, []string(rowsByUserID[userWithExistingSubscription.ID.String()].Channels))
require.NoError(t,
backfillLegacyNotificationSubscriptions(
db,
"task_available_email_subscribed",
- notification.NotificationTypeTaskAvailable,
+ notification.SubscriptionGateTaskAvailable,
"task-available email",
),
)
@@ -131,7 +131,7 @@ func TestBackfillLegacyRiskNotificationSubscriptions(t *testing.T) {
var rows []relational.UserNotificationSubscription
require.NoError(t, db.Find(&rows).Error)
require.Len(t, rows, 1)
- assert.Equal(t, notification.NotificationTypeRiskNotifications, rows[0].NotificationType)
+ assert.Equal(t, notification.SubscriptionGateRiskNotifications, rows[0].NotificationType)
assert.Equal(t, []string{notification.DeliveryChannelEmail}, []string(rows[0].Channels))
assert.Equal(t, user.ID.String(), rows[0].UserID)
@@ -158,7 +158,7 @@ func TestMigrateLegacySystemNotificationDestinationsBackfillsSlackDigestChannel(
var rows []relational.SystemNotificationDestination
require.NoError(t, db.Find(&rows).Error)
require.Len(t, rows, 1)
- assert.Equal(t, notification.NotificationTypeEvidenceDigest, rows[0].NotificationType)
+ assert.Equal(t, notification.SubscriptionGateEvidenceDigest, rows[0].NotificationType)
assert.Equal(t, notification.DeliveryChannelSlack, rows[0].Provider)
target := rows[0].Target.Data()
assert.Equal(t, "ccf-alerts", target.Address[slackprovider.AddressKeyChannel])
@@ -171,7 +171,7 @@ func TestMigrateLegacySystemNotificationDestinationsDoesNotOverwriteExistingRow(
require.NoError(t, db.AutoMigrate(&relational.SystemNotificationDestination{}))
existing := relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
diff --git a/internal/service/notification/constants.go b/internal/service/notification/constants.go
index b3d64999..db793e11 100644
--- a/internal/service/notification/constants.go
+++ b/internal/service/notification/constants.go
@@ -5,58 +5,89 @@ import (
)
const (
- NotificationTypeUngated = ""
-
- NotificationTypeEvidenceDigest = "evidence_digest"
- NotificationTypeTaskAvailable = "task_available"
- NotificationTypeTaskDailyDigest = "task_daily_digest"
- NotificationTypeRiskNotifications = "risk_notifications"
-
- NotificationTypeEvidenceDigestWire = "evidenceDigest"
- NotificationTypeTaskAvailableWire = "taskAvailable"
- NotificationTypeTaskDailyDigestWire = "taskDailyDigest"
- NotificationTypeRiskNotificationsWire = "riskNotifications"
-
- notificationTypeEvidenceDigestWireNormalized = "evidencedigest"
- notificationTypeTaskAvailableWireNormalized = "taskavailable"
- notificationTypeTaskDailyDigestWireNormalized = "taskdailydigest"
- notificationTypeRiskNotificationsWireNormalized = "risknotifications"
+ notificationNameEvidenceDigest = "evidence_digest"
+ notificationNameTaskAvailable = "task_available"
+ notificationNameTaskDailyDigest = "task_daily_digest"
+ notificationNameRiskNotifications = "risk_notifications"
+ notificationNameWorkflowExecutionFailed = "workflow_execution_failed"
+
+ NotificationKindEvidenceDigest = Kind(notificationNameEvidenceDigest)
+ NotificationKindWorkflowExecutionFailed = Kind(notificationNameWorkflowExecutionFailed)
+
+ SystemNotificationNameEvidenceDigest = notificationNameEvidenceDigest
+ SystemNotificationNameTaskAvailable = notificationNameTaskAvailable
+ SystemNotificationNameTaskDailyDigest = notificationNameTaskDailyDigest
+ SystemNotificationNameRiskNotifications = notificationNameRiskNotifications
+ SystemNotificationNameWorkflowExecutionFailed = notificationNameWorkflowExecutionFailed
+
+ SubscriptionGateUngated = ""
+
+ SubscriptionGateEvidenceDigest = notificationNameEvidenceDigest
+ SubscriptionGateTaskAvailable = notificationNameTaskAvailable
+ SubscriptionGateTaskDailyDigest = notificationNameTaskDailyDigest
+ SubscriptionGateRiskNotifications = notificationNameRiskNotifications
+
+ SubscriptionGateEvidenceDigestWire = "evidenceDigest"
+ SubscriptionGateTaskAvailableWire = "taskAvailable"
+ SubscriptionGateTaskDailyDigestWire = "taskDailyDigest"
+ SubscriptionGateRiskNotificationsWire = "riskNotifications"
+
+ subscriptionGateEvidenceDigestWireNormalized = "evidencedigest"
+ subscriptionGateTaskAvailableWireNormalized = "taskavailable"
+ subscriptionGateTaskDailyDigestWireNormalized = "taskdailydigest"
+ subscriptionGateRiskNotificationsWireNormalized = "risknotifications"
+
+ systemNotificationWorkflowExecutionFailedWireNormalized = "workflowexecutionfailed"
)
-var notificationTypeInputAliases = map[string]string{
- NotificationTypeEvidenceDigest: NotificationTypeEvidenceDigest,
- NotificationTypeTaskAvailable: NotificationTypeTaskAvailable,
- NotificationTypeTaskDailyDigest: NotificationTypeTaskDailyDigest,
- NotificationTypeRiskNotifications: NotificationTypeRiskNotifications,
- notificationTypeEvidenceDigestWireNormalized: NotificationTypeEvidenceDigest,
- notificationTypeTaskAvailableWireNormalized: NotificationTypeTaskAvailable,
- notificationTypeTaskDailyDigestWireNormalized: NotificationTypeTaskDailyDigest,
- notificationTypeRiskNotificationsWireNormalized: NotificationTypeRiskNotifications,
+var subscriptionGateInputAliases = map[string]string{
+ SubscriptionGateEvidenceDigest: SubscriptionGateEvidenceDigest,
+ SubscriptionGateTaskAvailable: SubscriptionGateTaskAvailable,
+ SubscriptionGateTaskDailyDigest: SubscriptionGateTaskDailyDigest,
+ SubscriptionGateRiskNotifications: SubscriptionGateRiskNotifications,
+ subscriptionGateEvidenceDigestWireNormalized: SubscriptionGateEvidenceDigest,
+ subscriptionGateTaskAvailableWireNormalized: SubscriptionGateTaskAvailable,
+ subscriptionGateTaskDailyDigestWireNormalized: SubscriptionGateTaskDailyDigest,
+ subscriptionGateRiskNotificationsWireNormalized: SubscriptionGateRiskNotifications,
}
-var notificationTypeWireValues = map[string]string{
- NotificationTypeEvidenceDigest: NotificationTypeEvidenceDigestWire,
- NotificationTypeTaskAvailable: NotificationTypeTaskAvailableWire,
- NotificationTypeTaskDailyDigest: NotificationTypeTaskDailyDigestWire,
- NotificationTypeRiskNotifications: NotificationTypeRiskNotificationsWire,
+var subscriptionGateWireValues = map[string]string{
+ SubscriptionGateEvidenceDigest: SubscriptionGateEvidenceDigestWire,
+ SubscriptionGateTaskAvailable: SubscriptionGateTaskAvailableWire,
+ SubscriptionGateTaskDailyDigest: SubscriptionGateTaskDailyDigestWire,
+ SubscriptionGateRiskNotifications: SubscriptionGateRiskNotificationsWire,
}
-var systemNotificationTypes = []string{
- NotificationTypeEvidenceDigest,
+var systemNotificationInputAliases = map[string]string{
+ SubscriptionGateEvidenceDigest: SystemNotificationNameEvidenceDigest,
+ SubscriptionGateTaskAvailable: SystemNotificationNameTaskAvailable,
+ SubscriptionGateTaskDailyDigest: SystemNotificationNameTaskDailyDigest,
+ SubscriptionGateRiskNotifications: SystemNotificationNameRiskNotifications,
+ notificationNameWorkflowExecutionFailed: SystemNotificationNameWorkflowExecutionFailed,
+ subscriptionGateEvidenceDigestWireNormalized: SystemNotificationNameEvidenceDigest,
+ subscriptionGateTaskAvailableWireNormalized: SystemNotificationNameTaskAvailable,
+ subscriptionGateTaskDailyDigestWireNormalized: SystemNotificationNameTaskDailyDigest,
+ subscriptionGateRiskNotificationsWireNormalized: SystemNotificationNameRiskNotifications,
+ systemNotificationWorkflowExecutionFailedWireNormalized: SystemNotificationNameWorkflowExecutionFailed,
+}
+
+var systemNotificationNames = []string{
+ SystemNotificationNameEvidenceDigest,
+ SystemNotificationNameWorkflowExecutionFailed,
}
func normalizeToken(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
-// NormalizeNotificationType canonicalizes a notification type and verifies support.
-func NormalizeNotificationType(notificationType string) (string, bool) {
- normalized := normalizeToken(notificationType)
+// NormalizeSubscriptionGate canonicalizes a subscription gate and verifies support.
+func NormalizeSubscriptionGate(subscriptionGate string) (string, bool) {
+ normalized := normalizeToken(subscriptionGate)
if normalized == "" {
return "", false
}
- canonical, ok := notificationTypeInputAliases[normalized]
+ canonical, ok := subscriptionGateInputAliases[normalized]
if !ok {
return "", false
}
@@ -64,14 +95,14 @@ func NormalizeNotificationType(notificationType string) (string, bool) {
return canonical, true
}
-// WireNotificationType returns camelCase for a supported notification type.
-func WireNotificationType(notificationType string) (string, bool) {
- canonical, ok := NormalizeNotificationType(notificationType)
+// WireSubscriptionGate returns camelCase for a supported subscription gate.
+func WireSubscriptionGate(subscriptionGate string) (string, bool) {
+ canonical, ok := NormalizeSubscriptionGate(subscriptionGate)
if !ok {
return "", false
}
- wireValue, ok := notificationTypeWireValues[canonical]
+ wireValue, ok := subscriptionGateWireValues[canonical]
if !ok {
return "", false
}
@@ -79,6 +110,21 @@ func WireNotificationType(notificationType string) (string, bool) {
return wireValue, true
}
-func SystemNotificationTypes() []string {
- return append([]string(nil), systemNotificationTypes...)
+// NormalizeSystemNotificationName canonicalizes a system notification name and verifies support.
+func NormalizeSystemNotificationName(notificationName string) (string, bool) {
+ normalized := normalizeToken(notificationName)
+ if normalized == "" {
+ return "", false
+ }
+
+ canonical, ok := systemNotificationInputAliases[normalized]
+ if !ok {
+ return "", false
+ }
+
+ return canonical, true
+}
+
+func SystemNotificationNames() []string {
+ return append([]string(nil), systemNotificationNames...)
}
diff --git a/internal/service/notification/constants_test.go b/internal/service/notification/constants_test.go
index cc3a3717..1f89aab1 100644
--- a/internal/service/notification/constants_test.go
+++ b/internal/service/notification/constants_test.go
@@ -24,72 +24,91 @@ func TestNormalizeDeliveryChannels_EmptyIsInvalid(t *testing.T) {
assert.Equal(t, 1, len(invalid))
}
-func TestNormalizeNotificationType(t *testing.T) {
- normalized, ok := NormalizeNotificationType(" Task_Available ")
+func TestNormalizeSubscriptionGate(t *testing.T) {
+ normalized, ok := NormalizeSubscriptionGate(" Task_Available ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeTaskAvailable, normalized)
+ assert.Equal(t, SubscriptionGateTaskAvailable, normalized)
}
-func TestNormalizeNotificationType_EvidenceDigest(t *testing.T) {
- normalized, ok := NormalizeNotificationType(" Evidence_Digest ")
+func TestNormalizeSubscriptionGate_EvidenceDigest(t *testing.T) {
+ normalized, ok := NormalizeSubscriptionGate(" Evidence_Digest ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeEvidenceDigest, normalized)
+ assert.Equal(t, SubscriptionGateEvidenceDigest, normalized)
}
-func TestNormalizeNotificationType_TaskDailyDigest(t *testing.T) {
- normalized, ok := NormalizeNotificationType(" Task_Daily_Digest ")
+func TestNormalizeSubscriptionGate_TaskDailyDigest(t *testing.T) {
+ normalized, ok := NormalizeSubscriptionGate(" Task_Daily_Digest ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeTaskDailyDigest, normalized)
+ assert.Equal(t, SubscriptionGateTaskDailyDigest, normalized)
}
-func TestNormalizeNotificationType_RiskNotifications(t *testing.T) {
- normalized, ok := NormalizeNotificationType(" Risk_Notifications ")
+func TestNormalizeSubscriptionGate_RiskNotifications(t *testing.T) {
+ normalized, ok := NormalizeSubscriptionGate(" Risk_Notifications ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeRiskNotifications, normalized)
+ assert.Equal(t, SubscriptionGateRiskNotifications, normalized)
}
-func TestNormalizeNotificationType_CamelCaseAliases(t *testing.T) {
- normalized, ok := NormalizeNotificationType(" taskAvailable ")
+func TestNormalizeSubscriptionGate_CamelCaseAliases(t *testing.T) {
+ normalized, ok := NormalizeSubscriptionGate(" taskAvailable ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeTaskAvailable, normalized)
+ assert.Equal(t, SubscriptionGateTaskAvailable, normalized)
- normalized, ok = NormalizeNotificationType(" evidenceDigest ")
+ normalized, ok = NormalizeSubscriptionGate(" evidenceDigest ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeEvidenceDigest, normalized)
+ assert.Equal(t, SubscriptionGateEvidenceDigest, normalized)
- normalized, ok = NormalizeNotificationType(" taskDailyDigest ")
+ normalized, ok = NormalizeSubscriptionGate(" taskDailyDigest ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeTaskDailyDigest, normalized)
+ assert.Equal(t, SubscriptionGateTaskDailyDigest, normalized)
- normalized, ok = NormalizeNotificationType(" riskNotifications ")
+ normalized, ok = NormalizeSubscriptionGate(" riskNotifications ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeRiskNotifications, normalized)
+ assert.Equal(t, SubscriptionGateRiskNotifications, normalized)
}
-func TestWireNotificationType(t *testing.T) {
- wireType, ok := WireNotificationType(" task_available ")
+func TestWireSubscriptionGate(t *testing.T) {
+ wireType, ok := WireSubscriptionGate(" task_available ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeTaskAvailableWire, wireType)
+ assert.Equal(t, SubscriptionGateTaskAvailableWire, wireType)
- wireType, ok = WireNotificationType(" evidenceDigest ")
+ wireType, ok = WireSubscriptionGate(" evidenceDigest ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeEvidenceDigestWire, wireType)
+ assert.Equal(t, SubscriptionGateEvidenceDigestWire, wireType)
- wireType, ok = WireNotificationType(" taskDailyDigest ")
+ wireType, ok = WireSubscriptionGate(" taskDailyDigest ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeTaskDailyDigestWire, wireType)
+ assert.Equal(t, SubscriptionGateTaskDailyDigestWire, wireType)
- wireType, ok = WireNotificationType(" risk_notifications ")
+ wireType, ok = WireSubscriptionGate(" risk_notifications ")
assert.True(t, ok)
- assert.Equal(t, NotificationTypeRiskNotificationsWire, wireType)
+ assert.Equal(t, SubscriptionGateRiskNotificationsWire, wireType)
}
-func TestNormalizeNotificationType_Invalid(t *testing.T) {
- normalized, ok := NormalizeNotificationType("risk_opened")
+func TestNormalizeSubscriptionGate_Invalid(t *testing.T) {
+ normalized, ok := NormalizeSubscriptionGate("risk_opened")
assert.False(t, ok)
assert.Equal(t, "", normalized)
}
-func TestSystemNotificationTypes(t *testing.T) {
- assert.Equal(t, []string{NotificationTypeEvidenceDigest}, SystemNotificationTypes())
+func TestNormalizeSystemNotificationName(t *testing.T) {
+ normalized, ok := NormalizeSystemNotificationName(" workflowExecutionFailed ")
+ assert.True(t, ok)
+ assert.Equal(t, SystemNotificationNameWorkflowExecutionFailed, normalized)
+
+ normalized, ok = NormalizeSystemNotificationName(" TASK_AVAILABLE ")
+ assert.True(t, ok)
+ assert.Equal(t, SystemNotificationNameTaskAvailable, normalized)
+}
+
+func TestNormalizeSystemNotificationNameRejectsUnsupportedName(t *testing.T) {
+ normalized, ok := NormalizeSystemNotificationName("risk_opened")
+ assert.False(t, ok)
+ assert.Equal(t, "", normalized)
+}
+
+func TestSystemNotificationNames(t *testing.T) {
+ assert.Equal(t, []string{
+ SystemNotificationNameEvidenceDigest,
+ SystemNotificationNameWorkflowExecutionFailed,
+ }, SystemNotificationNames())
}
diff --git a/internal/service/notification/definitions.go b/internal/service/notification/definitions.go
index cdb775fe..08ed3610 100644
--- a/internal/service/notification/definitions.go
+++ b/internal/service/notification/definitions.go
@@ -30,7 +30,7 @@ func BindRenderer(provider string, renderer ChannelRenderer) RendererBinding {
}
}
-func NewDefinition(kind Kind, subscriptionType string, bindings ...RendererBinding) Definition {
+func NewDefinition(kind Kind, subscriptionGate string, bindings ...RendererBinding) Definition {
supportedChannels := make([]string, 0, len(bindings))
renderers := make(map[string]ChannelRenderer, len(bindings))
@@ -42,7 +42,7 @@ func NewDefinition(kind Kind, subscriptionType string, bindings ...RendererBindi
return Definition{
Kind: kind,
- SubscriptionType: subscriptionType,
+ SubscriptionGate: subscriptionGate,
SupportedChannels: supportedChannels,
Renderers: renderers,
}
@@ -50,7 +50,7 @@ func NewDefinition(kind Kind, subscriptionType string, bindings ...RendererBindi
type Definition struct {
Kind Kind
- SubscriptionType string
+ SubscriptionGate string
SupportedChannels []string
Renderers map[string]ChannelRenderer
}
@@ -95,7 +95,7 @@ func (d Definition) normalized() (Definition, error) {
normalized := d
normalized.Kind = Kind(strings.TrimSpace(string(d.Kind)))
normalized.SupportedChannels = append([]string(nil), channels...)
- normalized.SubscriptionType = canonicalSubscriptionType(d.SubscriptionType)
+ normalized.SubscriptionGate = canonicalSubscriptionGate(d.SubscriptionGate)
normalized.Renderers = make(map[string]ChannelRenderer, len(d.Renderers))
for key, renderer := range d.Renderers {
channel, ok := NormalizeDeliveryChannel(key)
@@ -114,12 +114,12 @@ func (d Definition) normalized() (Definition, error) {
return normalized, nil
}
-func canonicalSubscriptionType(subscriptionType string) string {
- trimmed := strings.TrimSpace(subscriptionType)
+func canonicalSubscriptionGate(subscriptionGate string) string {
+ trimmed := strings.TrimSpace(subscriptionGate)
if trimmed == "" {
return ""
}
- if canonical, ok := NormalizeNotificationType(trimmed); ok {
+ if canonical, ok := NormalizeSubscriptionGate(trimmed); ok {
return canonical
}
return trimmed
diff --git a/internal/service/notification/definitions_test.go b/internal/service/notification/definitions_test.go
index 86f98f7b..e51d87b1 100644
--- a/internal/service/notification/definitions_test.go
+++ b/internal/service/notification/definitions_test.go
@@ -24,5 +24,5 @@ func TestNewDefinitionBuildsSupportedChannelsAndRenderers(t *testing.T) {
assert.Equal(t, []string{"email", "slack"}, definition.SupportedChannels)
assert.Contains(t, definition.Renderers, "email")
assert.Contains(t, definition.Renderers, "slack")
- assert.Equal(t, " taskAvailable ", definition.SubscriptionType)
+ assert.Equal(t, " taskAvailable ", definition.SubscriptionGate)
}
diff --git a/internal/service/notification/gorm_configured_destination_resolver.go b/internal/service/notification/gorm_configured_destination_resolver.go
index 8dc06e1f..56b627b7 100644
--- a/internal/service/notification/gorm_configured_destination_resolver.go
+++ b/internal/service/notification/gorm_configured_destination_resolver.go
@@ -77,7 +77,7 @@ func configuredDestinationLookup(key string) (configuredDestinationRecordLookup,
switch strings.TrimSpace(key) {
case configuredDestinationKeySlackDigest:
return configuredDestinationRecordLookup{
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Provider: DeliveryChannelSlack,
}, true
default:
diff --git a/internal/service/notification/gorm_configured_destination_resolver_test.go b/internal/service/notification/gorm_configured_destination_resolver_test.go
index c9f122d9..c1d6de83 100644
--- a/internal/service/notification/gorm_configured_destination_resolver_test.go
+++ b/internal/service/notification/gorm_configured_destination_resolver_test.go
@@ -26,7 +26,7 @@ func TestGORMConfiguredDestinationResolverResolveConfiguredDestination(t *testin
require.NoError(t, db.AutoMigrate(&relational.SystemNotificationDestination{}))
record := relational.SystemNotificationDestination{
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Provider: DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -57,7 +57,7 @@ func TestGORMConfiguredDestinationResolverResolveConfiguredDestinationUsesNewest
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
CreatedAt: older,
UpdatedAt: older,
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Provider: DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -69,7 +69,7 @@ func TestGORMConfiguredDestinationResolverResolveConfiguredDestinationUsesNewest
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
CreatedAt: newer,
UpdatedAt: newer,
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Provider: DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
diff --git a/internal/service/notification/gorm_user_repository_test.go b/internal/service/notification/gorm_user_repository_test.go
index 23a62106..cf52b99b 100644
--- a/internal/service/notification/gorm_user_repository_test.go
+++ b/internal/service/notification/gorm_user_repository_test.go
@@ -26,7 +26,7 @@ func TestGORMUserRepositoryFindUserByIDIncludesSlackIdentity(t *testing.T) {
}).Error)
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: userID.String(),
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Channels: datatypes.JSONSlice[string]{DeliveryChannelEmail, DeliveryChannelSlack},
}).Error)
require.NoError(t, db.Create(&relational.SlackUserLink{
@@ -45,7 +45,7 @@ func TestGORMUserRepositoryFindUserByIDIncludesSlackIdentity(t *testing.T) {
assert.Equal(t, "direct_message", identity["target_type"])
}
-func TestGORMUserRepositoryListActiveUsersByNotificationTypeIncludesSlackIdentity(t *testing.T) {
+func TestGORMUserRepositoryListActiveUsersBySubscriptionGateIncludesSlackIdentity(t *testing.T) {
db := newNotificationTestDB(t)
userID := uuid.New()
@@ -57,7 +57,7 @@ func TestGORMUserRepositoryListActiveUsersByNotificationTypeIncludesSlackIdentit
}).Error)
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: userID.String(),
- NotificationType: NotificationTypeTaskDailyDigest,
+ NotificationType: SubscriptionGateTaskDailyDigest,
Channels: datatypes.JSONSlice[string]{DeliveryChannelSlack},
}).Error)
require.NoError(t, db.Create(&relational.SlackUserLink{
@@ -67,7 +67,7 @@ func TestGORMUserRepositoryListActiveUsersByNotificationTypeIncludesSlackIdentit
}).Error)
repo := NewGORMUserRepository(db)
- users, err := repo.ListActiveUsersByNotificationType(context.Background(), NotificationTypeTaskDailyDigest)
+ users, err := repo.ListActiveUsersBySubscriptionGate(context.Background(), SubscriptionGateTaskDailyDigest)
require.NoError(t, err)
require.Len(t, users, 1)
@@ -77,7 +77,7 @@ func TestGORMUserRepositoryListActiveUsersByNotificationTypeIncludesSlackIdentit
assert.Equal(t, "direct_message", identity["target_type"])
}
-func TestGORMUserRepositoryListActiveUserIDsByNotificationType(t *testing.T) {
+func TestGORMUserRepositoryListActiveUserIDsBySubscriptionGate(t *testing.T) {
db := newNotificationTestDB(t)
activeID := uuid.New()
inactiveID := uuid.New()
@@ -115,27 +115,27 @@ func TestGORMUserRepositoryListActiveUserIDsByNotificationType(t *testing.T) {
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: activeID.String(),
- NotificationType: NotificationTypeTaskDailyDigest,
+ NotificationType: SubscriptionGateTaskDailyDigest,
Channels: datatypes.JSONSlice[string]{DeliveryChannelEmail},
}).Error)
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: inactiveID.String(),
- NotificationType: NotificationTypeTaskDailyDigest,
+ NotificationType: SubscriptionGateTaskDailyDigest,
Channels: datatypes.JSONSlice[string]{DeliveryChannelEmail},
}).Error)
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: lockedID.String(),
- NotificationType: NotificationTypeTaskDailyDigest,
+ NotificationType: SubscriptionGateTaskDailyDigest,
Channels: datatypes.JSONSlice[string]{DeliveryChannelSlack},
}).Error)
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: invalidChannelID.String(),
- NotificationType: NotificationTypeTaskDailyDigest,
+ NotificationType: SubscriptionGateTaskDailyDigest,
Channels: datatypes.JSONSlice[string]{"invalid-channel"},
}).Error)
repo := NewGORMUserRepository(db)
- userIDs, err := repo.ListActiveUserIDsByNotificationType(context.Background(), NotificationTypeTaskDailyDigest)
+ userIDs, err := repo.ListActiveUserIDsBySubscriptionGate(context.Background(), SubscriptionGateTaskDailyDigest)
require.NoError(t, err)
require.Len(t, userIDs, 1)
assert.Equal(t, activeID.String(), userIDs[0])
diff --git a/internal/service/notification/registry_test.go b/internal/service/notification/registry_test.go
index e51cf11b..a431f6b9 100644
--- a/internal/service/notification/registry_test.go
+++ b/internal/service/notification/registry_test.go
@@ -11,7 +11,7 @@ import (
func TestRegistryRegisterNormalizesDefinition(t *testing.T) {
registry, err := NewRegistry(Definition{
Kind: Kind("risk_review_due"),
- SubscriptionType: " riskNotifications ",
+ SubscriptionGate: " riskNotifications ",
SupportedChannels: []string{" Slack ", "email", "slack"},
Renderers: map[string]ChannelRenderer{
DeliveryChannelEmail: ProviderRenderer(DeliveryChannelEmail, func(context.Context, any) (any, error) {
@@ -26,7 +26,7 @@ func TestRegistryRegisterNormalizesDefinition(t *testing.T) {
definition, ok := registry.Definition(Kind("risk_review_due"))
require.True(t, ok)
- assert.Equal(t, NotificationTypeRiskNotifications, definition.SubscriptionType)
+ assert.Equal(t, SubscriptionGateRiskNotifications, definition.SubscriptionGate)
assert.Equal(t, []string{DeliveryChannelEmail, DeliveryChannelSlack}, definition.SupportedChannels)
}
diff --git a/internal/service/notification/resolver.go b/internal/service/notification/resolver.go
index a96df91a..efb98cd9 100644
--- a/internal/service/notification/resolver.go
+++ b/internal/service/notification/resolver.go
@@ -34,8 +34,8 @@ func (u User) FullName() string {
return strings.TrimSpace(u.FirstName) + " " + strings.TrimSpace(u.LastName)
}
-func (u User) NotificationChannels(notificationType string) []string {
- normalizedType, ok := NormalizeNotificationType(notificationType)
+func (u User) NotificationChannels(subscriptionGate string) []string {
+ normalizedGate, ok := NormalizeSubscriptionGate(subscriptionGate)
if !ok {
return nil
}
@@ -43,8 +43,8 @@ func (u User) NotificationChannels(notificationType string) []string {
seen := make(map[string]struct{})
channels := make([]string, 0)
for _, subscription := range u.Subscriptions {
- currentType, typeOK := NormalizeNotificationType(subscription.NotificationType)
- if !typeOK || currentType != normalizedType {
+ currentGate, gateOK := NormalizeSubscriptionGate(subscription.NotificationType)
+ if !gateOK || currentGate != normalizedGate {
continue
}
@@ -66,7 +66,7 @@ func (u User) NotificationChannels(notificationType string) []string {
type UserRepository interface {
FindUserByID(ctx context.Context, userID string) (User, error)
- ListActiveUsersByNotificationType(ctx context.Context, notificationType string) ([]User, error)
+ ListActiveUsersBySubscriptionGate(ctx context.Context, subscriptionGate string) ([]User, error)
}
type ActiveUserRepository interface {
@@ -128,17 +128,17 @@ func (r *GORMUserRepository) FindUserByID(ctx context.Context, userID string) (U
}, nil
}
-func (r *GORMUserRepository) ListActiveUsersByNotificationType(ctx context.Context, notificationType string) ([]User, error) {
- canonicalType, ok := NormalizeNotificationType(notificationType)
+func (r *GORMUserRepository) ListActiveUsersBySubscriptionGate(ctx context.Context, subscriptionGate string) ([]User, error) {
+ canonicalGate, ok := NormalizeSubscriptionGate(subscriptionGate)
if !ok {
return []User{}, nil
}
var subscriptions []relational.UserNotificationSubscription
if err := r.db.WithContext(ctx).
- Where("notification_type = ?", canonicalType).
+ Where("notification_type = ?", canonicalGate).
Find(&subscriptions).Error; err != nil {
- return nil, fmt.Errorf("failed to fetch notification subscriptions for type %s: %w", canonicalType, err)
+ return nil, fmt.Errorf("failed to fetch notification subscriptions for gate %s: %w", canonicalGate, err)
}
userIDSet := make(map[string]struct{}, len(subscriptions))
@@ -170,12 +170,12 @@ func (r *GORMUserRepository) ListActiveUsersByNotificationType(ctx context.Conte
Where("id IN ?", userIDs).
Where("is_active = ? AND is_locked = ?", true, false).
Find(&records).Error; err != nil {
- return nil, fmt.Errorf("failed to fetch subscribed users for type %s: %w", canonicalType, err)
+ return nil, fmt.Errorf("failed to fetch subscribed users for gate %s: %w", canonicalGate, err)
}
identitiesByUserID, err := r.loadProviderIdentitiesByUserID(ctx, userIDs)
if err != nil {
- return nil, fmt.Errorf("failed to fetch notification identities for type %s: %w", canonicalType, err)
+ return nil, fmt.Errorf("failed to fetch notification identities for gate %s: %w", canonicalGate, err)
}
subscriptionsByUserID := make(map[string][]UserSubscription, len(subscriptions))
@@ -214,19 +214,19 @@ func (r *GORMUserRepository) ListActiveUsersByNotificationType(ctx context.Conte
return users, nil
}
-// ListActiveUserIDsByNotificationType returns IDs for active, unlocked users
-// subscribed to the given notification type on at least one valid channel.
-func (r *GORMUserRepository) ListActiveUserIDsByNotificationType(ctx context.Context, notificationType string) ([]string, error) {
- canonicalType, ok := NormalizeNotificationType(notificationType)
+// ListActiveUserIDsBySubscriptionGate returns IDs for active, unlocked users
+// subscribed to the given subscription gate on at least one valid channel.
+func (r *GORMUserRepository) ListActiveUserIDsBySubscriptionGate(ctx context.Context, subscriptionGate string) ([]string, error) {
+ canonicalGate, ok := NormalizeSubscriptionGate(subscriptionGate)
if !ok {
return []string{}, nil
}
var subscriptions []relational.UserNotificationSubscription
if err := r.db.WithContext(ctx).
- Where("notification_type = ?", canonicalType).
+ Where("notification_type = ?", canonicalGate).
Find(&subscriptions).Error; err != nil {
- return nil, fmt.Errorf("failed to fetch notification subscriptions for type %s: %w", canonicalType, err)
+ return nil, fmt.Errorf("failed to fetch notification subscriptions for gate %s: %w", canonicalGate, err)
}
userIDSet := make(map[string]struct{}, len(subscriptions))
@@ -260,7 +260,7 @@ func (r *GORMUserRepository) ListActiveUserIDsByNotificationType(ctx context.Con
Where("id IN ?", userIDs).
Where("is_active = ? AND is_locked = ?", true, false).
Pluck("id", &activeUserIDs).Error; err != nil {
- return nil, fmt.Errorf("failed to fetch subscribed users for type %s: %w", canonicalType, err)
+ return nil, fmt.Errorf("failed to fetch subscribed users for gate %s: %w", canonicalGate, err)
}
sort.Strings(activeUserIDs)
@@ -420,11 +420,11 @@ func (r *Resolver) ListSubscribedUsers(ctx context.Context, definition Definitio
if err != nil {
return nil, err
}
- if normalizedDefinition.SubscriptionType == "" {
+ if normalizedDefinition.SubscriptionGate == "" {
return r.listActiveUsers(ctx)
}
- return r.users.ListActiveUsersByNotificationType(ctx, normalizedDefinition.SubscriptionType)
+ return r.users.ListActiveUsersBySubscriptionGate(ctx, normalizedDefinition.SubscriptionGate)
}
func (r *Resolver) listActiveUsers(ctx context.Context) ([]User, error) {
@@ -474,12 +474,12 @@ func (r *Resolver) ResolveUser(user User, options DispatchOptions, definition De
func (r *Resolver) resolveUser(user User, options DispatchOptions, definition Definition) ([]Target, error) {
channels := append([]string(nil), definition.SupportedChannels...)
- if definition.SubscriptionType != NotificationTypeUngated {
- if definition.SubscriptionType == "" {
- return nil, ErrMissingSubscriptionType
+ if definition.SubscriptionGate != SubscriptionGateUngated {
+ if definition.SubscriptionGate == "" {
+ return nil, ErrMissingSubscriptionGate
}
- channels = user.NotificationChannels(definition.SubscriptionType)
+ channels = user.NotificationChannels(definition.SubscriptionGate)
}
channels = applyRequestedChannelFilter(channels, options.RequestedChannel)
if len(channels) == 0 {
diff --git a/internal/service/notification/resolver_test.go b/internal/service/notification/resolver_test.go
index 0b490e1d..5eb082a3 100644
--- a/internal/service/notification/resolver_test.go
+++ b/internal/service/notification/resolver_test.go
@@ -21,12 +21,12 @@ func (r stubUserRepository) FindUserByID(_ context.Context, userID string) (User
return r.users[userID], nil
}
-func (r stubUserRepository) ListActiveUsersByNotificationType(_ context.Context, notificationType string) ([]User, error) {
+func (r stubUserRepository) ListActiveUsersBySubscriptionGate(_ context.Context, notificationType string) ([]User, error) {
if r.err != nil {
return nil, r.err
}
- canonicalType, ok := NormalizeNotificationType(notificationType)
+ canonicalType, ok := NormalizeSubscriptionGate(notificationType)
if !ok {
return []User{}, nil
}
@@ -86,7 +86,7 @@ func TestResolverResolveUserAudienceUsesSubscriptionsAndSkipsMissingSlackLink(t
},
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeRiskNotifications,
+ NotificationType: SubscriptionGateRiskNotifications,
Channels: []string{DeliveryChannelEmail, DeliveryChannelSlack},
},
},
@@ -101,7 +101,7 @@ func TestResolverResolveUserAudienceUsesSubscriptionsAndSkipsMissingSlackLink(t
},
}, Definition{
Kind: Kind("risk_review_due"),
- SubscriptionType: NotificationTypeRiskNotifications,
+ SubscriptionGate: SubscriptionGateRiskNotifications,
SupportedChannels: []string{DeliveryChannelEmail, DeliveryChannelSlack},
Renderers: map[string]ChannelRenderer{
DeliveryChannelEmail: ProviderRenderer(DeliveryChannelEmail, func(context.Context, any) (any, error) {
@@ -153,7 +153,7 @@ func TestResolverResolveUserAudienceSupportsUngatedDefinitions(t *testing.T) {
},
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeTaskAvailable,
+ NotificationType: SubscriptionGateTaskAvailable,
Channels: []string{DeliveryChannelSlack},
},
},
@@ -168,7 +168,7 @@ func TestResolverResolveUserAudienceSupportsUngatedDefinitions(t *testing.T) {
},
}, Definition{
Kind: Kind("workflow_execution_failed"),
- SubscriptionType: NotificationTypeUngated,
+ SubscriptionGate: SubscriptionGateUngated,
SupportedChannels: []string{DeliveryChannelEmail, DeliveryChannelSlack},
Renderers: map[string]ChannelRenderer{
DeliveryChannelEmail: ProviderRenderer(DeliveryChannelEmail, func(context.Context, any) (any, error) {
@@ -258,7 +258,7 @@ func TestResolverListSubscribedUsers(t *testing.T) {
},
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Channels: []string{DeliveryChannelEmail},
},
},
@@ -268,7 +268,7 @@ func TestResolverListSubscribedUsers(t *testing.T) {
Email: "bob@example.com",
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeRiskNotifications,
+ NotificationType: SubscriptionGateRiskNotifications,
Channels: []string{DeliveryChannelEmail},
},
},
@@ -278,7 +278,7 @@ func TestResolverListSubscribedUsers(t *testing.T) {
users, err := resolver.ListSubscribedUsers(context.Background(), Definition{
Kind: Kind("evidence_digest"),
- SubscriptionType: NotificationTypeEvidenceDigest,
+ SubscriptionGate: SubscriptionGateEvidenceDigest,
SupportedChannels: []string{DeliveryChannelEmail},
Renderers: map[string]ChannelRenderer{
DeliveryChannelEmail: ProviderRenderer(DeliveryChannelEmail, func(context.Context, any) (any, error) {
@@ -303,7 +303,7 @@ func TestResolverListSubscribedUsersSupportsUngatedDefinitions(t *testing.T) {
Email: "bob@example.com",
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeRiskNotifications,
+ NotificationType: SubscriptionGateRiskNotifications,
Channels: []string{DeliveryChannelEmail},
},
},
@@ -313,7 +313,7 @@ func TestResolverListSubscribedUsersSupportsUngatedDefinitions(t *testing.T) {
users, err := resolver.ListSubscribedUsers(context.Background(), Definition{
Kind: Kind("workflow_execution_failed"),
- SubscriptionType: NotificationTypeUngated,
+ SubscriptionGate: SubscriptionGateUngated,
SupportedChannels: []string{DeliveryChannelEmail},
Renderers: map[string]ChannelRenderer{
DeliveryChannelEmail: ProviderRenderer(DeliveryChannelEmail, func(context.Context, any) (any, error) {
diff --git a/internal/service/notification/service_test.go b/internal/service/notification/service_test.go
index e001b40a..e32758de 100644
--- a/internal/service/notification/service_test.go
+++ b/internal/service/notification/service_test.go
@@ -30,7 +30,7 @@ func (t *stubTransport) byChannel(channel string) []Delivery {
func TestServiceDispatchEnqueuesProviderReadyDeliveries(t *testing.T) {
registry := MustNewRegistry(Definition{
Kind: Kind("risk_review_due"),
- SubscriptionType: NotificationTypeRiskNotifications,
+ SubscriptionGate: SubscriptionGateRiskNotifications,
SupportedChannels: []string{DeliveryChannelEmail, DeliveryChannelSlack},
Renderers: map[string]ChannelRenderer{
DeliveryChannelEmail: ProviderRenderer(DeliveryChannelEmail, func(context.Context, any) (any, error) {
@@ -53,7 +53,7 @@ func TestServiceDispatchEnqueuesProviderReadyDeliveries(t *testing.T) {
},
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeRiskNotifications,
+ NotificationType: SubscriptionGateRiskNotifications,
Channels: []string{DeliveryChannelEmail, DeliveryChannelSlack},
},
},
@@ -106,7 +106,7 @@ func TestServiceDispatchEnqueuesProviderReadyDeliveries(t *testing.T) {
func TestServiceDispatchNoTargetsBecomesNoop(t *testing.T) {
registry := MustNewRegistry(Definition{
Kind: Kind("risk_review_due"),
- SubscriptionType: NotificationTypeRiskNotifications,
+ SubscriptionGate: SubscriptionGateRiskNotifications,
SupportedChannels: []string{DeliveryChannelSlack},
Renderers: map[string]ChannelRenderer{
DeliveryChannelSlack: ProviderRenderer(DeliveryChannelSlack, func(context.Context, any) (any, error) {
@@ -122,7 +122,7 @@ func TestServiceDispatchNoTargetsBecomesNoop(t *testing.T) {
Email: "user@example.com",
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeRiskNotifications,
+ NotificationType: SubscriptionGateRiskNotifications,
Channels: []string{DeliveryChannelSlack},
},
},
@@ -161,7 +161,7 @@ func TestServiceDispatchReturnsDefinitionErrorForUnknownKind(t *testing.T) {
func TestServiceDispatchFanoutSubscribedUsersBuildsPerUserDeliveries(t *testing.T) {
registry := MustNewRegistry(Definition{
Kind: Kind("evidence_digest"),
- SubscriptionType: NotificationTypeEvidenceDigest,
+ SubscriptionGate: SubscriptionGateEvidenceDigest,
SupportedChannels: []string{DeliveryChannelEmail, DeliveryChannelSlack},
Renderers: map[string]ChannelRenderer{
DeliveryChannelEmail: ProviderRenderer(DeliveryChannelEmail, func(_ context.Context, model any) (any, error) {
@@ -187,7 +187,7 @@ func TestServiceDispatchFanoutSubscribedUsersBuildsPerUserDeliveries(t *testing.
},
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Channels: []string{DeliveryChannelEmail, DeliveryChannelSlack},
},
},
@@ -201,7 +201,7 @@ func TestServiceDispatchFanoutSubscribedUsersBuildsPerUserDeliveries(t *testing.
},
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Channels: []string{DeliveryChannelEmail},
},
},
@@ -251,7 +251,7 @@ func TestServiceDispatchFanoutSubscribedUsersBuildsPerUserDeliveries(t *testing.
func TestServiceDispatchFanoutSupportsSharedAndSubscribedRequests(t *testing.T) {
registry := MustNewRegistry(Definition{
Kind: Kind("evidence_digest"),
- SubscriptionType: NotificationTypeEvidenceDigest,
+ SubscriptionGate: SubscriptionGateEvidenceDigest,
SupportedChannels: []string{DeliveryChannelEmail, DeliveryChannelSlack},
Renderers: map[string]ChannelRenderer{
DeliveryChannelEmail: ProviderRenderer(DeliveryChannelEmail, func(_ context.Context, model any) (any, error) {
@@ -274,7 +274,7 @@ func TestServiceDispatchFanoutSupportsSharedAndSubscribedRequests(t *testing.T)
},
Subscriptions: []UserSubscription{
{
- NotificationType: NotificationTypeEvidenceDigest,
+ NotificationType: SubscriptionGateEvidenceDigest,
Channels: []string{DeliveryChannelEmail},
},
},
diff --git a/internal/service/notification/system_destination_repository.go b/internal/service/notification/system_destination_repository.go
index 9dd25713..bd992b99 100644
--- a/internal/service/notification/system_destination_repository.go
+++ b/internal/service/notification/system_destination_repository.go
@@ -9,7 +9,8 @@ import (
)
type SystemDestinationRepository interface {
- ListTargetsByNotificationType(ctx context.Context, notificationType string) ([]Target, error)
+ ListTargetsBySubscriptionGate(ctx context.Context, subscriptionGate string) ([]Target, error)
+ ListTargetsBySystemNotificationName(ctx context.Context, notificationName string) ([]Target, error)
}
type GORMSystemDestinationRepository struct {
@@ -24,21 +25,38 @@ func NewGORMSystemDestinationRepository(db *gorm.DB, providers ProviderLookup) *
}
}
-func (r *GORMSystemDestinationRepository) ListTargetsByNotificationType(ctx context.Context, notificationType string) ([]Target, error) {
- if r == nil || r.db == nil || r.providers == nil {
+func (r *GORMSystemDestinationRepository) ListTargetsBySubscriptionGate(ctx context.Context, subscriptionGate string) ([]Target, error) {
+ if r.db == nil || r.providers == nil {
return nil, fmt.Errorf("system notification destination repository is not configured")
}
- canonicalType, ok := NormalizeNotificationType(notificationType)
+ canonicalGate, ok := NormalizeSubscriptionGate(subscriptionGate)
if !ok {
return []Target{}, nil
}
+ return r.listTargetsBySystemNotificationName(ctx, canonicalGate)
+}
+
+func (r *GORMSystemDestinationRepository) ListTargetsBySystemNotificationName(ctx context.Context, notificationName string) ([]Target, error) {
+ if r.db == nil || r.providers == nil {
+ return nil, fmt.Errorf("system notification destination repository is not configured")
+ }
+
+ canonicalName, ok := NormalizeSystemNotificationName(notificationName)
+ if !ok {
+ return []Target{}, nil
+ }
+
+ return r.listTargetsBySystemNotificationName(ctx, canonicalName)
+}
+
+func (r *GORMSystemDestinationRepository) listTargetsBySystemNotificationName(ctx context.Context, notificationName string) ([]Target, error) {
var records []relational.SystemNotificationDestination
if err := r.db.WithContext(ctx).
- Where("notification_type = ?", canonicalType).
+ Where("notification_type = ?", notificationName).
Find(&records).Error; err != nil {
- return nil, fmt.Errorf("failed to fetch system notification destinations for type %s: %w", canonicalType, err)
+ return nil, fmt.Errorf("failed to fetch system notification destinations for %s: %w", notificationName, err)
}
targets := make([]Target, 0, len(records))
@@ -75,18 +93,18 @@ func (r *GORMSystemDestinationRepository) ListTargetsByNotificationType(ctx cont
})
if err != nil {
return nil, fmt.Errorf(
- "invalid system notification destination %s for type %s provider %s: %w",
+ "invalid system notification destination %s for %s provider %s: %w",
recordID,
- canonicalType,
+ notificationName,
provider,
err,
)
}
if err := target.Validate(); err != nil {
return nil, fmt.Errorf(
- "invalid system notification destination %s for type %s provider %s: %w",
+ "invalid system notification destination %s for %s provider %s: %w",
recordID,
- canonicalType,
+ notificationName,
provider,
err,
)
diff --git a/internal/service/notification/system_destination_repository_test.go b/internal/service/notification/system_destination_repository_test.go
index be1efc31..dfc83f96 100644
--- a/internal/service/notification/system_destination_repository_test.go
+++ b/internal/service/notification/system_destination_repository_test.go
@@ -14,11 +14,11 @@ import (
"gorm.io/gorm"
)
-func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeExpandsTargets(t *testing.T) {
+func TestGORMSystemDestinationRepositoryListTargetsBySubscriptionGateExpandsTargets(t *testing.T) {
db := newNotificationSystemDestinationTestDB(t)
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -29,7 +29,7 @@ func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeExpandsTarg
}).Error)
repo := notification.NewGORMSystemDestinationRepository(db, notificationproviders.NewLookup())
- targets, err := repo.ListTargetsByNotificationType(context.Background(), notification.NotificationTypeEvidenceDigest)
+ targets, err := repo.ListTargetsBySubscriptionGate(context.Background(), notification.SubscriptionGateEvidenceDigest)
require.NoError(t, err)
require.Len(t, targets, 1)
@@ -38,11 +38,33 @@ func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeExpandsTarg
assert.Equal(t, "channel", targets[0].Address["target_type"])
}
-func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeExpandsMultipleRowsForSameProvider(t *testing.T) {
+func TestGORMSystemDestinationRepositoryListTargetsBySystemNotificationNameExpandsWorkflowFailureTargets(t *testing.T) {
db := newNotificationSystemDestinationTestDB(t)
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SystemNotificationNameWorkflowExecutionFailed,
+ Provider: notification.DeliveryChannelEmail,
+ Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
+ Address: map[string]string{
+ "email": " alerts@example.com ",
+ },
+ }),
+ }).Error)
+
+ repo := notification.NewGORMSystemDestinationRepository(db, notificationproviders.NewLookup())
+ targets, err := repo.ListTargetsBySystemNotificationName(context.Background(), " workflowExecutionFailed ")
+ require.NoError(t, err)
+ require.Len(t, targets, 1)
+
+ assert.Equal(t, notification.DeliveryChannelEmail, targets[0].Provider)
+ assert.Equal(t, "alerts@example.com", targets[0].Address["email"])
+}
+
+func TestGORMSystemDestinationRepositoryListTargetsBySubscriptionGateExpandsMultipleRowsForSameProvider(t *testing.T) {
+ db := newNotificationSystemDestinationTestDB(t)
+
+ require.NoError(t, db.Create(&relational.SystemNotificationDestination{
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -52,7 +74,7 @@ func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeExpandsMult
}),
}).Error)
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -63,7 +85,7 @@ func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeExpandsMult
}).Error)
repo := notification.NewGORMSystemDestinationRepository(db, notificationproviders.NewLookup())
- targets, err := repo.ListTargetsByNotificationType(context.Background(), notification.NotificationTypeEvidenceDigest)
+ targets, err := repo.ListTargetsBySubscriptionGate(context.Background(), notification.SubscriptionGateEvidenceDigest)
require.NoError(t, err)
require.Len(t, targets, 2)
@@ -71,11 +93,11 @@ func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeExpandsMult
assert.ElementsMatch(t, []string{"C-PRIMARY", "C-SECONDARY"}, channels)
}
-func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeDeduplicatesTargets(t *testing.T) {
+func TestGORMSystemDestinationRepositoryListTargetsBySubscriptionGateDeduplicatesTargets(t *testing.T) {
db := newNotificationSystemDestinationTestDB(t)
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -85,7 +107,7 @@ func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeDeduplicate
}),
}).Error)
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -96,17 +118,17 @@ func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeDeduplicate
}).Error)
repo := notification.NewGORMSystemDestinationRepository(db, notificationproviders.NewLookup())
- targets, err := repo.ListTargetsByNotificationType(context.Background(), notification.NotificationTypeEvidenceDigest)
+ targets, err := repo.ListTargetsBySubscriptionGate(context.Background(), notification.SubscriptionGateEvidenceDigest)
require.NoError(t, err)
require.Len(t, targets, 1)
assert.Equal(t, "C-DIGEST", targets[0].Address["channel"])
}
-func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeRejectsInvalidTargets(t *testing.T) {
+func TestGORMSystemDestinationRepositoryListTargetsBySubscriptionGateRejectsInvalidTargets(t *testing.T) {
db := newNotificationSystemDestinationTestDB(t)
require.NoError(t, db.Create(&relational.SystemNotificationDestination{
- NotificationType: notification.NotificationTypeEvidenceDigest,
+ NotificationType: notification.SubscriptionGateEvidenceDigest,
Provider: notification.DeliveryChannelSlack,
Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
Address: map[string]string{
@@ -116,7 +138,7 @@ func TestGORMSystemDestinationRepositoryListTargetsByNotificationTypeRejectsInva
}).Error)
repo := notification.NewGORMSystemDestinationRepository(db, notificationproviders.NewLookup())
- _, err := repo.ListTargetsByNotificationType(context.Background(), notification.NotificationTypeEvidenceDigest)
+ _, err := repo.ListTargetsBySubscriptionGate(context.Background(), notification.SubscriptionGateEvidenceDigest)
require.Error(t, err)
assert.ErrorContains(t, err, "invalid system notification destination")
}
diff --git a/internal/service/notification/types.go b/internal/service/notification/types.go
index a3619e4b..9102f62f 100644
--- a/internal/service/notification/types.go
+++ b/internal/service/notification/types.go
@@ -20,7 +20,7 @@ var (
ErrRegistryNotConfigured = errors.New("notification registry is not configured")
ErrTransportNotConfigured = errors.New("notification transport is not configured")
ErrConfiguredDestinationNotFound = errors.New("configured notification destination not found")
- ErrMissingSubscriptionType = errors.New("notification definition requires a subscription type for user audiences")
+ ErrMissingSubscriptionGate = errors.New("notification definition requires a subscription gate for user audiences")
ErrUngatedUserListingNotSupported = errors.New("notification user repository does not support ungated audience listing")
)
diff --git a/internal/service/slack/formatters/workflow_execution_failed_test.go b/internal/service/slack/formatters/workflow_execution_failed_test.go
index 6865ceae..cdc533bf 100644
--- a/internal/service/slack/formatters/workflow_execution_failed_test.go
+++ b/internal/service/slack/formatters/workflow_execution_failed_test.go
@@ -43,3 +43,41 @@ func TestFormatWorkflowExecutionFailedMessage_DefaultsMissingFields(t *testing.T
assert.Contains(t, msg.Text, "Workflow execution failed: Workflow")
require.NotEmpty(t, msg.Blocks)
}
+
+func TestFormatWorkflowExecutionFailedMessage_GenericAudienceOmitsMyTasksLink(t *testing.T) {
+ msg, err := FormatWorkflowExecutionFailedMessage(
+ "",
+ "Annual Audit",
+ "Audit 2026",
+ "exec-123",
+ "step execution failed",
+ "17 Apr 2026",
+ 1,
+ 3,
+ 4,
+ "https://app.example.com/workflow-executions/exec-123",
+ "",
+ )
+ require.NoError(t, err)
+ assert.Contains(t, msg.Text, "Workflow execution failed")
+ require.NotEmpty(t, msg.Blocks)
+
+ blockText := workflowExecutionFailedSectionText(msg.Blocks)
+ assert.Contains(t, blockText, "A workflow execution has failed and may require your attention.")
+ assert.Contains(t, blockText, "View Workflow Instance")
+ assert.NotContains(t, blockText, "Hi ")
+ assert.NotContains(t, blockText, "View My Tasks")
+}
+
+func workflowExecutionFailedSectionText(blocks []slack.Block) string {
+ parts := []string{}
+ for _, block := range blocks {
+ section, ok := block.(*slack.SectionBlock)
+ if !ok || section.Text == nil {
+ continue
+ }
+ parts = append(parts, section.Text.Text)
+ }
+
+ return strings.Join(parts, "\n")
+}
diff --git a/internal/service/worker/jobs.go b/internal/service/worker/jobs.go
index 095bf692..dcb2f9a4 100644
--- a/internal/service/worker/jobs.go
+++ b/internal/service/worker/jobs.go
@@ -168,8 +168,8 @@ func (u NotificationUser) FullName() string {
return u.FirstName + " " + u.LastName
}
-func (u NotificationUser) NotificationChannels(notificationType string) []string {
- normalizedType, ok := notification.NormalizeNotificationType(notificationType)
+func (u NotificationUser) NotificationChannels(subscriptionGate string) []string {
+ normalizedGate, ok := notification.NormalizeSubscriptionGate(subscriptionGate)
if !ok {
return nil
}
@@ -177,8 +177,8 @@ func (u NotificationUser) NotificationChannels(notificationType string) []string
seen := map[string]struct{}{}
channels := make([]string, 0)
for _, subscription := range u.NotificationSubscriptions {
- currentType, currentTypeOK := notification.NormalizeNotificationType(subscription.NotificationType)
- if !currentTypeOK || currentType != normalizedType {
+ currentGate, currentGateOK := notification.NormalizeSubscriptionGate(subscription.NotificationType)
+ if !currentGateOK || currentGate != normalizedGate {
continue
}
diff --git a/internal/service/worker/jobs_notification_channels_test.go b/internal/service/worker/jobs_notification_channels_test.go
index 17c02126..be8a9ff5 100644
--- a/internal/service/worker/jobs_notification_channels_test.go
+++ b/internal/service/worker/jobs_notification_channels_test.go
@@ -15,7 +15,7 @@ func TestNotificationUserNotificationChannels_NormalizesAndDeduplicates(t *testi
Channels: []string{" Email ", "slack", "EMAIL", "pagerduty"},
},
{
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: []string{"SLACK", "email"},
},
{
@@ -25,7 +25,7 @@ func TestNotificationUserNotificationChannels_NormalizesAndDeduplicates(t *testi
},
}
- channels := user.NotificationChannels(notification.NotificationTypeTaskAvailable)
+ channels := user.NotificationChannels(notification.SubscriptionGateTaskAvailable)
assert.Equal(t, []string{notification.DeliveryChannelEmail, notification.DeliveryChannelSlack}, channels)
}
@@ -33,7 +33,7 @@ func TestNotificationUserNotificationChannels_InvalidRequestedType(t *testing.T)
user := NotificationUser{
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: []string{notification.DeliveryChannelEmail},
},
},
@@ -46,13 +46,13 @@ func TestSelectUserNotificationChannels_ReturnsRequestedChannelOnly(t *testing.T
user := NotificationUser{
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: []string{notification.DeliveryChannelEmail, notification.DeliveryChannelSlack},
},
},
}
- channels, ok := selectUserNotificationChannels(user, notification.NotificationTypeTaskAvailable, notification.DeliveryChannelSlack)
+ channels, ok := selectUserNotificationChannels(user, notification.SubscriptionGateTaskAvailable, notification.DeliveryChannelSlack)
assert.True(t, ok)
assert.Equal(t, []string{notification.DeliveryChannelSlack}, channels)
}
@@ -61,13 +61,13 @@ func TestSelectUserNotificationChannels_EmptyRequestedChannelReturnsAllSubscribe
user := NotificationUser{
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: []string{notification.DeliveryChannelEmail, notification.DeliveryChannelSlack},
},
},
}
- channels, ok := selectUserNotificationChannels(user, notification.NotificationTypeTaskAvailable, "")
+ channels, ok := selectUserNotificationChannels(user, notification.SubscriptionGateTaskAvailable, "")
assert.True(t, ok)
assert.Equal(t, []string{notification.DeliveryChannelEmail, notification.DeliveryChannelSlack}, channels)
}
@@ -76,13 +76,13 @@ func TestSelectUserNotificationChannels_UnsubscribedRequestedChannelSkips(t *tes
user := NotificationUser{
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: []string{notification.DeliveryChannelEmail},
},
},
}
- channels, ok := selectUserNotificationChannels(user, notification.NotificationTypeTaskAvailable, notification.DeliveryChannelSlack)
+ channels, ok := selectUserNotificationChannels(user, notification.SubscriptionGateTaskAvailable, notification.DeliveryChannelSlack)
assert.True(t, ok)
assert.Empty(t, channels)
}
@@ -91,13 +91,13 @@ func TestSelectUserNotificationChannels_InvalidRequestedChannelReturnsFalse(t *t
user := NotificationUser{
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: []string{notification.DeliveryChannelEmail},
},
},
}
- channels, ok := selectUserNotificationChannels(user, notification.NotificationTypeTaskAvailable, "pagerduty")
+ channels, ok := selectUserNotificationChannels(user, notification.SubscriptionGateTaskAvailable, "pagerduty")
assert.False(t, ok)
assert.Nil(t, channels)
}
diff --git a/internal/service/worker/notification_definition_helpers.go b/internal/service/worker/notification_definition_helpers.go
index 730abe83..94d720b4 100644
--- a/internal/service/worker/notification_definition_helpers.go
+++ b/internal/service/worker/notification_definition_helpers.go
@@ -43,7 +43,7 @@ func decodeNotificationModel[T any](model any, modelName string) (T, error) {
func newTypedNotificationDefinition[T any](
kind notification.Kind,
- subscriptionType string,
+ subscriptionGate string,
decode notificationModelDecoder[T],
emailRender emailTemplateNotificationRenderer[T],
slackRender slackMessageNotificationRenderer[T],
@@ -70,16 +70,16 @@ func newTypedNotificationDefinition[T any](
}))
}
- return notification.NewDefinition(kind, subscriptionType, bindings...)
+ return notification.NewDefinition(kind, subscriptionGate, bindings...)
}
func newTypedEmailOnlyNotificationDefinition[T any](
kind notification.Kind,
- subscriptionType string,
+ subscriptionGate string,
decode notificationModelDecoder[T],
emailRender emailTemplateNotificationRenderer[T],
) notification.Definition {
- return newTypedNotificationDefinition(kind, subscriptionType, decode, emailRender, nil)
+ return newTypedNotificationDefinition(kind, subscriptionGate, decode, emailRender, nil)
}
func newNotificationRequest(
diff --git a/internal/service/worker/poam_deadline_reminder_worker_test.go b/internal/service/worker/poam_deadline_reminder_worker_test.go
index 59632ba2..be6fcfa0 100644
--- a/internal/service/worker/poam_deadline_reminder_worker_test.go
+++ b/internal/service/worker/poam_deadline_reminder_worker_test.go
@@ -28,7 +28,7 @@ func TestPoamDeadlineReminderWorker_SlackSubscribedUser_SendsSlack(t *testing.T)
LastName: "Person",
SlackUserID: "USLACKPOAM1",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeRiskNotifications, Channels: []string{"slack"}},
+ {NotificationType: notification.SubscriptionGateRiskNotifications, Channels: []string{"slack"}},
},
}
mockRepo.On("FindUserByID", ctx, recipientUserID).Return(user, nil)
@@ -78,7 +78,7 @@ func TestPoamDeadlineReminderWorker_EmailAndSlackSubscribedUser_SendsBoth(t *tes
LastName: "Person",
SlackUserID: "USLACKPOAM2",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeRiskNotifications, Channels: []string{"email", "slack"}},
+ {NotificationType: notification.SubscriptionGateRiskNotifications, Channels: []string{"email", "slack"}},
},
}
mockRepo.On("FindUserByID", ctx, recipientUserID).Return(user, nil)
diff --git a/internal/service/worker/poam_milestone_overdue_worker_test.go b/internal/service/worker/poam_milestone_overdue_worker_test.go
index 22aabc7e..77793f3b 100644
--- a/internal/service/worker/poam_milestone_overdue_worker_test.go
+++ b/internal/service/worker/poam_milestone_overdue_worker_test.go
@@ -28,7 +28,7 @@ func TestMilestoneOverdueReminderWorker_SlackSubscribedUser_SendsSlack(t *testin
LastName: "Person",
SlackUserID: "USLACKPOAM5",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeRiskNotifications, Channels: []string{"slack"}},
+ {NotificationType: notification.SubscriptionGateRiskNotifications, Channels: []string{"slack"}},
},
}
mockRepo.On("FindUserByID", ctx, recipientUserID).Return(user, nil)
@@ -78,7 +78,7 @@ func TestMilestoneOverdueReminderWorker_EmailAndSlackSubscribedUser_SendsBoth(t
LastName: "Person",
SlackUserID: "USLACKPOAM6",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeRiskNotifications, Channels: []string{"email", "slack"}},
+ {NotificationType: notification.SubscriptionGateRiskNotifications, Channels: []string{"email", "slack"}},
},
}
mockRepo.On("FindUserByID", ctx, recipientUserID).Return(user, nil)
diff --git a/internal/service/worker/poam_notifications.go b/internal/service/worker/poam_notifications.go
index dc94ff0b..f1d19b61 100644
--- a/internal/service/worker/poam_notifications.go
+++ b/internal/service/worker/poam_notifications.go
@@ -74,28 +74,28 @@ func NewPoamNotificationServiceFactory(
definitions: []notification.Definition{
newTypedNotificationDefinition(
poamDeadlineReminderNotificationKind,
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
newNotificationModelDecoder[poamDeadlineReminderNotificationModel]("poam deadline reminder model"),
renderPoamDeadlineReminderEmail,
renderPoamDeadlineReminderSlack,
),
newTypedNotificationDefinition(
poamMilestoneOverdueNotificationKind,
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
newNotificationModelDecoder[poamMilestoneOverdueNotificationModel]("poam milestone overdue notification model"),
renderPoamMilestoneOverdueNotificationEmail,
renderPoamMilestoneOverdueNotificationSlack,
),
newTypedNotificationDefinition(
poamOverdueNotificationKind,
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
newNotificationModelDecoder[poamOverdueNotificationModel]("poam overdue notification model"),
renderPoamOverdueNotificationEmail,
renderPoamOverdueNotificationSlack,
),
newTypedEmailOnlyNotificationDefinition(
poamOpenDigestNotificationKind,
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
newNotificationModelDecoder[poamOpenDigestNotificationModel]("poam open digest model"),
renderPoamOpenDigestEmail,
),
diff --git a/internal/service/worker/poam_overdue_notification_worker_test.go b/internal/service/worker/poam_overdue_notification_worker_test.go
index fb85a81c..f46a7278 100644
--- a/internal/service/worker/poam_overdue_notification_worker_test.go
+++ b/internal/service/worker/poam_overdue_notification_worker_test.go
@@ -28,7 +28,7 @@ func TestPoamOverdueNotificationWorker_SlackSubscribedUser_SendsSlack(t *testing
LastName: "Person",
SlackUserID: "USLACKPOAM3",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeRiskNotifications, Channels: []string{"slack"}},
+ {NotificationType: notification.SubscriptionGateRiskNotifications, Channels: []string{"slack"}},
},
}
mockRepo.On("FindUserByID", ctx, recipientUserID).Return(user, nil)
@@ -76,7 +76,7 @@ func TestPoamOverdueNotificationWorker_EmailAndSlackSubscribedUser_SendsBoth(t *
LastName: "Person",
SlackUserID: "USLACKPOAM4",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeRiskNotifications, Channels: []string{"email", "slack"}},
+ {NotificationType: notification.SubscriptionGateRiskNotifications, Channels: []string{"email", "slack"}},
},
}
mockRepo.On("FindUserByID", ctx, recipientUserID).Return(user, nil)
diff --git a/internal/service/worker/poam_workers_integration_test.go b/internal/service/worker/poam_workers_integration_test.go
index e2c4809c..c3b927f9 100644
--- a/internal/service/worker/poam_workers_integration_test.go
+++ b/internal/service/worker/poam_workers_integration_test.go
@@ -72,7 +72,7 @@ func (suite *PoamWorkersIntegrationSuite) seedUser(email string) uuid.UUID {
}).Error)
suite.Require().NoError(suite.DB.Create(&relational.UserNotificationSubscription{
UserID: id.String(),
- NotificationType: notification.NotificationTypeRiskNotifications,
+ NotificationType: notification.SubscriptionGateRiskNotifications,
Channels: []string{notification.DeliveryChannelEmail},
}).Error)
return id
diff --git a/internal/service/worker/risk_digest_worker_integration_test.go b/internal/service/worker/risk_digest_worker_integration_test.go
index cae34f9a..72ce33af 100644
--- a/internal/service/worker/risk_digest_worker_integration_test.go
+++ b/internal/service/worker/risk_digest_worker_integration_test.go
@@ -54,7 +54,7 @@ func (suite *RiskOpenDigestIntegrationSuite) TestRiskOpenDigestSchedulerAndWorke
}).Error)
suite.Require().NoError(suite.DB.Create(&relational.UserNotificationSubscription{
UserID: recipientID.String(),
- NotificationType: notification.NotificationTypeRiskNotifications,
+ NotificationType: notification.SubscriptionGateRiskNotifications,
Channels: []string{notification.DeliveryChannelEmail},
}).Error)
diff --git a/internal/service/worker/risk_digest_worker_test.go b/internal/service/worker/risk_digest_worker_test.go
index c16fea82..40a7dcf3 100644
--- a/internal/service/worker/risk_digest_worker_test.go
+++ b/internal/service/worker/risk_digest_worker_test.go
@@ -282,7 +282,7 @@ func TestRiskOpenDigestWorker_SendsGroupedDigest(t *testing.T) {
LastName: "Owner",
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeRiskNotifications,
+ NotificationType: notification.SubscriptionGateRiskNotifications,
Channels: []string{notification.DeliveryChannelEmail},
},
},
@@ -404,7 +404,7 @@ func TestRiskOpenDigestWorker_SlackSubscribed_SendsSlack(t *testing.T) {
SlackUserID: "URISKDIGEST",
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeRiskNotifications,
+ NotificationType: notification.SubscriptionGateRiskNotifications,
Channels: []string{notification.DeliveryChannelSlack},
},
},
diff --git a/internal/service/worker/risk_notifications.go b/internal/service/worker/risk_notifications.go
index 676d38ac..241b37f6 100644
--- a/internal/service/worker/risk_notifications.go
+++ b/internal/service/worker/risk_notifications.go
@@ -58,28 +58,28 @@ func NewRiskNotificationServiceFactory(
definitions: []notification.Definition{
newTypedNotificationDefinition(
riskReviewDueReminderNotificationKind,
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
newNotificationModelDecoder[riskReminderNotificationModel]("risk reminder model"),
renderRiskReviewDueReminderEmail,
renderRiskReviewDueReminderSlack,
),
newTypedNotificationDefinition(
riskReviewOverdueEscalationNotificationKind,
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
newNotificationModelDecoder[riskReminderNotificationModel]("risk reminder model"),
renderRiskReviewOverdueEscalationEmail,
renderRiskReviewOverdueEscalationSlack,
),
newTypedNotificationDefinition(
riskStaleOpenReminderNotificationKind,
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
newNotificationModelDecoder[riskReminderNotificationModel]("risk reminder model"),
renderRiskStaleOpenReminderEmail,
renderRiskStaleOpenReminderSlack,
),
newTypedNotificationDefinition(
riskOpenDigestNotificationKind,
- notification.NotificationTypeRiskNotifications,
+ notification.SubscriptionGateRiskNotifications,
newNotificationModelDecoder[riskOpenDigestNotificationModel]("risk open digest model"),
renderRiskOpenDigestEmail,
renderRiskOpenDigestSlack,
diff --git a/internal/service/worker/risk_workers_test.go b/internal/service/worker/risk_workers_test.go
index 9bc13fbd..8f683d9b 100644
--- a/internal/service/worker/risk_workers_test.go
+++ b/internal/service/worker/risk_workers_test.go
@@ -141,7 +141,7 @@ func createTestUserRiskChannels(t *testing.T, db *gorm.DB, id uuid.UUID, channel
t.Helper()
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: id.String(),
- NotificationType: notification.NotificationTypeRiskNotifications,
+ NotificationType: notification.SubscriptionGateRiskNotifications,
Channels: channels,
}).Error)
if slackUserID == "" {
diff --git a/internal/service/worker/workflow_execution_failed_worker.go b/internal/service/worker/workflow_execution_failed_worker.go
index 20fdc68d..49cfcd3a 100644
--- a/internal/service/worker/workflow_execution_failed_worker.go
+++ b/internal/service/worker/workflow_execution_failed_worker.go
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/compliance-framework/api/internal/service/notification"
+ notificationproviders "github.com/compliance-framework/api/internal/service/notification/providers"
"github.com/compliance-framework/api/internal/service/relational/workflows"
"github.com/compliance-framework/api/internal/workflow"
"github.com/google/uuid"
@@ -116,13 +117,25 @@ func (w *WorkflowExecutionFailedWorker) Work(ctx context.Context, job *river.Job
FailedSteps: failedSteps,
CompletedSteps: completedSteps,
TotalSteps: totalSteps,
- WorkflowURL: w.webBaseURL + "/my-tasks",
+ WorkflowURL: w.webBaseURL + "/workflow-executions/" + execution.ID.String(),
MyTasksURL: w.webBaseURL + "/my-tasks",
}
+ requests := []notification.Request{
+ buildWorkflowExecutionFailedNotificationRequest(args, recipient.ID, model),
+ }
+
+ targets, err := w.configuredWorkflowExecutionFailedTargets(ctx)
+ if err != nil {
+ return fmt.Errorf("resolve workflow-execution-failed system destinations: %w", err)
+ }
+
+ if systemRequest, ok := buildWorkflowExecutionFailedSystemNotificationRequest(args, targets, model); ok {
+ requests = append(requests, systemRequest)
+ }
- if err := notifier.Dispatch(
+ if err := notifier.DispatchFanout(
ctx,
- buildWorkflowExecutionFailedNotificationRequest(args, recipient.ID, model),
+ notification.FanoutRequest{Requests: requests},
); err != nil {
return fmt.Errorf("dispatch workflow-execution-failed notification: %w", err)
}
@@ -130,7 +143,17 @@ func (w *WorkflowExecutionFailedWorker) Work(ctx context.Context, job *river.Job
w.logger.Infow("WorkflowExecutionFailedWorker: failure notification sent",
"workflow_execution_id", args.WorkflowExecutionID,
"recipient", recipient.Email,
+ "system_target_count", len(targets),
)
return nil
}
+
+func (w *WorkflowExecutionFailedWorker) configuredWorkflowExecutionFailedTargets(ctx context.Context) ([]notification.Target, error) {
+ if w.db == nil {
+ return []notification.Target{}, nil
+ }
+
+ return notification.NewGORMSystemDestinationRepository(w.db, notificationproviders.NewLookup()).
+ ListTargetsBySystemNotificationName(ctx, notification.SystemNotificationNameWorkflowExecutionFailed)
+}
diff --git a/internal/service/worker/workflow_execution_failed_worker_test.go b/internal/service/worker/workflow_execution_failed_worker_test.go
index bb0d067c..945fee66 100644
--- a/internal/service/worker/workflow_execution_failed_worker_test.go
+++ b/internal/service/worker/workflow_execution_failed_worker_test.go
@@ -16,6 +16,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
+ "gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -68,7 +69,7 @@ func TestWorkflowExecutionFailedWorker_SlackSubscribedUser_SendsAllAssociatedCha
LastName: "Owner",
SlackUserID: "UWFEXEC1",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"slack"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"slack"}},
},
}
mockRepo.On("FindUserByID", ctx, createdByID.String()).Return(user, nil)
@@ -108,7 +109,7 @@ func TestWorkflowExecutionFailedWorker_EmailAndSlackUser_SendsBoth(t *testing.T)
LastName: "Owner",
SlackUserID: "UWFEXEC2",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"email", "slack"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"email", "slack"}},
},
}
mockRepo.On("FindUserByID", ctx, createdByID.String()).Return(user, nil)
@@ -131,6 +132,61 @@ func TestWorkflowExecutionFailedWorker_EmailAndSlackUser_SendsBoth(t *testing.T)
mockRepo.AssertExpectations(t)
}
+func TestWorkflowExecutionFailedWorker_ConfiguredSystemEmailDestination_SendsSystemAudience(t *testing.T) {
+ ctx := context.Background()
+ db := newWorkflowNotificationJobsTestDB(t)
+ createdByID := uuid.New()
+ executionID := seedWorkflowExecutionFailedFixture(t, db, createdByID)
+
+ require.NoError(t, db.Create(&relational.SystemNotificationDestination{
+ NotificationType: notification.SystemNotificationNameWorkflowExecutionFailed,
+ Provider: notification.DeliveryChannelEmail,
+ Target: datatypes.NewJSONType(relational.SystemNotificationTarget{
+ Address: map[string]string{"email": "alerts@example.com"},
+ }),
+ }).Error)
+
+ mockEmail := &MockEmailService{}
+ mockRepo := &MockUserRepository{}
+ mockLog := zap.NewNop().Sugar()
+
+ user := NotificationUser{
+ ID: createdByID.String(),
+ Email: "owner@example.com",
+ FirstName: "Workflow",
+ LastName: "Owner",
+ NotificationSubscriptions: []NotificationSubscription{
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"email"}},
+ },
+ }
+ mockRepo.On("FindUserByID", ctx, createdByID.String()).Return(user, nil)
+
+ mockEmail.On("UseTemplate", "workflow-execution-failed", mock.MatchedBy(func(data map[string]interface{}) bool {
+ return data["IsSystemAudience"] == false &&
+ data["RecipientName"] == "Workflow Owner" &&
+ data["MyTasksURL"] == "http://localhost:8000/my-tasks"
+ })).Return("failed", "failed text", nil).Once()
+ mockEmail.On("UseTemplate", "workflow-execution-failed", mock.MatchedBy(func(data map[string]interface{}) bool {
+ return data["IsSystemAudience"] == true &&
+ data["RecipientName"] == "" &&
+ data["MyTasksURL"] == ""
+ })).Return("system failed", "system failed text", nil).Once()
+ mockEmail.On("GetDefaultFromAddress").Return("noreply@example.com").Twice()
+ mockEmail.On("Send", ctx, mock.MatchedBy(func(msg *types.Message) bool {
+ return len(msg.To) == 1 && msg.To[0] == "owner@example.com"
+ })).Return(&types.SendResult{Success: true, MessageID: "wf-exec-email-user"}, nil).Once()
+ mockEmail.On("Send", ctx, mock.MatchedBy(func(msg *types.Message) bool {
+ return len(msg.To) == 1 && msg.To[0] == "alerts@example.com"
+ })).Return(&types.SendResult{Success: true, MessageID: "wf-exec-email-system"}, nil).Once()
+
+ w := NewWorkflowExecutionFailedWorker(db, mockRepo, "http://localhost:8000", newTestNotificationRuntimeProvider(mockEmail, nil), mockLog)
+
+ err := w.Work(ctx, makeFailedJob(WorkflowExecutionFailedArgs{WorkflowExecutionID: executionID.String()}))
+ assert.NoError(t, err)
+ mockEmail.AssertExpectations(t)
+ mockRepo.AssertExpectations(t)
+}
+
func seedWorkflowExecutionFailedFixture(t *testing.T, db *gorm.DB, createdByID uuid.UUID) uuid.UUID {
t.Helper()
diff --git a/internal/service/worker/workflow_notification_jobs_test.go b/internal/service/worker/workflow_notification_jobs_test.go
index 30ddcbfb..af52af87 100644
--- a/internal/service/worker/workflow_notification_jobs_test.go
+++ b/internal/service/worker/workflow_notification_jobs_test.go
@@ -99,7 +99,7 @@ func TestDueSoonCheckerWorker_EnqueuesOneJobPerChannel(t *testing.T) {
createWorkflowNotificationUser(t, db, userID, "alice@example.com", "UALICE")
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: userID.String(),
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: datatypes.JSONSlice[string]{
notification.DeliveryChannelEmail,
notification.DeliveryChannelSlack,
@@ -202,7 +202,7 @@ func TestDueSoonCheckerWorker_CachesFetchedUsersAcrossDispatches(t *testing.T) {
LastName: "User",
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeTaskAvailable,
+ NotificationType: notification.SubscriptionGateTaskAvailable,
Channels: []string{notification.DeliveryChannelEmail},
},
},
@@ -282,7 +282,7 @@ func TestWorkflowTaskDigestCheckerWorker_EnqueuesOneJobPerSubscribedUser(t *test
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: userOneID.String(),
- NotificationType: notification.NotificationTypeTaskDailyDigest,
+ NotificationType: notification.SubscriptionGateTaskDailyDigest,
Channels: datatypes.JSONSlice[string]{
notification.DeliveryChannelEmail,
notification.DeliveryChannelSlack,
@@ -290,12 +290,12 @@ func TestWorkflowTaskDigestCheckerWorker_EnqueuesOneJobPerSubscribedUser(t *test
}).Error)
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: userTwoID.String(),
- NotificationType: notification.NotificationTypeTaskDailyDigest,
+ NotificationType: notification.SubscriptionGateTaskDailyDigest,
Channels: datatypes.JSONSlice[string]{notification.DeliveryChannelEmail},
}).Error)
require.NoError(t, db.Create(&relational.UserNotificationSubscription{
UserID: missingUserID.String(),
- NotificationType: notification.NotificationTypeTaskDailyDigest,
+ NotificationType: notification.SubscriptionGateTaskDailyDigest,
Channels: datatypes.JSONSlice[string]{notification.DeliveryChannelSlack},
}).Error)
@@ -325,6 +325,7 @@ func newWorkflowNotificationJobsTestDB(t *testing.T) *gorm.DB {
&relational.User{},
&relational.SlackUserLink{},
&relational.UserNotificationSubscription{},
+ &relational.SystemNotificationDestination{},
&relational.SystemSecurityPlan{},
&workflows.WorkflowDefinition{},
&workflows.WorkflowInstance{},
diff --git a/internal/service/worker/workflow_notifications.go b/internal/service/worker/workflow_notifications.go
index f6f6de7d..699118ac 100644
--- a/internal/service/worker/workflow_notifications.go
+++ b/internal/service/worker/workflow_notifications.go
@@ -16,7 +16,7 @@ const (
workflowTaskAssignedNotificationKind = notification.Kind(JobTypeWorkflowTaskAssigned)
workflowTaskDueSoonNotificationKind = notification.Kind(JobTypeWorkflowTaskDueSoon)
workflowTaskDigestNotificationKind = notification.Kind(JobTypeWorkflowTaskDigest)
- workflowExecutionFailedNotificationKind = notification.Kind(JobTypeWorkflowExecutionFailed)
+ workflowExecutionFailedNotificationKind = notification.NotificationKindWorkflowExecutionFailed
)
type workflowTaskAssignedNotificationModel struct {
@@ -60,6 +60,7 @@ type workflowExecutionFailedNotificationModel struct {
TotalSteps int
WorkflowURL string
MyTasksURL string
+ IsSystemAudience bool
}
type notificationUserRepositoryAdapter struct {
@@ -75,28 +76,28 @@ func newWorkflowNotificationServiceFromFactory(
users,
newTypedNotificationDefinition(
workflowTaskAssignedNotificationKind,
- notification.NotificationTypeTaskAvailable,
+ notification.SubscriptionGateTaskAvailable,
newNotificationModelDecoder[workflowTaskAssignedNotificationModel]("workflow task assigned model"),
renderWorkflowTaskAssignedEmail,
renderWorkflowTaskAssignedSlack,
),
newTypedNotificationDefinition(
workflowTaskDueSoonNotificationKind,
- notification.NotificationTypeTaskAvailable,
+ notification.SubscriptionGateTaskAvailable,
newNotificationModelDecoder[workflowTaskDueSoonNotificationModel]("workflow task due soon model"),
renderWorkflowTaskDueSoonEmail,
renderWorkflowTaskDueSoonSlack,
),
newTypedNotificationDefinition(
workflowTaskDigestNotificationKind,
- notification.NotificationTypeTaskDailyDigest,
+ notification.SubscriptionGateTaskDailyDigest,
newNotificationModelDecoder[workflowTaskDigestNotificationModel]("workflow task digest model"),
renderWorkflowTaskDigestEmail,
renderWorkflowTaskDigestSlack,
),
newTypedNotificationDefinition(
workflowExecutionFailedNotificationKind,
- notification.NotificationTypeUngated,
+ notification.SubscriptionGateUngated,
newNotificationModelDecoder[workflowExecutionFailedNotificationModel]("workflow execution failed model"),
renderWorkflowExecutionFailedEmail,
renderWorkflowExecutionFailedSlack,
@@ -152,6 +153,7 @@ func newWorkflowExecutionFailedNotificationModel(data workflowExecutionFailedNot
TotalSteps: data.TotalSteps,
WorkflowURL: strings.TrimSpace(data.WorkflowURL),
MyTasksURL: strings.TrimSpace(data.MyTasksURL),
+ IsSystemAudience: data.IsSystemAudience,
}
}
@@ -210,6 +212,40 @@ func buildWorkflowExecutionFailedNotificationRequest(
)
}
+func buildWorkflowExecutionFailedSystemNotificationRequest(
+ args WorkflowExecutionFailedArgs,
+ targets []notification.Target,
+ data workflowExecutionFailedNotificationModel,
+) (notification.Request, bool) {
+ audiences := make([]notification.Audience, 0, len(targets))
+ for i := range targets {
+ // Directly assign the map since no mutation is needed
+ audiences = append(audiences, notification.Audience{
+ Direct: ¬ification.DirectAudience{
+ Provider: targets[i].Provider,
+ Address: targets[i].Address,
+ },
+ })
+ }
+
+ if len(audiences) == 0 {
+ return notification.Request{}, false
+ }
+
+ // Use the provided data directly and only modify necessary fields
+ systemModel := data
+ systemModel.RecipientName = ""
+ systemModel.MyTasksURL = ""
+ systemModel.IsSystemAudience = true
+
+ return notification.Request{
+ Kind: workflowExecutionFailedNotificationKind,
+ Audiences: audiences,
+ Model: systemModel,
+ Options: newJobDispatchOptions(JobTypeWorkflowExecutionFailed, "", args.WorkflowExecutionID),
+ }, true
+}
+
func (m workflowTaskDigestNotificationModel) templateData() map[string]interface{} {
return map[string]interface{}{
"UserName": m.UserName,
@@ -233,6 +269,7 @@ func (m workflowExecutionFailedNotificationModel) templateData() map[string]inte
"TotalSteps": m.TotalSteps,
"WorkflowURL": m.WorkflowURL,
"MyTasksURL": m.MyTasksURL,
+ "IsSystemAudience": m.IsSystemAudience,
}
}
@@ -292,7 +329,7 @@ func (a *notificationUserRepositoryAdapter) cacheUsers(users ...NotificationUser
}
}
-func (a *notificationUserRepositoryAdapter) ListActiveUsersByNotificationType(_ context.Context, notificationType string) ([]notification.User, error) {
+func (a *notificationUserRepositoryAdapter) ListActiveUsersBySubscriptionGate(_ context.Context, notificationType string) ([]notification.User, error) {
if a == nil || len(a.cached) == 0 {
return []notification.User{}, nil
}
@@ -395,11 +432,18 @@ func renderWorkflowExecutionFailedEmail(failedModel workflowExecutionFailedNotif
instanceName = failedModel.WorkflowTitle
}
+ subject := fmt.Sprintf("Workflow execution failed: %s", instanceName)
+ textBody := fmt.Sprintf("Workflow execution failed for %s. Open: %s", instanceName, failedModel.WorkflowURL)
+ if failedModel.IsSystemAudience {
+ subject = fmt.Sprintf("Workflow execution failed: %s", instanceName)
+ textBody = fmt.Sprintf("Workflow execution failed for %s. Review details: %s", instanceName, failedModel.WorkflowURL)
+ }
+
return emailprovider.TemplateContent{
TemplateName: "workflow-execution-failed",
TemplateData: failedModel.templateData(),
- Subject: fmt.Sprintf("Workflow execution failed: %s", instanceName),
- TextBody: fmt.Sprintf("Workflow execution failed for %s. Open: %s", instanceName, failedModel.WorkflowURL),
+ Subject: subject,
+ TextBody: textBody,
}, nil
}
@@ -451,6 +495,11 @@ func renderWorkflowTaskDigestSlack(digestModel workflowTaskDigestNotificationMod
}
func renderWorkflowExecutionFailedSlack(failedModel workflowExecutionFailedNotificationModel) (*slackprovider.Message, error) {
+ myTasksURL := failedModel.MyTasksURL
+ if failedModel.IsSystemAudience {
+ myTasksURL = ""
+ }
+
message, err := slackprovider.FormatWorkflowExecutionFailedMessage(
failedModel.RecipientName,
failedModel.WorkflowTitle,
@@ -462,7 +511,7 @@ func renderWorkflowExecutionFailedSlack(failedModel workflowExecutionFailedNotif
failedModel.CompletedSteps,
failedModel.TotalSteps,
failedModel.WorkflowURL,
- failedModel.MyTasksURL,
+ myTasksURL,
)
if err != nil {
return nil, fmt.Errorf("failed to format workflow-execution-failed slack message: %w", err)
diff --git a/internal/service/worker/workflow_task_assigned_worker_test.go b/internal/service/worker/workflow_task_assigned_worker_test.go
index d6f86d70..54f4c2d3 100644
--- a/internal/service/worker/workflow_task_assigned_worker_test.go
+++ b/internal/service/worker/workflow_task_assigned_worker_test.go
@@ -48,7 +48,7 @@ func TestWorkflowTaskAssignedWorker_SubscribedUser_SendsEmail(t *testing.T) {
FirstName: "Alice",
LastName: "Smith",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-1").Return(user, nil)
@@ -93,7 +93,7 @@ func TestWorkflowTaskAssignedWorker_UnsubscribedUser_Skips(t *testing.T) {
Email: "bob@example.com",
FirstName: "Bob",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{}},
},
}
mockRepo.On("FindUserByID", ctx, "user-2").Return(user, nil)
@@ -157,7 +157,7 @@ func TestWorkflowTaskAssignedWorker_TemplateError_ReturnsError(t *testing.T) {
Email: "carol@example.com",
FirstName: "Carol",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-3").Return(user, nil)
@@ -198,7 +198,7 @@ func TestWorkflowTaskAssignedWorker_MultiChannel_EmailChannelJob_SendsOnlyEmail(
FirstName: "Dora",
SlackUserID: "U12345",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"slack", "email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"slack", "email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-4").Return(user, nil)
@@ -249,7 +249,7 @@ func TestWorkflowTaskAssignedWorker_MultiChannel_SlackChannelJob_SendsOnlySlack(
FirstName: "Ella",
SlackUserID: "USLACK5",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"slack", "email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"slack", "email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-5").Return(user, nil)
@@ -336,7 +336,7 @@ func TestWorkflowTaskAssignedWorker_WithNotificationEnqueuer_EnqueuesSubscribedC
FirstName: "Grace",
SlackUserID: "USLACK7",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"slack", "email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"slack", "email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-7").Return(user, nil)
diff --git a/internal/service/worker/workflow_task_digest_checker.go b/internal/service/worker/workflow_task_digest_checker.go
index d979138c..39911bda 100644
--- a/internal/service/worker/workflow_task_digest_checker.go
+++ b/internal/service/worker/workflow_task_digest_checker.go
@@ -43,7 +43,7 @@ func (w *WorkflowTaskDigestCheckerWorker) Work(ctx context.Context, job *river.J
return fmt.Errorf("WorkflowTaskDigestCheckerWorker: db is nil")
}
- userIDs, err := notification.NewGORMUserRepository(w.db).ListActiveUserIDsByNotificationType(ctx, notification.NotificationTypeTaskDailyDigest)
+ userIDs, err := notification.NewGORMUserRepository(w.db).ListActiveUserIDsBySubscriptionGate(ctx, notification.SubscriptionGateTaskDailyDigest)
if err != nil {
return fmt.Errorf("workflow-task-digest-checker: failed to load subscribed users: %w", err)
}
diff --git a/internal/service/worker/workflow_task_digest_worker_test.go b/internal/service/worker/workflow_task_digest_worker_test.go
index a9c906fb..46cab503 100644
--- a/internal/service/worker/workflow_task_digest_worker_test.go
+++ b/internal/service/worker/workflow_task_digest_worker_test.go
@@ -29,7 +29,7 @@ func TestWorkflowTaskDigestWorker_DBRequiredAfterUserLookup(t *testing.T) {
FirstName: "Alice",
NotificationSubscriptions: []NotificationSubscription{
{
- NotificationType: notification.NotificationTypeTaskDailyDigest,
+ NotificationType: notification.SubscriptionGateTaskDailyDigest,
Channels: []string{},
},
},
diff --git a/internal/service/worker/workflow_task_due_soon_worker_test.go b/internal/service/worker/workflow_task_due_soon_worker_test.go
index a0e40d89..8b87f94a 100644
--- a/internal/service/worker/workflow_task_due_soon_worker_test.go
+++ b/internal/service/worker/workflow_task_due_soon_worker_test.go
@@ -33,7 +33,7 @@ func TestWorkflowTaskDueSoonWorker_SubscribedUser_SendsEmail(t *testing.T) {
FirstName: "Alice",
LastName: "Smith",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-1").Return(user, nil)
@@ -78,7 +78,7 @@ func TestWorkflowTaskDueSoonWorker_UnsubscribedUser_Skips(t *testing.T) {
Email: "bob@example.com",
FirstName: "Bob",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{}},
},
}
mockRepo.On("FindUserByID", ctx, "user-2").Return(user, nil)
@@ -142,7 +142,7 @@ func TestWorkflowTaskDueSoonWorker_TemplateError_ReturnsError(t *testing.T) {
Email: "carol@example.com",
FirstName: "Carol",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-3").Return(user, nil)
@@ -184,7 +184,7 @@ func TestWorkflowTaskDueSoonWorker_MultiChannel_EmailChannelJob_SendsOnlyEmail(t
FirstName: "Dora",
SlackUserID: "U12345",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"slack", "email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"slack", "email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-4").Return(user, nil)
@@ -235,7 +235,7 @@ func TestWorkflowTaskDueSoonWorker_MultiChannel_SlackChannelJob_SendsOnlySlack(t
FirstName: "Fran",
SlackUserID: "USLACK6",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"slack", "email"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"slack", "email"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-6").Return(user, nil)
@@ -285,7 +285,7 @@ func TestWorkflowTaskDueSoonWorker_SlackOnlyUser_SendsSlack(t *testing.T) {
FirstName: "Eve",
SlackUserID: "USLACK1",
NotificationSubscriptions: []NotificationSubscription{
- {NotificationType: notification.NotificationTypeTaskAvailable, Channels: []string{"slack"}},
+ {NotificationType: notification.SubscriptionGateTaskAvailable, Channels: []string{"slack"}},
},
}
mockRepo.On("FindUserByID", ctx, "user-5").Return(user, nil)