package notification import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "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 } // 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 } }