package service import ( "context" "fmt" "net/url" "eslogad-be/internal/config" "eslogad-be/internal/contract" "github.com/google/uuid" novu "github.com/novuhq/go-novu/lib" ) type NotificationService interface { TriggerNotification(ctx context.Context, req *contract.TriggerNotificationRequest) (*contract.TriggerNotificationResponse, error) BulkTriggerNotification(ctx context.Context, req *contract.BulkTriggerNotificationRequest) (*contract.BulkTriggerNotificationResponse, error) GetSubscriber(ctx context.Context, userID uuid.UUID) (*contract.GetSubscriberResponse, error) UpdateSubscriberChannel(ctx context.Context, req *contract.UpdateSubscriberChannelRequest) (*contract.UpdateSubscriberChannelResponse, error) } type NotificationServiceImpl struct { client *novu.APIClient config *config.NovuConfig userProcessor UserProcessorForNotification } type UserProcessorForNotification interface { GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) } func NewNotificationService(cfg *config.NovuConfig, userProcessor UserProcessorForNotification) *NotificationServiceImpl { var client *novu.APIClient if cfg.APIKey != "" { // Create Novu config with backend URL novuConfig := &novu.Config{} if cfg.BaseURL != "" { backendURL, err := url.Parse(cfg.BaseURL) if err == nil { novuConfig.BackendURL = backendURL } } client = novu.NewAPIClient(cfg.APIKey, novuConfig) } return &NotificationServiceImpl{ client: client, config: cfg, userProcessor: userProcessor, } } func (s *NotificationServiceImpl) TriggerNotification(ctx context.Context, req *contract.TriggerNotificationRequest) (*contract.TriggerNotificationResponse, error) { if s.client == nil { return &contract.TriggerNotificationResponse{ Success: false, Message: "notification service not configured", }, nil } subscriberID := req.UserID.String() _, err := s.ensureSubscriberExists(ctx, req.UserID) if err != nil { return &contract.TriggerNotificationResponse{ Success: false, Message: fmt.Sprintf("failed to ensure subscriber exists: %v", err), }, nil } // Prepare the trigger payload to := map[string]interface{}{ "subscriberId": subscriberID, } // Add additional recipient information if provided if req.To != nil { if req.To.Email != "" { to["email"] = req.To.Email } if req.To.Phone != "" { to["phone"] = req.To.Phone } } // Prepare overrides overrides := make(map[string]interface{}) if req.Overrides != nil { if req.Overrides.Email != nil { overrides["email"] = req.Overrides.Email } if req.Overrides.SMS != nil { overrides["sms"] = req.Overrides.SMS } if req.Overrides.InApp != nil { overrides["in_app"] = req.Overrides.InApp } if req.Overrides.Push != nil { overrides["push"] = req.Overrides.Push } if req.Overrides.Chat != nil { overrides["chat"] = req.Overrides.Chat } } // Trigger the notification using the template ID triggerPayload := novu.ITriggerPayloadOptions{ To: to, Payload: req.TemplateData, Overrides: overrides, } resp, err := s.client.EventApi.Trigger(ctx, req.TemplateID, triggerPayload) if err != nil { return &contract.TriggerNotificationResponse{ Success: false, Message: fmt.Sprintf("failed to trigger notification: %v", err), }, nil } // Extract transaction ID from response transactionID := "" if respData, ok := resp.Data.(map[string]interface{}); ok { if txID, exists := respData["transactionId"]; exists { transactionID = fmt.Sprintf("%v", txID) } } return &contract.TriggerNotificationResponse{ Success: true, TransactionID: transactionID, Message: "notification triggered successfully", }, nil } func (s *NotificationServiceImpl) BulkTriggerNotification(ctx context.Context, req *contract.BulkTriggerNotificationRequest) (*contract.BulkTriggerNotificationResponse, error) { if s.client == nil { return &contract.BulkTriggerNotificationResponse{ Success: false, TotalSent: 0, TotalFailed: len(req.UserIDs), }, nil } results := make([]contract.NotificationResult, 0, len(req.UserIDs)) successCount := 0 failedCount := 0 for _, userID := range req.UserIDs { // Create individual trigger request triggerReq := &contract.TriggerNotificationRequest{ UserID: userID, TemplateID: req.TemplateID, TemplateData: req.TemplateData, Overrides: req.Overrides, } resp, err := s.TriggerNotification(ctx, triggerReq) result := contract.NotificationResult{ UserID: userID, Success: resp.Success, } if resp.Success { result.TransactionID = resp.TransactionID successCount++ } else { if err != nil { result.Error = err.Error() } else { result.Error = resp.Message } failedCount++ } results = append(results, result) } return &contract.BulkTriggerNotificationResponse{ Success: failedCount == 0, TotalSent: successCount, TotalFailed: failedCount, Results: results, }, nil } func (s *NotificationServiceImpl) GetSubscriber(ctx context.Context, userID uuid.UUID) (*contract.GetSubscriberResponse, error) { if s.client == nil { return nil, fmt.Errorf("notification service not configured") } subscriberID := userID.String() // Try to get the subscriber subscriber, err := s.client.SubscriberApi.Get(ctx, subscriberID) if err != nil { // If subscriber doesn't exist, create it _, createErr := s.ensureSubscriberExists(ctx, userID) if createErr != nil { return nil, fmt.Errorf("failed to get or create subscriber: %w", createErr) } // Try to get again after creation subscriber, err = s.client.SubscriberApi.Get(ctx, subscriberID) if err != nil { return nil, fmt.Errorf("failed to get subscriber after creation: %w", err) } } // Convert Novu subscriber to our response format response := &contract.GetSubscriberResponse{ SubscriberID: subscriberID, } if subData, ok := subscriber.Data.(map[string]interface{}); ok { if email, exists := subData["email"]; exists { response.Email = fmt.Sprintf("%v", email) } if firstName, exists := subData["firstName"]; exists { response.FirstName = fmt.Sprintf("%v", firstName) } if lastName, exists := subData["lastName"]; exists { response.LastName = fmt.Sprintf("%v", lastName) } if phone, exists := subData["phone"]; exists { response.Phone = fmt.Sprintf("%v", phone) } if avatar, exists := subData["avatar"]; exists { response.Avatar = fmt.Sprintf("%v", avatar) } if data, exists := subData["data"]; exists { if dataMap, ok := data.(map[string]interface{}); ok { response.Data = dataMap } } if channels, exists := subData["channels"]; exists { if channelList, ok := channels.([]interface{}); ok { response.Channels = make([]contract.ChannelCredentials, 0, len(channelList)) for _, ch := range channelList { if chMap, ok := ch.(map[string]interface{}); ok { channelCred := contract.ChannelCredentials{} if chType, exists := chMap["providerId"]; exists { channelCred.Channel = fmt.Sprintf("%v", chType) } if creds, exists := chMap["credentials"]; exists { if credMap, ok := creds.(map[string]interface{}); ok { channelCred.Credentials = credMap } } response.Channels = append(response.Channels, channelCred) } } } } } return response, nil } func (s *NotificationServiceImpl) UpdateSubscriberChannel(ctx context.Context, req *contract.UpdateSubscriberChannelRequest) (*contract.UpdateSubscriberChannelResponse, error) { if s.client == nil { return &contract.UpdateSubscriberChannelResponse{ Success: false, Message: "notification service not configured", }, nil } subscriberID := req.UserID.String() // Ensure subscriber exists _, err := s.ensureSubscriberExists(ctx, req.UserID) if err != nil { return &contract.UpdateSubscriberChannelResponse{ Success: false, Message: fmt.Sprintf("failed to ensure subscriber exists: %v", err), }, nil } // Since the Novu Go SDK doesn't have UpdateCredentials, we'll update the subscriber data instead // Get user info to update subscriber user, err := s.userProcessor.GetUserByID(ctx, req.UserID) if err != nil { return &contract.UpdateSubscriberChannelResponse{ Success: false, Message: fmt.Sprintf("failed to get user data: %v", err), }, nil } // Prepare subscriber data with channel credentials stored in data data := map[string]interface{}{ "userId": user.ID.String(), "email": user.Email, "isActive": user.IsActive, "updatedAt": user.UpdatedAt, } // Store channel credentials in the subscriber data channelKey := fmt.Sprintf("channel_%s", req.Channel) data[channelKey] = req.Credentials // Update subscriber with new data updateData := novu.SubscriberPayload{ Email: user.Email, FirstName: user.Name, LastName: "", Phone: "", Avatar: "", Data: data, } _, err = s.client.SubscriberApi.Update(ctx, subscriberID, updateData) if err != nil { return &contract.UpdateSubscriberChannelResponse{ Success: false, Message: fmt.Sprintf("failed to update subscriber channel: %v", err), }, nil } return &contract.UpdateSubscriberChannelResponse{ Success: true, Message: "subscriber channel updated successfully", }, nil } func (s *NotificationServiceImpl) ensureSubscriberExists(ctx context.Context, userID uuid.UUID) (bool, error) { subscriberID := userID.String() _, err := s.client.SubscriberApi.Get(ctx, subscriberID) if err == nil { return false, nil } user, err := s.userProcessor.GetUserByID(ctx, userID) if err != nil { return false, fmt.Errorf("failed to get user data: %w", err) } data := map[string]interface{}{ "userId": user.ID.String(), "email": user.Email, "isActive": user.IsActive, "createdAt": user.CreatedAt, } if user.Roles != nil && len(user.Roles) > 0 { roles := make([]map[string]interface{}, len(user.Roles)) for i, role := range user.Roles { roles[i] = map[string]interface{}{ "id": role.ID.String(), "name": role.Name, "code": role.Code, } } data["roles"] = roles } if user.DepartmentResponse != nil && len(user.DepartmentResponse) > 0 { depts := make([]map[string]interface{}, len(user.DepartmentResponse)) for i, dept := range user.DepartmentResponse { depts[i] = map[string]interface{}{ "id": dept.ID.String(), "name": dept.Name, "code": dept.Code, } } data["departments"] = depts } subscriber := novu.SubscriberPayload{ Email: user.Email, FirstName: user.Name, LastName: "", Phone: "", Avatar: "", Data: data, } _, err = s.client.SubscriberApi.Identify(ctx, subscriberID, subscriber) if err != nil { return false, fmt.Errorf("failed to create subscriber: %w", err) } return true, nil }