// Package notifier provides a lightweight HTTP client for the notification // service. All methods are fire-and-forget: errors are logged but never // propagate to the caller, so a notification failure never breaks a render // flow. package notifier import ( "bytes" "context" "encoding/json" "fmt" "log" "net/http" "time" "github.com/google/uuid" ) // Client sends notifications to the internal notification service endpoint. type Client struct { baseURL string serviceToken string http *http.Client } // New returns a Client pointing at baseURL (e.g. "http://notification-svc:8080") // and authenticating with serviceToken. func New(baseURL, serviceToken string) *Client { return &Client{ baseURL: baseURL, serviceToken: serviceToken, http: &http.Client{Timeout: 5 * time.Second}, } } // createNotificationReq mirrors the notification service's CreateNotificationRequest. type createNotificationReq struct { UserID uuid.UUID `json:"user_id"` TenantID uuid.UUID `json:"tenant_id"` NotificationType string `json:"notification_type"` Priority string `json:"priority"` Title string `json:"title"` Message string `json:"message"` RenderJobID *uuid.UUID `json:"render_job_id,omitempty"` ExportID *uuid.UUID `json:"export_id,omitempty"` ActionURL *string `json:"action_url,omitempty"` ActionText *string `json:"action_text,omitempty"` Channels []string `json:"channels"` } // send posts a notification to the service. Returns an error for caller // awareness, but callers are expected to ignore it. func (c *Client) send(ctx context.Context, req createNotificationReq) error { body, err := json.Marshal(req) if err != nil { return fmt.Errorf("notifier marshal: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/notifications", bytes.NewReader(body)) if err != nil { return fmt.Errorf("notifier new request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+c.serviceToken) resp, err := c.http.Do(httpReq) if err != nil { return fmt.Errorf("notifier send: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("notifier: status %d", resp.StatusCode) } return nil } // NotifyRenderDone fires a RenderCompleted notification. Never blocks the // caller — errors are only logged. func (c *Client) NotifyRenderDone( ctx context.Context, userID, tenantID, jobID uuid.UUID, exportID *uuid.UUID, jobName string, ) { actionURL := "/dashboard/renders/" + jobID.String() actionText := "مشاهده فایل" req := createNotificationReq{ UserID: userID, TenantID: tenantID, NotificationType: "RenderCompleted", Priority: "High", Title: "رندر تکمیل شد 🎉", Message: fmt.Sprintf("پروژه «%s» با موفقیت رندر شد و آماده دانلود است.", jobName), RenderJobID: &jobID, ExportID: exportID, ActionURL: &actionURL, ActionText: &actionText, Channels: []string{"InApp"}, } if err := c.send(ctx, req); err != nil { log.Printf("[notifier] RenderDone job=%s err=%v", jobID, err) } } // NotifyRenderFailed fires a RenderFailed notification. Never blocks the // caller — errors are only logged. func (c *Client) NotifyRenderFailed( ctx context.Context, userID, tenantID, jobID uuid.UUID, jobName, reason string, ) { actionURL := "/dashboard/renders/" + jobID.String() actionText := "جزئیات" var msg string if reason != "" { msg = fmt.Sprintf("رندر پروژه «%s» با خطا مواجه شد: %s", jobName, reason) } else { msg = fmt.Sprintf("رندر پروژه «%s» با خطا مواجه شد. لطفاً دوباره تلاش کنید.", jobName) } req := createNotificationReq{ UserID: userID, TenantID: tenantID, NotificationType: "RenderFailed", Priority: "High", Title: "خطا در رندر", Message: msg, RenderJobID: &jobID, ActionURL: &actionURL, ActionText: &actionText, Channels: []string{"InApp"}, } if err := c.send(ctx, req); err != nil { log.Printf("[notifier] RenderFailed job=%s err=%v", jobID, err) } }