feat(backend): implement notification system (logger mock)
This commit is contained in:
parent
b63242b8fd
commit
6de471ce3e
4 changed files with 108 additions and 7 deletions
37
backend/internal/notifications/service.go
Normal file
37
backend/internal/notifications/service.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/saveinmed/backend-go/internal/domain"
|
"github.com/saveinmed/backend-go/internal/domain"
|
||||||
"github.com/saveinmed/backend-go/internal/http/handler"
|
"github.com/saveinmed/backend-go/internal/http/handler"
|
||||||
"github.com/saveinmed/backend-go/internal/http/middleware"
|
"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/payments"
|
||||||
"github.com/saveinmed/backend-go/internal/repository/postgres"
|
"github.com/saveinmed/backend-go/internal/repository/postgres"
|
||||||
"github.com/saveinmed/backend-go/internal/usecase"
|
"github.com/saveinmed/backend-go/internal/usecase"
|
||||||
|
|
@ -34,9 +35,11 @@ func New(cfg config.Config) (*Server, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := postgres.New(db)
|
repoInstance := postgres.New(db)
|
||||||
gateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission)
|
paymentGateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MarketplaceCommission)
|
||||||
svc := usecase.NewService(repo, gateway, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper)
|
// Services
|
||||||
|
notifySvc := notifications.NewLoggerNotificationService()
|
||||||
|
svc := usecase.NewService(repoInstance, paymentGateway, notifySvc, cfg.MarketplaceCommission, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper)
|
||||||
h := handler.New(svc, cfg.BuyerFeeRate)
|
h := handler.New(svc, cfg.BuyerFeeRate)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
|
|
||||||
"github.com/saveinmed/backend-go/internal/domain"
|
"github.com/saveinmed/backend-go/internal/domain"
|
||||||
|
"github.com/saveinmed/backend-go/internal/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository defines DB contract for the core entities.
|
// Repository defines DB contract for the core entities.
|
||||||
|
|
@ -72,6 +73,7 @@ type PaymentGateway interface {
|
||||||
type Service struct {
|
type Service struct {
|
||||||
repo Repository
|
repo Repository
|
||||||
pay PaymentGateway
|
pay PaymentGateway
|
||||||
|
notify notifications.NotificationService
|
||||||
jwtSecret []byte
|
jwtSecret []byte
|
||||||
tokenTTL time.Duration
|
tokenTTL time.Duration
|
||||||
marketplaceCommission float64
|
marketplaceCommission float64
|
||||||
|
|
@ -83,10 +85,11 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewService wires use cases together.
|
// 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{
|
return &Service{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
pay: pay,
|
pay: pay,
|
||||||
|
notify: notify,
|
||||||
jwtSecret: []byte(jwtSecret),
|
jwtSecret: []byte(jwtSecret),
|
||||||
tokenTTL: tokenTTL,
|
tokenTTL: tokenTTL,
|
||||||
marketplaceCommission: commissionPct,
|
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 {
|
func (s *Service) CreateOrder(ctx context.Context, order *domain.Order) error {
|
||||||
order.ID = uuid.Must(uuid.NewV7())
|
order.ID = uuid.Must(uuid.NewV7())
|
||||||
order.Status = domain.OrderStatusPending
|
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) {
|
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 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 {
|
func (s *Service) DeleteOrder(ctx context.Context, id uuid.UUID) error {
|
||||||
|
|
|
||||||
|
|
@ -329,11 +329,22 @@ func (m *MockPaymentGateway) CreatePreference(ctx context.Context, order *domain
|
||||||
}, nil
|
}, 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
|
// Helper to create a test service
|
||||||
func newTestService() (*Service, *MockRepository) {
|
func newTestService() (*Service, *MockRepository) {
|
||||||
repo := NewMockRepository()
|
repo := NewMockRepository()
|
||||||
gateway := &MockPaymentGateway{}
|
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
|
return svc, repo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue