dukcapil/internal/processor/notification_processor.go
2025-09-21 14:39:02 +07:00

390 lines
10 KiB
Go

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,
},
})
}