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"` SubscriptionAttributes json.RawMessage `json:"subscription_attributes"` SubscriptionType string `json:"subscription_type"` // "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) { if req.SubscriptionType == "" { return nil, fmt.Errorf("subscription_type is required") } if len(req.SubscriptionAttributes) == 0 { return nil, fmt.Errorf("subscription_attributes is required") } identifier, err := notificationSubscriptionIdentifier(req) if err != nil { return nil, err } req.Identifier = identifier // 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.Save(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 { sub, err := s.repo.FindByUserIDAndPushToken(ctx, userID, identifier) if err != nil { return nil } if sub == nil { return nil } 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 && m["device_id"] == nil && m["push_token"] == nil { return fmt.Errorf("missing required field: token") } } return nil } func notificationSubscriptionIdentifier(req *CreateSubscriptionRequest) (string, error) { if req.Identifier != "" { return req.Identifier, nil } var attrs map[string]any if err := json.Unmarshal(req.SubscriptionAttributes, &attrs); err != nil { return "", err } switch req.SubscriptionType { case "browser_push": if endpoint, _ := attrs["endpoint"].(string); endpoint != "" { return endpoint, nil } case "fcm": if deviceID, _ := attrs["device_id"].(string); deviceID != "" { return deviceID, nil } if pushToken, _ := attrs["push_token"].(string); pushToken != "" { return pushToken, nil } } return "", fmt.Errorf("identifier is required") }