package services import ( "context" "encoding/json" "fmt" "github.com/google/uuid" "github.com/patbritton/sojorn-backend/internal/models" "github.com/patbritton/sojorn-backend/internal/repository" "github.com/rs/zerolog/log" ) type NotificationService struct { notifRepo *repository.NotificationRepository pushSvc *PushService } func NewNotificationService(notifRepo *repository.NotificationRepository, pushSvc *PushService) *NotificationService { return &NotificationService{ notifRepo: notifRepo, pushSvc: pushSvc, } } func (s *NotificationService) CreateNotification(ctx context.Context, userID, actorID, notificationType string, postID *string, commentID *string, metadata map[string]interface{}) error { // Parse UUIDs userUUID, err := uuid.Parse(userID) if err != nil { return fmt.Errorf("invalid user ID: %w", err) } actorUUID, err := uuid.Parse(actorID) if err != nil { return fmt.Errorf("invalid actor ID: %w", err) } // Create database notification notif := &models.Notification{ UserID: userUUID, ActorID: actorUUID, Type: notificationType, PostID: parseNullableUUID(postID), CommentID: parseNullableUUID(commentID), IsRead: false, } // Serialize metadata if metadata != nil { metadataBytes, err := json.Marshal(metadata) if err != nil { log.Error().Err(err).Msg("Failed to marshal notification metadata") } else { notif.Metadata = metadataBytes } } // Insert into database err = s.notifRepo.CreateNotification(ctx, notif) if err != nil { return fmt.Errorf("failed to create notification: %w", err) } // Send push notification if s.pushSvc != nil { title, body, data := s.buildPushNotification(notificationType, metadata) if err := s.pushSvc.SendPush(ctx, userID, title, body, data); err != nil { log.Warn().Err(err).Str("user_id", userID).Msg("Failed to send push notification") } } return nil } func (s *NotificationService) buildPushNotification(notificationType string, metadata map[string]interface{}) (title, body string, data map[string]string) { actorName, _ := metadata["actor_name"].(string) switch notificationType { case "beacon_vouch": title = "Beacon Vouched" body = fmt.Sprintf("%s vouched for your beacon", actorName) data = map[string]string{ "type": "beacon_vouch", "beacon_id": getString(metadata, "beacon_id"), "target": "beacon_map", // Deep link to map } case "beacon_report": title = "Beacon Reported" body = fmt.Sprintf("%s reported your beacon", actorName) data = map[string]string{ "type": "beacon_report", "beacon_id": getString(metadata, "beacon_id"), "target": "beacon_map", // Deep link to map } case "comment": title = "New Comment" postType := getString(metadata, "post_type") if postType == "beacon" { body = fmt.Sprintf("%s commented on your beacon", actorName) data = map[string]string{ "type": "comment", "post_id": getString(metadata, "post_id"), "target": "beacon_map", // Deep link to map for beacon comments } } else if postType == "quip" { body = fmt.Sprintf("%s commented on your quip", actorName) data = map[string]string{ "type": "comment", "post_id": getString(metadata, "post_id"), "target": "quip_feed", // Deep link to quip feed } } else { body = fmt.Sprintf("%s commented on your post", actorName) data = map[string]string{ "type": "comment", "post_id": getString(metadata, "post_id"), "target": "main_feed", // Deep link to main feed } } default: title = "Sojorn" body = "You have a new notification" data = map[string]string{"type": notificationType} } return title, body, data } // Helper functions func parseNullableUUID(s *string) *uuid.UUID { if s == nil { return nil } u, err := uuid.Parse(*s) if err != nil { return nil } return &u } func getString(m map[string]interface{}, key string) string { if val, ok := m[key]; ok { if str, ok := val.(string); ok { return str } } return "" }