feat(backend): implement notification system (logger mock)

This commit is contained in:
Tiago Yamamoto 2025-12-27 01:24:43 -03:00
parent b63242b8fd
commit 6de471ce3e
4 changed files with 108 additions and 7 deletions

View file

@ -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
}

View file

@ -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()

View file

@ -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 {

View file

@ -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
}