sojorn/go-backend/internal/services/notification_service.go
Patrick Britton 38653f5854 Sojorn Backend Finalization & Cleanup - Complete Migration from Supabase
##  Phase 1: Critical Feature Completion (Beacon Voting)
- Add VouchBeacon, ReportBeacon, RemoveBeaconVote methods to PostRepository
- Implement beacon voting HTTP handlers with confidence score calculations
- Register new beacon routes: /beacons/:id/vouch, /beacons/:id/report, /beacons/:id/vouch (DELETE)
- Auto-flag beacons at 5+ reports, confidence scoring (0.5 base + 0.1 per vouch)

##  Phase 2: Feed Logic & Post Distribution Integrity
- Verify unified feed logic supports all content types (Standard, Quips, Beacons)
- Ensure proper distribution: Profile Feed + Main/Home Feed for followers
- Beacon Map integration for location-based content
- Video content filtering for Quips feed

##  Phase 3: The Notification System
- Create comprehensive NotificationService with FCM integration
- Add CreateNotification method to NotificationRepository
- Implement smart deep linking: beacon_map, quip_feed, main_feed
- Trigger notifications for beacon interactions and cross-post comments
- Push notification logic with proper content type detection

##  Phase 4: The Great Supabase Purge
- Delete function_proxy.go and remove /functions/:name route
- Remove SupabaseURL, SupabaseKey from config.go
- Remove SupabaseID field from User model
- Clean all Supabase imports and dependencies
- Sanitize codebase of legacy Supabase references

##  Phase 5: Flutter Frontend Integration
- Implement vouchBeacon(), reportBeacon(), removeBeaconVote() in ApiService
- Replace TODO delay in video_comments_sheet.dart with actual publishComment call
- Fix compilation errors (named parameters, orphaned child properties)
- Complete frontend integration with Go API endpoints

##  Additional Improvements
- Fix compilation errors in threaded_comment_widget.dart (orphaned child property)
- Update video_comments_sheet.dart to use proper named parameters
- Comprehensive error handling and validation
- Production-ready notification system with deep linking

##  Migration Status: 100% Complete
- Backend: Fully migrated from Supabase to custom Go/Gin API
- Frontend: Integrated with new Go endpoints
- Notifications: Complete FCM integration with smart routing
- Database: Clean of all Supabase dependencies
- Features: All functionality preserved and enhanced

Ready for VPS deployment and production testing!
2026-01-30 09:24:31 -06:00

149 lines
3.9 KiB
Go

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 ""
}