package service import ( "context" "encoding/json" "fmt" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" applogger "github.com/gochat/gochat/pkg/logger" ) // NotificationSubscriptionService manages user push notification subscriptions. // Reference: Chatwoot app/controllers/api/v1/notification_subscriptions_controller.rb // Routes: resource :notification_subscriptions, only: [:create, :destroy] type NotificationSubscriptionService struct { repo *repository.NotificationSubscriptionRepo } func NewNotificationSubscriptionService(repo *repository.NotificationSubscriptionRepo) *NotificationSubscriptionService { return &NotificationSubscriptionService{repo: repo} } // CreateSubscriptionRequest matches Chatwoot's create action payload. type CreateSubscriptionRequest struct { Identifier string `json:"identifier" binding:"required"` SubscriptionAttributes json.RawMessage `json:"subscription_attributes" binding:"required"` SubscriptionType string `json:"subscription_type" binding:"required"` // "browser_push" or "fcm" } // Create adds a new notification subscription for a user. // Chatwoot behavior: validates identifier uniqueness, creates subscription with type enum. func (s *NotificationSubscriptionService) Create(ctx context.Context, userID uint, req *CreateSubscriptionRequest) (*model.NotificationSubscription, error) { // Check for duplicate identifier existing, err := s.repo.FindByIdentifier(ctx, req.Identifier) if err == nil && existing != nil { // Chatwoot allows updating existing subscription with same identifier existing.SubscriptionAttributes = req.SubscriptionAttributes existing.SubscriptionType = model.NotificationSubscriptionTypeFromString(req.SubscriptionType) existing.UserID = userID if err := s.repo.Create(ctx, existing); err != nil { applogger.L().Errorf("NotificationSubscription Create update: %v", err) return nil, err } return existing, nil } subType := model.NotificationSubscriptionTypeFromString(req.SubscriptionType) sub := &model.NotificationSubscription{ Identifier: req.Identifier, SubscriptionAttributes: req.SubscriptionAttributes, SubscriptionType: subType, UserID: userID, } if err := s.repo.Create(ctx, sub); err != nil { applogger.L().Errorf("NotificationSubscription Create: %v", err) return nil, err } return sub, nil } // Destroy removes a notification subscription. // Chatwoot behavior: finds by identifier and deletes. func (s *NotificationSubscriptionService) Destroy(ctx context.Context, userID uint, identifier string) error { // Chatwoot finds by identifier first, then deletes sub, err := s.repo.FindByIdentifier(ctx, identifier) if err != nil { applogger.L().Errorf("NotificationSubscription Destroy find: %v", err) return err } if sub == nil || sub.UserID != userID { return fmt.Errorf("notification subscription not found") } return s.repo.Delete(ctx, sub.ID) } // ListByUser retrieves all subscriptions for a user. func (s *NotificationSubscriptionService) ListByUser(ctx context.Context, userID uint) ([]model.NotificationSubscription, error) { return s.repo.FindByUserID(ctx, userID) } // ValidateSubscriptionAttributes checks that subscription_attributes contains required fields. // For browser_push: endpoint, p256dh, auth keys must be present. // For fcm: token must be present. func ValidateSubscriptionAttributes(subType string, attrs json.RawMessage) error { var m map[string]interface{} if err := json.Unmarshal(attrs, &m); err != nil { return err } switch subType { case "browser_push": if m["endpoint"] == nil || m["p256dh"] == nil || m["auth"] == nil { return fmt.Errorf("missing required field: endpoint, p256dh, auth") } case "fcm": if m["token"] == nil { return fmt.Errorf("missing required field: token") } } return nil }