package processor import ( "context" "fmt" "net/url" "eslogad-be/internal/config" "eslogad-be/internal/contract" "eslogad-be/internal/entities" "github.com/google/uuid" novu "github.com/novuhq/go-novu/lib" ) type NotificationProcessor interface { // User management CreateSubscriber(ctx context.Context, user *entities.User) error UpdateSubscriber(ctx context.Context, user *entities.User) error DeleteSubscriber(ctx context.Context, userID uuid.UUID) error CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error BulkCreateSubscribers(ctx context.Context, users []*entities.User) error // Letter notifications SendIncomingLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error SendOutgoingLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error } type NotificationProcessorImpl struct { provider NotificationProvider workflowID string } func NewNotificationProcessor(provider NotificationProvider, workflowID string) *NotificationProcessorImpl { return &NotificationProcessorImpl{ provider: provider, workflowID: workflowID, } } func (p *NotificationProcessorImpl) CreateSubscriber(ctx context.Context, user *entities.User) error { return p.provider.CreateSubscriber(ctx, user) } func (p *NotificationProcessorImpl) UpdateSubscriber(ctx context.Context, user *entities.User) error { return p.provider.UpdateSubscriber(ctx, user) } func (p *NotificationProcessorImpl) DeleteSubscriber(ctx context.Context, userID uuid.UUID) error { return p.provider.DeleteSubscriber(ctx, userID) } func (p *NotificationProcessorImpl) CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error { return p.provider.CreateSubscriberFromContract(ctx, user) } func (p *NotificationProcessorImpl) BulkCreateSubscribers(ctx context.Context, users []*entities.User) error { return p.provider.BulkCreateSubscribers(ctx, users) } func (p *NotificationProcessorImpl) SendIncomingLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error { // Ensure subscriber exists if err := p.provider.EnsureSubscriberExists(ctx, recipientUserID); err != nil { return fmt.Errorf("failed to ensure subscriber exists: %w", err) } // Build notification URL url := fmt.Sprintf("/en/apps/surat-menyurat/masuk-detail/%s", letterID.String()) // Use workflow ID from config (defaults to "notification-dashbpard") workflowID := p.workflowID if workflowID == "" { workflowID = "notification-dashbpard" } // Send notification return p.provider.SendNotification(ctx, NotificationPayload{ RecipientID: recipientUserID, EventName: workflowID, Data: map[string]interface{}{ "subject": subject, "body": body, "url": url, }, }) } // NotificationProvider interface for different notification services type NotificationProvider interface { // User management CreateSubscriber(ctx context.Context, user *entities.User) error UpdateSubscriber(ctx context.Context, user *entities.User) error DeleteSubscriber(ctx context.Context, userID uuid.UUID) error CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error BulkCreateSubscribers(ctx context.Context, users []*entities.User) error // Core notification methods EnsureSubscriberExists(ctx context.Context, userID uuid.UUID) error SendNotification(ctx context.Context, payload NotificationPayload) error } type NotificationPayload struct { RecipientID uuid.UUID EventName string Data map[string]interface{} } // NovuProvider implements NotificationProvider using Novu type NovuProvider struct { client *novu.APIClient config *config.NovuConfig } func NewNovuProvider(cfg *config.NovuConfig) *NovuProvider { if cfg.APIKey == "" { return &NovuProvider{ client: nil, config: cfg, } } // Create Novu config with backend URL novuConfig := &novu.Config{} if cfg.BaseURL != "" { backendURL, err := url.Parse(cfg.BaseURL) if err == nil { novuConfig.BackendURL = backendURL } } client := novu.NewAPIClient(cfg.APIKey, novuConfig) return &NovuProvider{ client: client, config: cfg, } } func (p *NovuProvider) CreateSubscriber(ctx context.Context, user *entities.User) error { if p.client == nil { return fmt.Errorf("novu client not initialized") } subscriberID := user.ID.String() data := map[string]interface{}{ "userId": user.ID.String(), "email": user.Email, "isActive": user.IsActive, "createdAt": user.CreatedAt, } if user.Departments != nil && len(user.Departments) > 0 { depts := make([]map[string]interface{}, len(user.Departments)) for i, dept := range user.Departments { depts[i] = map[string]interface{}{ "id": dept.ID.String(), "name": dept.Name, "code": dept.Code, } } data["departments"] = depts } subscriber := novu.SubscriberPayload{ Email: user.Email, FirstName: user.Name, LastName: "", Phone: "", Avatar: "", Data: data, } _, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber) if err != nil { return fmt.Errorf("failed to create subscriber: %w", err) } return nil } func (p *NovuProvider) UpdateSubscriber(ctx context.Context, user *entities.User) error { if p.client == nil { return fmt.Errorf("novu client not initialized") } subscriberID := user.ID.String() data := map[string]interface{}{ "userId": user.ID.String(), "email": user.Email, "isActive": user.IsActive, "updatedAt": user.UpdatedAt, } if user.Departments != nil && len(user.Departments) > 0 { depts := make([]map[string]interface{}, len(user.Departments)) for i, dept := range user.Departments { depts[i] = map[string]interface{}{ "id": dept.ID.String(), "name": dept.Name, "code": dept.Code, } } data["departments"] = depts } updateData := novu.SubscriberPayload{ Email: user.Email, FirstName: user.Name, LastName: "", Phone: "", Avatar: "", Data: data, } _, err := p.client.SubscriberApi.Update(ctx, subscriberID, updateData) if err != nil { return fmt.Errorf("failed to update subscriber: %w", err) } return nil } func (p *NovuProvider) DeleteSubscriber(ctx context.Context, userID uuid.UUID) error { if p.client == nil { return fmt.Errorf("novu client not initialized") } subscriberID := userID.String() _, err := p.client.SubscriberApi.Delete(ctx, subscriberID) if err != nil { return fmt.Errorf("failed to delete subscriber: %w", err) } return nil } func (p *NovuProvider) CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error { if p.client == nil { return fmt.Errorf("novu client not initialized") } subscriberID := user.ID.String() data := map[string]interface{}{ "userId": user.ID.String(), "email": user.Email, "isActive": user.IsActive, "createdAt": user.CreatedAt, } if user.Roles != nil && len(user.Roles) > 0 { roles := make([]map[string]interface{}, len(user.Roles)) for i, role := range user.Roles { roles[i] = map[string]interface{}{ "id": role.ID.String(), "name": role.Name, "code": role.Code, } } data["roles"] = roles } if user.DepartmentResponse != nil && len(user.DepartmentResponse) > 0 { depts := make([]map[string]interface{}, len(user.DepartmentResponse)) for i, dept := range user.DepartmentResponse { depts[i] = map[string]interface{}{ "id": dept.ID.String(), "name": dept.Name, "code": dept.Code, } } data["departments"] = depts } subscriber := novu.SubscriberPayload{ Email: user.Email, FirstName: user.Name, LastName: "", Phone: "", Avatar: "", Data: data, } _, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber) if err != nil { return fmt.Errorf("failed to create subscriber from contract: %w", err) } return nil } func (p *NovuProvider) BulkCreateSubscribers(ctx context.Context, users []*entities.User) error { if p.client == nil { return fmt.Errorf("novu client not initialized") } var lastErr error successCount := 0 for _, user := range users { err := p.CreateSubscriber(ctx, user) if err != nil { lastErr = err continue } successCount++ } if lastErr != nil && successCount == 0 { return fmt.Errorf("failed to create any subscribers, last error: %w", lastErr) } if lastErr != nil { return fmt.Errorf("created %d out of %d subscribers, last error: %w", successCount, len(users), lastErr) } return nil } func (p *NovuProvider) EnsureSubscriberExists(ctx context.Context, userID uuid.UUID) error { if p.client == nil { return fmt.Errorf("novu client not initialized") } subscriberID := userID.String() // Check if subscriber exists _, err := p.client.SubscriberApi.Get(ctx, subscriberID) if err != nil { // Subscriber doesn't exist, create a basic one subscriber := novu.SubscriberPayload{ Email: fmt.Sprintf("%s@placeholder.com", subscriberID), } _, err = p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber) if err != nil { return fmt.Errorf("failed to ensure subscriber exists: %w", err) } } return nil } func (p *NovuProvider) SendNotification(ctx context.Context, payload NotificationPayload) error { if p.client == nil { return fmt.Errorf("novu client not initialized") } triggerPayload := novu.ITriggerPayloadOptions{ To: payload.RecipientID.String(), Payload: payload.Data, } _, err := p.client.EventApi.Trigger(ctx, payload.EventName, triggerPayload) if err != nil { return fmt.Errorf("failed to send notification: %w", err) } return nil } func (p *NotificationProcessorImpl) SendOutgoingLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error { // Ensure subscriber exists if err := p.provider.EnsureSubscriberExists(ctx, recipientUserID); err != nil { return fmt.Errorf("failed to ensure subscriber exists: %w", err) } // Build notification URL for outgoing letters url := fmt.Sprintf("/en/apps/surat-menyurat/keluar-detail/%s", letterID.String()) // Use workflow ID from config (defaults to "notification-dashbpard") workflowID := p.workflowID if workflowID == "" { workflowID = "notification-dashbpard" } // Send notification return p.provider.SendNotification(ctx, NotificationPayload{ RecipientID: recipientUserID, EventName: workflowID, Data: map[string]interface{}{ "subject": subject, "body": body, "url": url, }, }) }