From 6de471ce3ee730ffd3b29d995f6c55607d3856a6 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sat, 27 Dec 2025 01:24:43 -0300 Subject: [PATCH] feat(backend): implement notification system (logger mock) --- backend/internal/notifications/service.go | 37 +++++++++++++++ backend/internal/server/server.go | 9 ++-- backend/internal/usecase/usecase.go | 56 +++++++++++++++++++++-- backend/internal/usecase/usecase_test.go | 13 +++++- 4 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 backend/internal/notifications/service.go diff --git a/backend/internal/notifications/service.go b/backend/internal/notifications/service.go new file mode 100644 index 0000000..05fa8db --- /dev/null +++ b/backend/internal/notifications/service.go @@ -0,0 +1,37 @@ +package notifications + +import ( + "context" + "log" + + "github.com/saveinmed/backend-go/internal/domain" +) + +// NotificationService defines the contract for sending alerts. +type NotificationService interface { + NotifyOrderCreated(ctx context.Context, order *domain.Order, buyer, seller *domain.User) error + NotifyOrderStatusChanged(ctx context.Context, order *domain.Order, buyer *domain.User) error +} + +// LoggerNotificationService prints notifications to stdout for dev/MVP. +type LoggerNotificationService struct{} + +func NewLoggerNotificationService() *LoggerNotificationService { + return &LoggerNotificationService{} +} + +func (s *LoggerNotificationService) NotifyOrderCreated(ctx context.Context, order *domain.Order, buyer, seller *domain.User) error { + log.Printf("📧 [EMAIL] To: Seller <%s>\n Subject: Novo Pedido #%s recebido!\n Body: Olá %s, você recebeu um novo pedido de %s. Total: R$ %.2f", + seller.Email, order.ID, seller.Name, buyer.Name, float64(order.TotalCents)/100) + + log.Printf("📧 [EMAIL] To: Buyer <%s>\n Subject: Pedido #%s confirmado!\n Body: Olá %s, seu pedido foi recebido e está aguardando processamento.", + buyer.Email, order.ID, buyer.Name) + + return nil +} + +func (s *LoggerNotificationService) NotifyOrderStatusChanged(ctx context.Context, order *domain.Order, buyer *domain.User) error { + log.Printf("📧 [EMAIL] To: Buyer <%s>\n Subject: Atualização do Pedido #%s\n Body: Olá %s, seu pedido mudou para status: %s.", + buyer.Email, order.ID, buyer.Name, order.Status) + return nil +} diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index 76f6bbc..2afe9f3 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -15,6 +15,7 @@ import ( "github.com/saveinmed/backend-go/internal/domain" "github.com/saveinmed/backend-go/internal/http/handler" "github.com/saveinmed/backend-go/internal/http/middleware" + "github.com/saveinmed/backend-go/internal/notifications" "github.com/saveinmed/backend-go/internal/payments" "github.com/saveinmed/backend-go/internal/repository/postgres" "github.com/saveinmed/backend-go/internal/usecase" @@ -34,9 +35,11 @@ func New(cfg config.Config) (*Server, error) { return nil, err } - repo := postgres.New(db) - gateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission) - svc := usecase.NewService(repo, gateway, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper) + repoInstance := postgres.New(db) + paymentGateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission) + // Services + notifySvc := notifications.NewLoggerNotificationService() + svc := usecase.NewService(repoInstance, paymentGateway, notifySvc, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper) h := handler.New(svc, cfg.BuyerFeeRate) mux := http.NewServeMux() diff --git a/backend/internal/usecase/usecase.go b/backend/internal/usecase/usecase.go index 314e14b..2d0bcb5 100644 --- a/backend/internal/usecase/usecase.go +++ b/backend/internal/usecase/usecase.go @@ -13,6 +13,7 @@ import ( "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" + "github.com/saveinmed/backend-go/internal/notifications" ) // Repository defines DB contract for the core entities. @@ -72,6 +73,7 @@ type PaymentGateway interface { type Service struct { repo Repository pay PaymentGateway + notify notifications.NotificationService jwtSecret []byte tokenTTL time.Duration marketplaceCommission float64 @@ -83,10 +85,11 @@ const ( ) // NewService wires use cases together. -func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration, passwordPepper string) *Service { +func NewService(repo Repository, pay PaymentGateway, notify notifications.NotificationService, commissionPct float64, jwtSecret string, tokenTTL time.Duration, passwordPepper string) *Service { return &Service{ repo: repo, pay: pay, + notify: notify, jwtSecret: []byte(jwtSecret), tokenTTL: tokenTTL, marketplaceCommission: commissionPct, @@ -313,7 +316,33 @@ func (s *Service) AdjustInventory(ctx context.Context, productID uuid.UUID, delt func (s *Service) CreateOrder(ctx context.Context, order *domain.Order) error { order.ID = uuid.Must(uuid.NewV7()) order.Status = domain.OrderStatusPending - return s.repo.CreateOrder(ctx, order) + + if err := s.repo.CreateOrder(ctx, order); err != nil { + return err + } + + // Async notification (ignore errors to not block request) + go func() { + // Fetch buyer details + buyer, err := s.repo.GetUser(context.Background(), order.BuyerID) + if err != nil { + return + } + + // Fetch seller details (hack: get list and pick first for MVP) + users, _, _ := s.repo.ListUsers(context.Background(), domain.UserFilter{CompanyID: &order.SellerID, Limit: 1}) + var seller *domain.User + if len(users) > 0 { + seller = &users[0] + } else { + // Fallback mock + seller = &domain.User{Name: "Seller", Email: "seller@platform.com"} + } + + s.notify.NotifyOrderCreated(context.Background(), order, buyer, seller) + }() + + return nil } func (s *Service) ListOrders(ctx context.Context, filter domain.OrderFilter, page, pageSize int) (*domain.OrderPage, error) { @@ -368,7 +397,28 @@ func (s *Service) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status do return errors.New("order is in terminal state") } - return s.repo.UpdateOrderStatus(ctx, id, status) + if err := s.repo.UpdateOrderStatus(ctx, id, status); err != nil { + return err + } + + // Async notification + go func() { + // Re-fetch order to get all details if necessary, but we have fields. + // Need buyer to send email + updatedOrder, _ := s.repo.GetOrder(context.Background(), id) + if updatedOrder == nil { + return + } + + buyer, err := s.repo.GetUser(context.Background(), updatedOrder.BuyerID) + if err != nil { + return + } + + s.notify.NotifyOrderStatusChanged(context.Background(), updatedOrder, buyer) + }() + + return nil } func (s *Service) DeleteOrder(ctx context.Context, id uuid.UUID) error { diff --git a/backend/internal/usecase/usecase_test.go b/backend/internal/usecase/usecase_test.go index 46cf019..51919da 100644 --- a/backend/internal/usecase/usecase_test.go +++ b/backend/internal/usecase/usecase_test.go @@ -329,11 +329,22 @@ func (m *MockPaymentGateway) CreatePreference(ctx context.Context, order *domain }, nil } +// MockNotificationService for testing +type MockNotificationService struct{} + +func (m *MockNotificationService) NotifyOrderCreated(ctx context.Context, order *domain.Order, buyer, seller *domain.User) error { + return nil +} +func (m *MockNotificationService) NotifyOrderStatusChanged(ctx context.Context, order *domain.Order, buyer *domain.User) error { + return nil +} + // Helper to create a test service func newTestService() (*Service, *MockRepository) { repo := NewMockRepository() gateway := &MockPaymentGateway{} - svc := NewService(repo, gateway, 2.5, "test-secret", time.Hour, "test-pepper") + notify := &MockNotificationService{} + svc := NewService(repo, gateway, notify, 2.5, "test-secret", time.Hour, "test-pepper") return svc, repo }