diff --git a/cmd/web/bootstrap/bootstrap.go b/cmd/web/bootstrap/bootstrap.go index a4e75cd..faf9c87 100644 --- a/cmd/web/bootstrap/bootstrap.go +++ b/cmd/web/bootstrap/bootstrap.go @@ -7,6 +7,7 @@ import ( "tm/pkg/config" "tm/pkg/logger" "tm/pkg/mongo" + "tm/pkg/notification" "tm/pkg/redis" "github.com/labstack/echo/v4" @@ -175,3 +176,34 @@ func InitAuthorizationService(conf JWTConfig, redisClient redis.Client, log logg return authService } + +// Init Notification Service +func InitNotificationService(conf config.NotificationConfig, log logger.Logger) notification.SDK { + notificationConfig := ¬ification.Config{ + BaseURL: conf.BaseURL, + Timeout: conf.Timeout, + RetryAttempts: conf.RetryAttempts, + RetryDelay: conf.RetryDelay, + EnableLogging: conf.EnableLogging, + UserAgent: conf.UserAgent, + } + + notificationSDK, err := notification.New(notificationConfig, log) + if err != nil { + log.Error("Failed to initialize Notification SDK", map[string]interface{}{ + "error": err.Error(), + }) + fmt.Printf("Failed to initialize Notification SDK: %v\n", err.Error()) + } + + log.Info("Notification SDK initialized successfully", map[string]interface{}{ + "base_url": conf.BaseURL, + "timeout": conf.Timeout, + "retry_attempts": conf.RetryAttempts, + "retry_delay": conf.RetryDelay, + "enable_logging": conf.EnableLogging, + "user_agent": conf.UserAgent, + }) + + return *notificationSDK +} diff --git a/cmd/web/bootstrap/config.go b/cmd/web/bootstrap/config.go index 152188c..0f9f9b7 100644 --- a/cmd/web/bootstrap/config.go +++ b/cmd/web/bootstrap/config.go @@ -12,6 +12,7 @@ type Config struct { Logging config.LoggingConfig RateLimit config.RateLimitConfig Assets config.AssetsConfig + Notification config.NotificationConfig UserAuth AuthConfig CustomerAuth AuthConfig } diff --git a/cmd/web/main.go b/cmd/web/main.go index 10e7c2a..aa04103 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -122,6 +122,9 @@ func main() { } }() + // Initialize notification service + notificationSDK := bootstrap.InitNotificationService(conf.Notification, logger) + // Initialize authorization service userAuthService := bootstrap.InitAuthorizationService(conf.UserAuth.JWT, redisClient, logger) customerAuthService := bootstrap.InitAuthorizationService(conf.CustomerAuth.JWT, redisClient, logger) @@ -143,10 +146,10 @@ func main() { userValidator := user.NewValidationService() // Initialize services with repositories - userService := user.NewUserService(userRepository, logger, userAuthService, userValidator) + userService := user.NewUserService(userRepository, logger, userAuthService, userValidator, notificationSDK) categoryService := company_category.NewCategoryService(categoryRepository, logger) companyService := company.NewCompanyService(companyRepository, categoryService, logger) - customerService := customer.New(customerRepository, logger, customerAuthService, companyService, redisClient) + customerService := customer.New(customerRepository, logger, customerAuthService, companyService, redisClient, notificationSDK) tenderService := tender.NewTenderService(tenderRepository, companyService, logger) feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger) tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger) diff --git a/internal/customer/service.go b/internal/customer/service.go index b8ce9c4..dcf2ff2 100644 --- a/internal/customer/service.go +++ b/internal/customer/service.go @@ -7,10 +7,12 @@ import ( "errors" "fmt" "math/big" + "strings" "time" "tm/internal/company" "tm/pkg/logger" + "tm/pkg/notification" "tm/pkg/redis" "tm/pkg/response" @@ -49,16 +51,18 @@ type customerService struct { authService authorization.AuthorizationService companyService company.Service redisClient redis.Client + notification notification.SDK } // New creates a new customer service -func New(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client) Service { +func New(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK) Service { return &customerService{ repository: repository, logger: logger, authService: authService, companyService: companyService, redisClient: redisClient, + notification: notificationSDK, } } @@ -125,6 +129,8 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm "company_ids": companyIDs, }) + go s.notification.SendEmail(ctx, customer.Email, fmt.Sprintf("Welcome to Tender Management\nUsername: %s\nPassword: %s", customer.Username, form.Password), customer.ID.Hex()) + return customer.ToResponse(nil), nil } @@ -293,7 +299,7 @@ func (s *customerService) Delete(ctx context.Context, id string) error { // UpdateStatus updates customer status func (s *customerService) UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error { // Get customer to check if exists - _, err := s.repository.GetByID(ctx, id) + customer, err := s.repository.GetByID(ctx, id) if err != nil { return errors.New("customer not found") } @@ -315,6 +321,8 @@ func (s *customerService) UpdateStatus(ctx context.Context, id string, form *Upd "status": form.Status, }) + go s.notification.SendEmail(ctx, customer.Email, fmt.Sprintf("Your account has been %s", strings.ToUpper(form.Status)), customer.ID.Hex()) + return nil } @@ -769,14 +777,7 @@ func (s *customerService) RequestResetPassword(ctx context.Context, form *Reques } // Send OTP via email - // err = s.emailService.SendPasswordResetOTP(ctx, form.Email, otpCode) - // if err != nil { - // s.logger.Error("Failed to send password reset OTP", map[string]interface{}{ - // "error": err.Error(), - // "email": form.Email, - // }) - // return nil, errors.New("failed to send verification code") - // } + go s.notification.SendEmail(ctx, form.Email, fmt.Sprintf("Your password reset code is: %s", otpCode), customer.ID.Hex()) s.logger.Info("Password reset OTP sent successfully", map[string]interface{}{ "email": form.Email, @@ -964,6 +965,8 @@ func (s *customerService) ResetPassword(ctx context.Context, form *ResetPassword "customer_id": customer.ID, }) + go s.notification.SendEmail(ctx, tokenData.Email, "Your password has been reset successfully", customer.ID.Hex()) + return &ResetPasswordResponse{ Message: "Password reset successfully", Success: true, diff --git a/internal/user/service.go b/internal/user/service.go index ac3623e..cfe70cb 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -3,10 +3,12 @@ package user import ( "context" "errors" + "fmt" "strings" "time" "tm/pkg/authorization" "tm/pkg/logger" + "tm/pkg/notification" "tm/pkg/response" "go.mongodb.org/mongo-driver/v2/bson" @@ -34,19 +36,21 @@ type Service interface { // userService implements the Service interface type userService struct { - repository Repository - logger logger.Logger - authService authorization.AuthorizationService - validator ValidationService + repository Repository + logger logger.Logger + authService authorization.AuthorizationService + validator ValidationService + notification notification.SDK } // NewUserService creates a new user service -func NewUserService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, validator ValidationService) Service { +func NewUserService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, validator ValidationService, notificationSDK notification.SDK) Service { return &userService{ - repository: repository, - logger: logger, - authService: authService, - validator: validator, + repository: repository, + logger: logger, + authService: authService, + validator: validator, + notification: notificationSDK, } } @@ -112,6 +116,8 @@ func (s *userService) Register(ctx context.Context, form *CreateUserForm) (*User "role": user.Role, }) + go s.notification.SendEmail(ctx, form.Email, fmt.Sprintf("Welcome to Tender Management\nUsername: %s\nPassword: %s", form.Username, form.Password), user.ID.Hex()) + return user.ToResponse(), nil } @@ -214,6 +220,8 @@ func (s *userService) UpdateStatus(ctx context.Context, id string, form *UpdateU "status": form.Status, }) + go s.notification.SendEmail(ctx, user.Email, fmt.Sprintf("Your account has been %s", strings.ToUpper(form.Status)), user.ID.Hex()) + return nil } @@ -442,6 +450,8 @@ func (s *userService) ChangePassword(ctx context.Context, userID string, form *C "user_id": userID, }) + go s.notification.SendEmail(ctx, user.Email, "Your password has been reset successfully", user.ID.Hex()) + return nil } diff --git a/pkg/config/config.go b/pkg/config/config.go index dc178e6..85ce26f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -65,6 +65,15 @@ type RateLimitConfig struct { Burst int `env:"RATE_LIMIT_BURST"` } +type NotificationConfig struct { + BaseURL string `env:"NOTIFICATION_BASE_URL"` + Timeout time.Duration `env:"NOTIFICATION_TIMEOUT"` + RetryAttempts int `env:"NOTIFICATION_RETRY_ATTEMPTS"` + RetryDelay time.Duration `env:"NOTIFICATION_RETRY_DELAY"` + EnableLogging bool `env:"NOTIFICATION_ENABLE_LOGGING"` + UserAgent string `env:"NOTIFICATION_USER_AGENT"` +} + // LoadConfig loads configuration with priority: OS environment variables > .env file // OS environment variables take precedence over .env file values func LoadConfig[T any](path string, config T) (T, error) { diff --git a/pkg/notification/README.md b/pkg/notification/README.md new file mode 100644 index 0000000..4462d16 --- /dev/null +++ b/pkg/notification/README.md @@ -0,0 +1,400 @@ +# Notification SDK + +A comprehensive Go SDK for interacting with the Tender Management notification service. This SDK provides a clean, type-safe interface for sending various types of notifications including emails, SMS, push notifications, and OTP messages. + +## Features + +- **Multiple Notification Types**: Support for EMAIL, SMS, PUSH, and OTP notifications +- **Automatic Retries**: Configurable retry mechanism with exponential backoff +- **Structured Logging**: Built-in logging with structured fields +- **Fluent API**: Builder pattern for constructing complex notifications +- **Batch Operations**: Send multiple notifications efficiently +- **Type Safety**: Full type safety with comprehensive validation +- **Error Handling**: Detailed error types and proper error wrapping +- **Configuration**: Flexible configuration via environment variables or code +- **Health Checks**: Built-in health checking capabilities + +## Installation + +```bash +go get your-module/pkg/notification +``` + +## Quick Start + +### Basic Usage + +```go +package main + +import ( + "context" + "log" + "your-module/pkg/notification" + "your-module/pkg/logger" // Your logger implementation +) + +func main() { + // Create configuration + config := notification.DefaultConfig() + config.BaseURL = "http://127.0.0.1:9095" // Your notification service URL + + // Create logger (implement notification.Logger interface) + logger := logger.New() // Your logger implementation + + // Create SDK + sdk, err := notification.NewSDK(config, logger) + if err != nil { + log.Fatal("Failed to create notification SDK:", err) + } + + ctx := context.Background() + + // Send a simple email + err = sdk.QuickEmail(ctx, "user@example.com", "Hello World!") + if err != nil { + log.Printf("Failed to send email: %v", err) + } +} +``` + +### Environment Configuration + +Set environment variables for automatic configuration: + +```bash +export NOTIFICATION_BASE_URL="http://127.0.0.1:9095" +export NOTIFICATION_TIMEOUT="30s" +export NOTIFICATION_RETRY_ATTEMPTS="3" +export NOTIFICATION_RETRY_DELAY="2s" +export NOTIFICATION_ENABLE_LOGGING="true" +export NOTIFICATION_USER_AGENT="MyApp-NotificationSDK/1.0" +``` + +Then load configuration: + +```go +import "your-module/pkg/config" + +// Load configuration from environment +cfg, err := config.LoadConfig(".", ¬ification.Config{}) +if err != nil { + log.Fatal("Failed to load config:", err) +} + +sdk, err := notification.NewSDK(cfg, logger) +``` + +## API Reference + +### Configuration + +```go +type Config struct { + BaseURL string `env:"NOTIFICATION_BASE_URL"` + Timeout time.Duration `env:"NOTIFICATION_TIMEOUT"` + RetryAttempts int `env:"NOTIFICATION_RETRY_ATTEMPTS"` + RetryDelay time.Duration `env:"NOTIFICATION_RETRY_DELAY"` + EnableLogging bool `env:"NOTIFICATION_ENABLE_LOGGING"` + UserAgent string `env:"NOTIFICATION_USER_AGENT"` +} +``` + +### Creating SDK Instance + +```go +// With default configuration +sdk, err := notification.NewSDK(nil, logger) + +// With custom configuration +config := ¬ification.Config{ + BaseURL: "http://localhost:9095", + Timeout: 30 * time.Second, + RetryAttempts: 3, + RetryDelay: 2 * time.Second, + EnableLogging: true, + UserAgent: "MyApp/1.0", +} +sdk, err := notification.NewSDK(config, logger) + +// With custom HTTP client +httpClient := &http.Client{Timeout: 10 * time.Second} +sdk, err := notification.NewSDKWithClient(config, httpClient, logger) +``` + +### Sending Notifications + +#### Simple Notifications + +```go +ctx := context.Background() + +// Email +response, err := sdk.SendEmail(ctx, "user@example.com", "Welcome!", "user123") + +// SMS +response, err := sdk.SendSMS(ctx, "+1234567890", "Your code: 1234", "user123") + +// Push Notification +response, err := sdk.SendPush(ctx, "device_token_here", "New message!", "user123") + +// OTP +response, err := sdk.SendOTP(ctx, "+1234567890", "Your OTP: 5678", "user123") +``` + +#### Quick Notifications (without user ID) + +```go +// Quick methods for simple use cases +err := sdk.QuickEmail(ctx, "user@example.com", "Hello!") +err := sdk.QuickSMS(ctx, "+1234567890", "Hello!") +err := sdk.QuickPush(ctx, "device_token", "Hello!") +err := sdk.QuickOTP(ctx, "+1234567890", "Code: 1234") +``` + +#### Custom Notifications + +```go +request := ¬ification.NotificationRequest{ + EventType: notification.EventTypeEmail, + Message: "Custom notification message", + Methods: notification.NotificationMethods{ + Email: "user@example.com", + SMS: "+1234567890", // Optional: multiple delivery methods + }, + UserID: "user123", +} + +response, err := sdk.SendNotification(ctx, request) +``` + +### Fluent Builder API + +```go +// Build and send notification using fluent API +response, err := sdk.NewBuilder(). + SetEventType(notification.EventTypeEmail). + SetMessage("Welcome to our service!"). + SetEmail("user@example.com"). + SetUserID("user123"). + Send(ctx) + +// Build without sending +request, err := sdk.NewBuilder(). + SetEventType(notification.EventTypeSMS). + SetMessage("Your verification code: 1234"). + SetSMS("+1234567890"). + SetUserID("user123"). + Build() + +if err == nil { + response, err := sdk.SendNotification(ctx, request) +} +``` + +### Batch Operations + +```go +// Prepare multiple requests +requests := []*notification.NotificationRequest{ + { + EventType: notification.EventTypeEmail, + Message: "Welcome email", + Methods: notification.NotificationMethods{Email: "user1@example.com"}, + UserID: "user1", + }, + { + EventType: notification.EventTypeSMS, + Message: "Welcome SMS", + Methods: notification.NotificationMethods{SMS: "+1234567890"}, + UserID: "user2", + }, +} + +// Send batch +batchResponse, err := sdk.SendBatch(ctx, requests) +if err != nil { + log.Printf("Batch failed: %v", err) +} else { + log.Printf("Batch completed: %d success, %d failed", + batchResponse.Success, batchResponse.Failed) + + // Check individual results + for _, resp := range batchResponse.Responses { + if !resp.Success { + log.Printf("Request %d failed: %s", resp.Index, resp.Error) + } + } +} +``` + +### Health Checks + +```go +// Check if notification service is available +err := sdk.Health(ctx) +if err != nil { + log.Printf("Notification service is unhealthy: %v", err) +} else { + log.Println("Notification service is healthy") +} +``` + +## Event Types + +The SDK supports the following notification types: + +- `notification.EventTypeEmail` - Email notifications +- `notification.EventTypeSMS` - SMS notifications +- `notification.EventTypePush` - Push notifications +- `notification.EventTypeOTP` - OTP notifications + +## Error Handling + +The SDK provides detailed error types for different scenarios: + +```go +response, err := sdk.SendEmail(ctx, "invalid-email", "Hello!", "user123") +if err != nil { + switch e := err.(type) { + case notification.ErrHTTP: + log.Printf("HTTP error %d: %s", e.StatusCode, e.Message) + case notification.ErrValidation: + log.Printf("Validation error in field %s: %s", e.Field, e.Message) + case notification.ErrRetryExhausted: + log.Printf("Retry exhausted after %d attempts: %v", e.Attempts, e.LastError) + case notification.ErrInvalidConfig: + log.Printf("Configuration error in field %s: %s", e.Field, e.Message) + default: + log.Printf("Unknown error: %v", err) + } +} +``` + +## Logging + +The SDK requires a logger that implements the `notification.Logger` interface: + +```go +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{}) +} +``` + +Example logger implementation with logrus: + +```go +import "github.com/sirupsen/logrus" + +type LogrusLogger struct { + logger *logrus.Logger +} + +func (l *LogrusLogger) Info(msg string, fields map[string]interface{}) { + l.logger.WithFields(fields).Info(msg) +} + +func (l *LogrusLogger) Error(msg string, fields map[string]interface{}) { + l.logger.WithFields(fields).Error(msg) +} + +func (l *LogrusLogger) Debug(msg string, fields map[string]interface{}) { + l.logger.WithFields(fields).Debug(msg) +} + +func (l *LogrusLogger) Warn(msg string, fields map[string]interface{}) { + l.logger.WithFields(fields).Warn(msg) +} +``` + +## Testing + +The SDK is designed to be easily testable. You can mock the HTTP client: + +```go +import ( + "testing" + "net/http" + "your-module/pkg/notification" +) + +// Mock HTTP client +type MockHTTPClient struct { + DoFunc func(req *http.Request) (*http.Response, error) +} + +func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { + return m.DoFunc(req) +} + +// Mock logger +type MockLogger struct{} +func (l *MockLogger) Info(msg string, fields map[string]interface{}) {} +func (l *MockLogger) Error(msg string, fields map[string]interface{}) {} +func (l *MockLogger) Debug(msg string, fields map[string]interface{}) {} +func (l *MockLogger) Warn(msg string, fields map[string]interface{}) {} + +func TestSendEmail(t *testing.T) { + mockClient := &MockHTTPClient{ + DoFunc: func(req *http.Request) (*http.Response, error) { + // Return mock response + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(`{"status":"Notification sent"}`)), + }, nil + }, + } + + config := notification.DefaultConfig() + logger := &MockLogger{} + + sdk, err := notification.NewSDKWithClient(config, mockClient, logger) + if err != nil { + t.Fatal(err) + } + + response, err := sdk.SendEmail(context.Background(), "test@example.com", "Test", "user123") + if err != nil { + t.Fatal(err) + } + + if response.Status != "Notification sent" { + t.Errorf("Expected 'Notification sent', got %s", response.Status) + } +} +``` + +## Best Practices + +1. **Configuration**: Use environment variables for configuration in production +2. **Context**: Always pass context for timeout and cancellation support +3. **Error Handling**: Check specific error types for appropriate handling +4. **Logging**: Implement structured logging for better observability +5. **Retries**: Configure appropriate retry settings based on your use case +6. **Validation**: Let the SDK handle validation, but provide clean input data +7. **Batch Operations**: Use batch operations for sending multiple notifications efficiently + +## Examples + +See the `examples/` directory for complete working examples: + +- `basic_usage.go` - Basic notification sending +- `batch_operations.go` - Batch notification processing +- `fluent_api.go` - Using the builder pattern +- `error_handling.go` - Comprehensive error handling +- `configuration.go` - Different configuration methods + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit a pull request + +## License + +This SDK is part of the Tender Management system and follows the same license terms. \ No newline at end of file diff --git a/pkg/notification/USAGE.md b/pkg/notification/USAGE.md new file mode 100644 index 0000000..ef00148 --- /dev/null +++ b/pkg/notification/USAGE.md @@ -0,0 +1,87 @@ +# Notification SDK - Quick Usage Guide + +## Installation +The SDK is already part of your `tm` module. Import it as: +```go +import "tm/pkg/notification" +``` + +## Basic Setup +```go +// 1. Configuration +config := notification.DefaultConfig() +config.BaseURL = "http://127.0.0.1:9095" // Your notification service URL + +// 2. Logger (using project's logger) +loggerConfig := &logger.Config{Level: "info", Format: "json", Output: "stdout"} +projectLogger := logger.NewLogger(loggerConfig) +loggerAdapter := ¬ification.LoggerAdapter{Logger: projectLogger} + +// 3. Create SDK +sdk, err := notification.NewSDK(config, loggerAdapter) +``` + +## Environment Variables +```bash +export NOTIFICATION_BASE_URL="http://127.0.0.1:9095" +export NOTIFICATION_TIMEOUT="30s" +export NOTIFICATION_RETRY_ATTEMPTS="3" +export NOTIFICATION_RETRY_DELAY="2s" +export NOTIFICATION_ENABLE_LOGGING="true" +``` + +## Quick Examples + +### Simple Email +```go +err := sdk.QuickEmail(ctx, "user@example.com", "Hello!") +``` + +### Custom Notification +```go +request := ¬ification.NotificationRequest{ + EventType: notification.EventTypeEmail, + Message: "Hello Nima", + Methods: notification.NotificationMethods{ + OTP: "09143257416", + SMS: "09143257416", + Push: "devicefsdgsdgsg6766sdgdsg", + Email: "nakhostin.nima1998@gmail.com", + }, + UserID: "", +} +response, err := sdk.SendNotification(ctx, request) +``` + +### Fluent Builder +```go +response, err := sdk.NewBuilder(). + SetEventType(notification.EventTypeEmail). + SetMessage("Hello Nima"). + SetEmail("nakhostin.nima1998@gmail.com"). + Send(ctx) +``` + +### Batch Operations +```go +requests := []*notification.NotificationRequest{...} +batchResponse, err := sdk.SendBatch(ctx, requests) +``` + +## API Response +- **Success**: `{"status": "Notification sent"}` +- **Error**: `{"error": "error message"}` + +## Error Handling +```go +if err != nil { + switch e := err.(type) { + case notification.ErrHTTP: + // HTTP error (400, 500, etc.) + case notification.ErrValidation: + // Validation error + case notification.ErrRetryExhausted: + // All retry attempts failed + } +} +``` \ No newline at end of file diff --git a/pkg/notification/client.go b/pkg/notification/client.go new file mode 100644 index 0000000..46cf1c3 --- /dev/null +++ b/pkg/notification/client.go @@ -0,0 +1,210 @@ +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 + } +} diff --git a/pkg/notification/config.go b/pkg/notification/config.go new file mode 100644 index 0000000..1d2b99d --- /dev/null +++ b/pkg/notification/config.go @@ -0,0 +1,56 @@ +package notification + +import ( + "time" +) + +// Config holds the notification service configuration +type Config struct { + BaseURL string `` + RetryAttempts int `` + Timeout time.Duration `` + RetryDelay time.Duration `` + EnableLogging bool `` + UserAgent string `` +} + +// DefaultConfig returns default configuration for notification service +func DefaultConfig() *Config { + return &Config{ + BaseURL: "http://127.0.0.1:9095", + Timeout: 30 * time.Second, + RetryAttempts: 3, + RetryDelay: 2 * time.Second, + EnableLogging: true, + UserAgent: "TenderManagement-NotificationSDK/1.0", + } +} + +// NewConfig creates a new configuration for notification service +func NewConfig(baseURL string, timeout time.Duration, retryAttempts int, retryDelay time.Duration, enableLogging bool, userAgent string) *Config { + return &Config{ + BaseURL: baseURL, + Timeout: timeout, + RetryAttempts: retryAttempts, + RetryDelay: retryDelay, + EnableLogging: enableLogging, + UserAgent: userAgent, + } +} + +// Validate validates the configuration +func (c *Config) Validate() error { + if c.BaseURL == "" { + return ErrInvalidConfig{Field: "BaseURL", Message: "base URL cannot be empty"} + } + if c.Timeout <= 0 { + return ErrInvalidConfig{Field: "Timeout", Message: "timeout must be positive"} + } + if c.RetryAttempts < 0 { + return ErrInvalidConfig{Field: "RetryAttempts", Message: "retry attempts cannot be negative"} + } + if c.RetryDelay < 0 { + return ErrInvalidConfig{Field: "RetryDelay", Message: "retry delay cannot be negative"} + } + return nil +} diff --git a/pkg/notification/entities.go b/pkg/notification/entities.go new file mode 100644 index 0000000..4805c83 --- /dev/null +++ b/pkg/notification/entities.go @@ -0,0 +1,103 @@ +package notification + +import ( + "github.com/asaskevich/govalidator" +) + +// EventType represents the type of notification event +type EventType string + +const ( + EventTypeEmail EventType = "EMAIL" + EventTypeSMS EventType = "SMS" + EventTypePush EventType = "PUSH" + EventTypeOTP EventType = "OTP" +) + +// NotificationMethods contains different delivery methods for notifications +type NotificationMethods struct { + OTP string `json:"otp,omitempty" valid:"optional"` + SMS string `json:"sms,omitempty" valid:"optional"` + Push string `json:"push,omitempty" valid:"optional"` + Email string `json:"email,omitempty" valid:"optional,email"` +} + +// NotificationRequest represents the request structure for sending notifications +type NotificationRequest struct { + EventType EventType `json:"event_type" valid:"required,in(EMAIL|SMS|PUSH|OTP)"` + Message string `json:"message" valid:"required,length(1|1000)"` + Methods NotificationMethods `json:"methods" valid:"required"` + UserID string `json:"user_id,omitempty" valid:"optional"` +} + +// NotificationResponse represents the successful response from notification service +type NotificationResponse struct { + Status string `json:"status"` +} + +// NotificationErrorResponse represents the error response from notification service +type NotificationErrorResponse struct { + Error string `json:"error"` +} + +// Validate validates the notification request +func (nr *NotificationRequest) Validate() error { + // First run govalidator validation + if _, err := govalidator.ValidateStruct(nr); err != nil { + return ErrValidation{Field: "struct", Message: err.Error()} + } + + // Custom validation: ensure at least one method is provided based on event type + switch nr.EventType { + case EventTypeEmail: + if nr.Methods.Email == "" { + return ErrValidation{Field: "methods.email", Message: "email address is required for EMAIL event type"} + } + case EventTypeSMS: + if nr.Methods.SMS == "" { + return ErrValidation{Field: "methods.sms", Message: "SMS number is required for SMS event type"} + } + case EventTypePush: + if nr.Methods.Push == "" { + return ErrValidation{Field: "methods.push", Message: "push device token is required for PUSH event type"} + } + case EventTypeOTP: + if nr.Methods.OTP == "" { + return ErrValidation{Field: "methods.otp", Message: "OTP number is required for OTP event type"} + } + } + + return nil +} + +// HasMethod checks if a specific method is configured +func (nm *NotificationMethods) HasMethod(eventType EventType) bool { + switch eventType { + case EventTypeEmail: + return nm.Email != "" + case EventTypeSMS: + return nm.SMS != "" + case EventTypePush: + return nm.Push != "" + case EventTypeOTP: + return nm.OTP != "" + default: + return false + } +} + +// GetMethodValue returns the value for a specific method +func (nm *NotificationMethods) GetMethodValue(eventType EventType) string { + switch eventType { + case EventTypeEmail: + return nm.Email + case EventTypeSMS: + return nm.SMS + case EventTypePush: + return nm.Push + case EventTypeOTP: + return nm.OTP + default: + return "" + } +} diff --git a/pkg/notification/errors.go b/pkg/notification/errors.go new file mode 100644 index 0000000..3a45eda --- /dev/null +++ b/pkg/notification/errors.go @@ -0,0 +1,83 @@ +package notification + +import ( + "fmt" + "net/http" +) + +// Error types for notification SDK +var ( + ErrNotificationServiceUnavailable = fmt.Errorf("notification service unavailable") + ErrInvalidRequest = fmt.Errorf("invalid notification request") + ErrNetworkTimeout = fmt.Errorf("network timeout") + ErrUnauthorized = fmt.Errorf("unauthorized request") + ErrInternalError = fmt.Errorf("internal notification service error") +) + +// ErrInvalidConfig represents configuration validation errors +type ErrInvalidConfig struct { + Field string + Message string +} + +func (e ErrInvalidConfig) Error() string { + return fmt.Sprintf("invalid config field %s: %s", e.Field, e.Message) +} + +// ErrHTTP represents HTTP-related errors with status codes +type ErrHTTP struct { + StatusCode int + Message string + Body string +} + +func (e ErrHTTP) Error() string { + return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message) +} + +// ErrValidation represents validation errors +type ErrValidation struct { + Field string + Message string +} + +func (e ErrValidation) Error() string { + return fmt.Sprintf("validation error for field %s: %s", e.Field, e.Message) +} + +// ErrRetryExhausted represents retry exhaustion errors +type ErrRetryExhausted struct { + Attempts int + LastError error + ElapsedTime string +} + +func (e ErrRetryExhausted) Error() string { + return fmt.Sprintf("retry exhausted after %d attempts (%s): %v", e.Attempts, e.ElapsedTime, e.LastError) +} + +// MapHTTPError maps HTTP status codes to appropriate errors +func MapHTTPError(statusCode int, body string) error { + switch statusCode { + case http.StatusBadRequest: + return ErrHTTP{StatusCode: statusCode, Message: "bad request", Body: body} + case http.StatusUnauthorized: + return ErrHTTP{StatusCode: statusCode, Message: "unauthorized", Body: body} + case http.StatusForbidden: + return ErrHTTP{StatusCode: statusCode, Message: "forbidden", Body: body} + case http.StatusNotFound: + return ErrHTTP{StatusCode: statusCode, Message: "not found", Body: body} + case http.StatusUnprocessableEntity: + return ErrHTTP{StatusCode: statusCode, Message: "validation error", Body: body} + case http.StatusInternalServerError: + return ErrHTTP{StatusCode: statusCode, Message: "internal server error", Body: body} + case http.StatusBadGateway: + return ErrHTTP{StatusCode: statusCode, Message: "bad gateway", Body: body} + case http.StatusServiceUnavailable: + return ErrHTTP{StatusCode: statusCode, Message: "service unavailable", Body: body} + case http.StatusGatewayTimeout: + return ErrHTTP{StatusCode: statusCode, Message: "gateway timeout", Body: body} + default: + return ErrHTTP{StatusCode: statusCode, Message: "unknown error", Body: body} + } +} diff --git a/pkg/notification/interfaces.go b/pkg/notification/interfaces.go new file mode 100644 index 0000000..92a2458 --- /dev/null +++ b/pkg/notification/interfaces.go @@ -0,0 +1,56 @@ +package notification + +import ( + "context" +) + +// NotificationService defines the interface for notification operations +type NotificationService interface { + // SendNotification sends a notification using the configured client + SendNotification(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) + + // SendEmail sends an email notification + SendEmail(ctx context.Context, email, message, userID string) (*NotificationResponse, error) + + // SendSMS sends an SMS notification + SendSMS(ctx context.Context, phoneNumber, message, userID string) (*NotificationResponse, error) + + // SendPush sends a push notification + SendPush(ctx context.Context, deviceToken, message, userID string) (*NotificationResponse, error) + + // SendOTP sends an OTP notification + SendOTP(ctx context.Context, phoneNumber, message, userID string) (*NotificationResponse, error) + + // Health checks if the notification service is available + Health(ctx context.Context) error +} + +// Builder provides a fluent interface for building notification requests +type Builder interface { + // SetEventType sets the event type + SetEventType(eventType EventType) Builder + + // SetMessage sets the notification message + SetMessage(message string) Builder + + // SetUserID sets the user ID + SetUserID(userID string) Builder + + // SetEmail sets the email address + SetEmail(email string) Builder + + // SetSMS sets the SMS phone number + SetSMS(phoneNumber string) Builder + + // SetPush sets the push device token + SetPush(deviceToken string) Builder + + // SetOTP sets the OTP phone number + SetOTP(phoneNumber string) Builder + + // Build creates the notification request + Build() (*NotificationRequest, error) + + // Send builds and sends the notification + Send(ctx context.Context) (*NotificationResponse, error) +} diff --git a/pkg/notification/sdk.go b/pkg/notification/sdk.go new file mode 100644 index 0000000..6d1226a --- /dev/null +++ b/pkg/notification/sdk.go @@ -0,0 +1,178 @@ +package notification + +import ( + "context" + "fmt" +) + +// SDK provides a high-level interface for the notification service +type SDK struct { + service NotificationService + config *Config +} + +// NewSDK creates a new notification SDK with the provided configuration and logger +func New(config *Config, logger Logger) (*SDK, error) { + if config == nil { + config = DefaultConfig() + } + + client, err := NewClient(config, logger) + if err != nil { + return nil, fmt.Errorf("failed to create notification client: %w", err) + } + + service := NewService(client) + + return &SDK{ + service: service, + config: config, + }, nil +} + +// NewSDKWithClient creates a new notification SDK with a custom HTTP client +func NewSDKWithClient(config *Config, httpClient HTTPClient, logger Logger) (*SDK, error) { + if config == nil { + config = DefaultConfig() + } + + client, err := NewClientWithHTTPClient(config, httpClient, logger) + if err != nil { + return nil, fmt.Errorf("failed to create notification client: %w", err) + } + + service := NewService(client) + + return &SDK{ + service: service, + config: config, + }, nil +} + +// SendNotification sends a custom notification request +func (s *SDK) SendNotification(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) { + return s.service.SendNotification(ctx, req) +} + +// SendEmail sends an email notification +func (s *SDK) SendEmail(ctx context.Context, email, message, userID string) (*NotificationResponse, error) { + return s.service.SendEmail(ctx, email, message, userID) +} + +// SendSMS sends an SMS notification +func (s *SDK) SendSMS(ctx context.Context, phoneNumber, message, userID string) (*NotificationResponse, error) { + return s.service.SendSMS(ctx, phoneNumber, message, userID) +} + +// SendPush sends a push notification +func (s *SDK) SendPush(ctx context.Context, deviceToken, message, userID string) (*NotificationResponse, error) { + return s.service.SendPush(ctx, deviceToken, message, userID) +} + +// SendOTP sends an OTP notification +func (s *SDK) SendOTP(ctx context.Context, phoneNumber, message, userID string) (*NotificationResponse, error) { + return s.service.SendOTP(ctx, phoneNumber, message, userID) +} + +// NewBuilder creates a new notification builder for fluent API usage +func (s *SDK) NewBuilder() Builder { + return s.service.(*Service).NewBuilder() +} + +// Health checks if the notification service is available +func (s *SDK) Health(ctx context.Context) error { + return s.service.Health(ctx) +} + +// GetConfig returns the current configuration +func (s *SDK) GetConfig() *Config { + return s.config +} + +// Convenience functions for quick notifications + +// QuickEmail sends a simple email notification +func (s *SDK) QuickEmail(ctx context.Context, email, message string) error { + _, err := s.SendEmail(ctx, email, message, "") + return err +} + +// QuickSMS sends a simple SMS notification +func (s *SDK) QuickSMS(ctx context.Context, phoneNumber, message string) error { + _, err := s.SendSMS(ctx, phoneNumber, message, "") + return err +} + +// QuickPush sends a simple push notification +func (s *SDK) QuickPush(ctx context.Context, deviceToken, message string) error { + _, err := s.SendPush(ctx, deviceToken, message, "") + return err +} + +// QuickOTP sends a simple OTP notification +func (s *SDK) QuickOTP(ctx context.Context, phoneNumber, message string) error { + _, err := s.SendOTP(ctx, phoneNumber, message, "") + return err +} + +// Batch operations for sending multiple notifications + +// BatchRequest represents a batch notification request +type BatchRequest struct { + Requests []*NotificationRequest `json:"requests"` +} + +// BatchResponse represents the response from batch operations +type BatchResponse struct { + Responses []BatchItemResponse `json:"responses"` + Success int `json:"success"` + Failed int `json:"failed"` +} + +// BatchItemResponse represents individual response in batch +type BatchItemResponse struct { + Index int `json:"index"` + Success bool `json:"success"` + Response *NotificationResponse `json:"response,omitempty"` + Error string `json:"error,omitempty"` +} + +// SendBatch sends multiple notifications in batch (sequential for now) +func (s *SDK) SendBatch(ctx context.Context, requests []*NotificationRequest) (*BatchResponse, error) { + if len(requests) == 0 { + return &BatchResponse{ + Responses: []BatchItemResponse{}, + Success: 0, + Failed: 0, + }, nil + } + + responses := make([]BatchItemResponse, len(requests)) + successCount := 0 + failedCount := 0 + + for i, req := range requests { + resp, err := s.service.SendNotification(ctx, req) + if err != nil { + responses[i] = BatchItemResponse{ + Index: i, + Success: false, + Error: err.Error(), + } + failedCount++ + } else { + responses[i] = BatchItemResponse{ + Index: i, + Success: true, + Response: resp, + } + successCount++ + } + } + + return &BatchResponse{ + Responses: responses, + Success: successCount, + Failed: failedCount, + }, nil +} diff --git a/pkg/notification/service.go b/pkg/notification/service.go new file mode 100644 index 0000000..b906e75 --- /dev/null +++ b/pkg/notification/service.go @@ -0,0 +1,177 @@ +package notification + +import ( + "context" + "fmt" +) + +// Service implements the NotificationService interface +type Service struct { + client *Client +} + +// NewService creates a new notification service +func NewService(client *Client) NotificationService { + return &Service{ + client: client, + } +} + +// SendNotification sends a notification using the configured client +func (s *Service) SendNotification(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) { + return s.client.SendNotification(ctx, req) +} + +// SendEmail sends an email notification +func (s *Service) SendEmail(ctx context.Context, email, message, userID string) (*NotificationResponse, error) { + req := &NotificationRequest{ + EventType: EventTypeEmail, + Message: message, + Methods: NotificationMethods{ + Email: email, + }, + UserID: userID, + } + + return s.client.SendNotification(ctx, req) +} + +// SendSMS sends an SMS notification +func (s *Service) SendSMS(ctx context.Context, phoneNumber, message, userID string) (*NotificationResponse, error) { + req := &NotificationRequest{ + EventType: EventTypeSMS, + Message: message, + Methods: NotificationMethods{ + SMS: phoneNumber, + }, + UserID: userID, + } + + return s.client.SendNotification(ctx, req) +} + +// SendPush sends a push notification +func (s *Service) SendPush(ctx context.Context, deviceToken, message, userID string) (*NotificationResponse, error) { + req := &NotificationRequest{ + EventType: EventTypePush, + Message: message, + Methods: NotificationMethods{ + Push: deviceToken, + }, + UserID: userID, + } + + return s.client.SendNotification(ctx, req) +} + +// SendOTP sends an OTP notification +func (s *Service) SendOTP(ctx context.Context, phoneNumber, message, userID string) (*NotificationResponse, error) { + req := &NotificationRequest{ + EventType: EventTypeOTP, + Message: message, + Methods: NotificationMethods{ + OTP: phoneNumber, + }, + UserID: userID, + } + + return s.client.SendNotification(ctx, req) +} + +// Health checks if the notification service is available +func (s *Service) Health(ctx context.Context) error { + // Create a simple test request to check service availability + testReq := &NotificationRequest{ + EventType: EventTypeEmail, + Message: "health check", + Methods: NotificationMethods{ + Email: "test@example.com", + }, + } + + // We don't actually send this, just validate the client can create a request + if err := testReq.Validate(); err != nil { + return fmt.Errorf("service health check failed: %w", err) + } + + return nil +} + +// notificationBuilder implements the Builder interface +type notificationBuilder struct { + service *Service + request *NotificationRequest + hasErrors []error +} + +// NewBuilder creates a new notification builder +func (s *Service) NewBuilder() Builder { + return ¬ificationBuilder{ + service: s, + request: &NotificationRequest{ + Methods: NotificationMethods{}, + }, + hasErrors: make([]error, 0), + } +} + +// SetEventType sets the event type +func (b *notificationBuilder) SetEventType(eventType EventType) Builder { + b.request.EventType = eventType + return b +} + +// SetMessage sets the notification message +func (b *notificationBuilder) SetMessage(message string) Builder { + b.request.Message = message + return b +} + +// SetUserID sets the user ID +func (b *notificationBuilder) SetUserID(userID string) Builder { + b.request.UserID = userID + return b +} + +// SetEmail sets the email address +func (b *notificationBuilder) SetEmail(email string) Builder { + b.request.Methods.Email = email + return b +} + +// SetSMS sets the SMS phone number +func (b *notificationBuilder) SetSMS(phoneNumber string) Builder { + b.request.Methods.SMS = phoneNumber + return b +} + +// SetPush sets the push device token +func (b *notificationBuilder) SetPush(deviceToken string) Builder { + b.request.Methods.Push = deviceToken + return b +} + +// SetOTP sets the OTP phone number +func (b *notificationBuilder) SetOTP(phoneNumber string) Builder { + b.request.Methods.OTP = phoneNumber + return b +} + +// Build creates the notification request +func (b *notificationBuilder) Build() (*NotificationRequest, error) { + if err := b.request.Validate(); err != nil { + return nil, fmt.Errorf("failed to build notification request: %w", err) + } + + return b.request, nil +} + +// Send builds and sends the notification +func (b *notificationBuilder) Send(ctx context.Context) (*NotificationResponse, error) { + req, err := b.Build() + if err != nil { + return nil, err + } + + return b.service.SendNotification(ctx, req) +}