package notification import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // HTTPClient defines the interface for HTTP operations type HTTPClient interface { Do(req *http.Request) (*http.Response, error) } // Client represents the notification service HTTP client type Client struct { config *Config httpClient HTTPClient logger Logger } // Logger interface for structured logging type Logger interface { Info(msg string, fields map[string]interface{}) Error(msg string, fields map[string]interface{}) Debug(msg string, fields map[string]interface{}) Warn(msg string, fields map[string]interface{}) } // NewClient creates a new notification client func NewClient(config *Config, logger Logger) (*Client, error) { if config == nil { config = DefaultConfig() } if err := config.Validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) } httpClient := &http.Client{ Timeout: config.Timeout, } return &Client{ config: config, httpClient: httpClient, logger: logger, }, nil } // NewClientWithHTTPClient creates a new notification client with custom HTTP client func NewClientWithHTTPClient(config *Config, httpClient HTTPClient, logger Logger) (*Client, error) { if config == nil { config = DefaultConfig() } if err := config.Validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) } return &Client{ config: config, httpClient: httpClient, logger: logger, }, nil } // SendNotification sends a notification request to the notification service func (c *Client) SendNotification(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) { // Validate request // if err := req.Validate(); err != nil { // return nil, fmt.Errorf("request validation failed: %w", err) // } var lastErr error start := time.Now() for attempt := 0; attempt <= c.config.RetryAttempts; attempt++ { if c.config.EnableLogging && c.logger != nil { c.logger.Debug("Sending notification request", map[string]interface{}{ "attempt": attempt + 1, "event_type": req.EventType, "user_id": req.UserID, "base_url": c.config.BaseURL, }) } response, err := c.sendRequest(ctx, req) if err == nil { if c.config.EnableLogging && c.logger != nil { c.logger.Info("Notification sent successfully", map[string]interface{}{ "event_type": req.EventType, "user_id": req.UserID, "attempts": attempt + 1, "elapsed_time": time.Since(start).String(), }) } return response, nil } lastErr = err // Log retry attempt if c.config.EnableLogging && c.logger != nil { c.logger.Warn("Notification attempt failed", map[string]interface{}{ "attempt": attempt + 1, "error": err.Error(), "event_type": req.EventType, "user_id": req.UserID, }) } // Don't retry on certain errors if !c.shouldRetry(err) { break } // Don't sleep on last attempt if attempt < c.config.RetryAttempts { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(c.config.RetryDelay): // Continue to next attempt } } } if c.config.EnableLogging && c.logger != nil { c.logger.Error("All notification attempts failed", map[string]interface{}{ "attempts": c.config.RetryAttempts + 1, "elapsed_time": time.Since(start).String(), "last_error": lastErr.Error(), "event_type": req.EventType, "user_id": req.UserID, }) } return nil, ErrRetryExhausted{ Attempts: c.config.RetryAttempts + 1, LastError: lastErr, ElapsedTime: time.Since(start).String(), } } // sendRequest performs the actual HTTP request func (c *Client) sendRequest(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) { // Marshal request body requestBody, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } // Create HTTP request url := fmt.Sprintf("%s/api/v1/notifications", c.config.BaseURL) httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(requestBody)) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } // Set headers httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("User-Agent", c.config.UserAgent) // Send request resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } defer resp.Body.Close() // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Handle non-2xx status codes if resp.StatusCode < 200 || resp.StatusCode >= 300 { // Try to parse error response var errorResp NotificationErrorResponse if json.Unmarshal(body, &errorResp) == nil && errorResp.Error != "" { return nil, MapHTTPError(resp.StatusCode, errorResp.Error) } return nil, MapHTTPError(resp.StatusCode, string(body)) } // Parse success response var notificationResp NotificationResponse if err := json.Unmarshal(body, ¬ificationResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return ¬ificationResp, nil } // GetNotifications retrieves a list of notifications from the notification service func (c *Client) GetNotifications(ctx context.Context, req *GetNotificationsRequest) (*NotificationListResponse, error) { if c.config.EnableLogging && c.logger != nil { c.logger.Debug("Getting notifications", map[string]interface{}{ "status": req.Status, "method": req.Method, "event_type": req.EventType, "type": req.Type, "user_id": req.UserID, "seen": req.Seen, "priority": req.Priority, "base_url": c.config.BaseURL, }) } // Build query parameters queryParams := c.buildQueryParams(req) // Create HTTP request url := fmt.Sprintf("%s/api/v1/notifications?%s", c.config.BaseURL, queryParams) httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } // Set headers httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("User-Agent", c.config.UserAgent) // Send request resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } defer resp.Body.Close() // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Handle non-2xx status codes if resp.StatusCode < 200 || resp.StatusCode >= 300 { // Try to parse error response var errorResp NotificationErrorResponse if json.Unmarshal(body, &errorResp) == nil && errorResp.Error != "" { return nil, MapHTTPError(resp.StatusCode, errorResp.Error) } return nil, MapHTTPError(resp.StatusCode, string(body)) } // Parse success response var notificationsResp NotificationListResponse if err := json.Unmarshal(body, ¬ificationsResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if c.config.EnableLogging && c.logger != nil { c.logger.Info("Notifications retrieved successfully", map[string]interface{}{ "count": len(notificationsResp.Data), "total": notificationsResp.Pagination.Total, "page": notificationsResp.Pagination.CurrentPage, "total_pages": notificationsResp.Pagination.TotalPages, }) } return ¬ificationsResp, nil } // GetNotification retrieves a single notification by ID from the notification service func (c *Client) GetNotification(ctx context.Context, notificationID string) (*DetailedNotificationResponse, error) { if notificationID == "" { return nil, fmt.Errorf("notification ID is required") } if c.config.EnableLogging && c.logger != nil { c.logger.Debug("Getting notification", map[string]interface{}{ "notification_id": notificationID, "base_url": c.config.BaseURL, }) } // Create HTTP request url := fmt.Sprintf("%s/api/v1/notifications/view/%s", c.config.BaseURL, notificationID) httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } // Set headers httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("User-Agent", c.config.UserAgent) // Send request resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } defer resp.Body.Close() // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Handle non-2xx status codes if resp.StatusCode < 200 || resp.StatusCode >= 300 { // Try to parse error response var errorResp NotificationErrorResponse if json.Unmarshal(body, &errorResp) == nil && errorResp.Error != "" { return nil, MapHTTPError(resp.StatusCode, errorResp.Error) } return nil, MapHTTPError(resp.StatusCode, string(body)) } // Parse success response var notificationResp DetailedNotificationResponse if err := json.Unmarshal(body, ¬ificationResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if c.config.EnableLogging && c.logger != nil { c.logger.Info("Notification retrieved successfully", map[string]interface{}{ "notification_id": notificationResp.Data.ID, "user_id": notificationResp.Data.UserID, "type": notificationResp.Data.Type, }) } return ¬ificationResp, nil } // buildQueryParams builds query parameters from the request func (c *Client) buildQueryParams(req *GetNotificationsRequest) string { params := make([]string, 0) if req.Status != "" { params = append(params, fmt.Sprintf("status=%s", req.Status)) } if req.Method != "" { params = append(params, fmt.Sprintf("method=%s", req.Method)) } if req.EventType != "" { params = append(params, fmt.Sprintf("event_type=%s", req.EventType)) } if req.Type != "" { params = append(params, fmt.Sprintf("type=%s", req.Type)) } if req.UserID != "" { params = append(params, fmt.Sprintf("user_id=%s", req.UserID)) } if req.Page > 0 { params = append(params, fmt.Sprintf("page=%d", req.Page)) } if req.PerPage > 0 { params = append(params, fmt.Sprintf("per_page=%d", req.PerPage)) } if req.Seen != nil { params = append(params, fmt.Sprintf("seen=%t", *req.Seen)) } if req.Priority != "" { params = append(params, fmt.Sprintf("priority=%s", req.Priority)) } // Join parameters with & if len(params) == 0 { return "" } result := strings.Join(params, "&") return result } // MarkSeen marks a notification as seen func (c *Client) MarkSeen(ctx context.Context, notificationID string) (*MarkSeenResponse, error) { if c.config.EnableLogging && c.logger != nil { c.logger.Debug("Marking notification as seen", map[string]interface{}{ "notification_id": notificationID, "base_url": c.config.BaseURL, }) } // Create HTTP request url := fmt.Sprintf("%s/api/v1/notifications/%s/seen", c.config.BaseURL, notificationID) httpReq, err := http.NewRequestWithContext(ctx, "PUT", url, nil) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } // Set headers httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("User-Agent", c.config.UserAgent) // Send request resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } defer resp.Body.Close() // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Handle non-2xx status codes if resp.StatusCode < 200 || resp.StatusCode >= 300 { // Try to parse error response var errorResp NotificationErrorResponse if json.Unmarshal(body, &errorResp) == nil && errorResp.Error != "" { return nil, MapHTTPError(resp.StatusCode, errorResp.Error) } return nil, MapHTTPError(resp.StatusCode, string(body)) } // Parse success response var markSeenResp MarkSeenResponse if err := json.Unmarshal(body, &markSeenResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if c.config.EnableLogging && c.logger != nil { c.logger.Info("Notification marked as seen successfully", map[string]interface{}{ "notification_id": notificationID, "status": markSeenResp.Status, }) } return &markSeenResp, nil } // MarkSeen marks a notification as seen func (c *Client) AllMarkSeen(ctx context.Context, userID string) (*MarkSeenResponse, error) { if c.config.EnableLogging && c.logger != nil { c.logger.Debug("Marking notification as seen", map[string]interface{}{ "user_id": userID, "base_url": c.config.BaseURL, }) } // Create HTTP request url := fmt.Sprintf("%s/api/v1/notifications/seen-all/%s", c.config.BaseURL, userID) httpReq, err := http.NewRequestWithContext(ctx, "PUT", url, nil) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } // Set headers httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("User-Agent", c.config.UserAgent) // Send request resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } defer resp.Body.Close() // Read response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } // Handle non-2xx status codes if resp.StatusCode < 200 || resp.StatusCode >= 300 { // Try to parse error response var errorResp NotificationErrorResponse if json.Unmarshal(body, &errorResp) == nil && errorResp.Error != "" { return nil, MapHTTPError(resp.StatusCode, errorResp.Error) } return nil, MapHTTPError(resp.StatusCode, string(body)) } // Parse success response var markSeenResp MarkSeenResponse if err := json.Unmarshal(body, &markSeenResp); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } if c.config.EnableLogging && c.logger != nil { c.logger.Info("Notification marked as seen successfully", map[string]interface{}{ "user_id": userID, "status": markSeenResp.Status, }) } return &markSeenResp, nil } // shouldRetry determines if a request should be retried based on the error func (c *Client) shouldRetry(err error) bool { switch e := err.(type) { case ErrHTTP: // Retry on server errors (5xx) and certain client errors return e.StatusCode >= 500 || e.StatusCode == http.StatusTooManyRequests default: // Retry on network errors, timeouts, etc. return true } }