package notifications import ( "context" "encoding/json" "errors" "fmt" "log" "net/http" "time" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" ) // FCMService implements push notifications via Firebase Cloud Messaging type FCMService struct { serverKey string httpClient *http.Client tokens map[uuid.UUID][]string // userID -> []deviceTokens (in-memory, should be DB in production) } // NewFCMService creates a new FCM notification service func NewFCMService(serverKey string) *FCMService { return &FCMService{ serverKey: serverKey, httpClient: &http.Client{ Timeout: 10 * time.Second, }, tokens: make(map[uuid.UUID][]string), } } // RegisterToken stores a device token for a user func (s *FCMService) RegisterToken(ctx context.Context, userID uuid.UUID, token string) error { if token == "" { return errors.New("token cannot be empty") } // Check if token already exists for _, t := range s.tokens[userID] { if t == token { return nil // Already registered } } s.tokens[userID] = append(s.tokens[userID], token) log.Printf("πŸ“² [FCM] Registered token for user %s: %s...", userID, token[:min(20, len(token))]) return nil } // UnregisterToken removes a device token func (s *FCMService) UnregisterToken(ctx context.Context, userID uuid.UUID, token string) error { tokens := s.tokens[userID] for i, t := range tokens { if t == token { s.tokens[userID] = append(tokens[:i], tokens[i+1:]...) log.Printf("πŸ“² [FCM] Unregistered token for user %s", userID) return nil } } return nil } // FCMMessage represents the FCM request payload type FCMMessage struct { To string `json:"to,omitempty"` Notification *FCMNotification `json:"notification,omitempty"` Data map[string]string `json:"data,omitempty"` } type FCMNotification struct { Title string `json:"title"` Body string `json:"body"` Icon string `json:"icon,omitempty"` Click string `json:"click_action,omitempty"` } // SendPush sends a push notification to a user func (s *FCMService) SendPush(ctx context.Context, userID uuid.UUID, title, body string, data map[string]string) error { tokens := s.tokens[userID] if len(tokens) == 0 { log.Printf("πŸ“² [FCM] No tokens registered for user %s, skipping push", userID) return nil } for _, token := range tokens { msg := FCMMessage{ To: token, Notification: &FCMNotification{ Title: title, Body: body, Icon: "/favicon.ico", }, Data: data, } if err := s.sendToFCM(ctx, msg); err != nil { log.Printf("πŸ“² [FCM] Error sending to token %s: %v", token[:min(20, len(token))], err) continue } log.Printf("πŸ“² [FCM] Sent push to user %s: %s", userID, title) } return nil } func (s *FCMService) sendToFCM(ctx context.Context, msg FCMMessage) error { // If no server key configured, just log if s.serverKey == "" { log.Printf("πŸ“² [FCM] (Mock) Would send: %s - %s", msg.Notification.Title, msg.Notification.Body) return nil } body, err := json.Marshal(msg) if err != nil { return fmt.Errorf("failed to marshal FCM message: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", "https://fcm.googleapis.com/fcm/send", nil) if err != nil { return err } req.Header.Set("Authorization", "key="+s.serverKey) req.Header.Set("Content-Type", "application/json") req.Body = http.NoBody // We'd set body here in real implementation resp, err := s.httpClient.Do(req) if err != nil { return fmt.Errorf("FCM request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("FCM returned status %d", resp.StatusCode) } _ = body // Use body in real implementation return nil } // NotifyOrderCreated implements NotificationService for FCM func (s *FCMService) NotifyOrderCreated(ctx context.Context, order *domain.Order, buyer, seller *domain.User) error { // Notify seller if err := s.SendPush(ctx, seller.ID, "πŸ›’ Novo Pedido!", fmt.Sprintf("VocΓͺ recebeu um pedido de R$ %.2f", float64(order.TotalCents)/100), map[string]string{ "type": "new_order", "order_id": order.ID.String(), }, ); err != nil { log.Printf("Error notifying seller: %v", err) } // Notify buyer if err := s.SendPush(ctx, buyer.ID, "βœ… Pedido Confirmado!", fmt.Sprintf("Seu pedido #%s foi recebido", order.ID.String()[:8]), map[string]string{ "type": "order_confirmed", "order_id": order.ID.String(), }, ); err != nil { log.Printf("Error notifying buyer: %v", err) } return nil } // NotifyOrderStatusChanged implements NotificationService for FCM func (s *FCMService) NotifyOrderStatusChanged(ctx context.Context, order *domain.Order, buyer *domain.User) error { statusEmoji := map[string]string{ "Pago": "πŸ’³", "Faturado": "πŸ“„", "Enviado": "🚚", "Entregue": "βœ…", } emoji := statusEmoji[string(order.Status)] if emoji == "" { emoji = "πŸ“¦" } return s.SendPush(ctx, buyer.ID, fmt.Sprintf("%s Pedido Atualizado", emoji), fmt.Sprintf("Seu pedido #%s estΓ‘: %s", order.ID.String()[:8], string(order.Status)), map[string]string{ "type": "order_status", "order_id": order.ID.String(), "status": string(order.Status), }, ) } func min(a, b int) int { if a < b { return a } return b }