package usecase import ( "context" "errors" "strings" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" ) // Repository defines DB contract for the core entities. type Repository interface { CreateCompany(ctx context.Context, company *domain.Company) error ListCompanies(ctx context.Context) ([]domain.Company, error) GetCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) UpdateCompany(ctx context.Context, company *domain.Company) error CreateProduct(ctx context.Context, product *domain.Product) error ListProducts(ctx context.Context) ([]domain.Product, error) GetProduct(ctx context.Context, id uuid.UUID) (*domain.Product, error) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, error) CreateOrder(ctx context.Context, order *domain.Order) error GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error CreateShipment(ctx context.Context, shipment *domain.Shipment) error GetShipmentByOrderID(ctx context.Context, orderID uuid.UUID) (*domain.Shipment, error) CreateUser(ctx context.Context, user *domain.User) error ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) UpdateUser(ctx context.Context, user *domain.User) error DeleteUser(ctx context.Context, id uuid.UUID) error AddCartItem(ctx context.Context, item *domain.CartItem) (*domain.CartItem, error) ListCartItems(ctx context.Context, buyerID uuid.UUID) ([]domain.CartItem, error) DeleteCartItem(ctx context.Context, id uuid.UUID, buyerID uuid.UUID) error CreateReview(ctx context.Context, review *domain.Review) error GetCompanyRating(ctx context.Context, companyID uuid.UUID) (*domain.CompanyRating, error) SellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error) AdminDashboard(ctx context.Context, since time.Time) (*domain.AdminDashboard, error) } // PaymentGateway abstracts Mercado Pago integration. type PaymentGateway interface { CreatePreference(ctx context.Context, order *domain.Order) (*domain.PaymentPreference, error) } type Service struct { repo Repository pay PaymentGateway jwtSecret []byte tokenTTL time.Duration marketplaceCommission float64 passwordPepper string } // NewService wires use cases together. func NewService(repo Repository, pay PaymentGateway, commissionPct float64, jwtSecret string, tokenTTL time.Duration, passwordPepper string) *Service { return &Service{ repo: repo, pay: pay, jwtSecret: []byte(jwtSecret), tokenTTL: tokenTTL, marketplaceCommission: commissionPct, passwordPepper: passwordPepper, } } func (s *Service) RegisterCompany(ctx context.Context, company *domain.Company) error { company.ID = uuid.Must(uuid.NewV7()) return s.repo.CreateCompany(ctx, company) } func (s *Service) ListCompanies(ctx context.Context) ([]domain.Company, error) { return s.repo.ListCompanies(ctx) } func (s *Service) GetCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) { return s.repo.GetCompany(ctx, id) } func (s *Service) RegisterProduct(ctx context.Context, product *domain.Product) error { product.ID = uuid.Must(uuid.NewV7()) return s.repo.CreateProduct(ctx, product) } func (s *Service) ListProducts(ctx context.Context) ([]domain.Product, error) { return s.repo.ListProducts(ctx) } func (s *Service) ListInventory(ctx context.Context, filter domain.InventoryFilter) ([]domain.InventoryItem, error) { return s.repo.ListInventory(ctx, filter) } func (s *Service) AdjustInventory(ctx context.Context, productID uuid.UUID, delta int64, reason string) (*domain.InventoryItem, error) { return s.repo.AdjustInventory(ctx, productID, delta, reason) } 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) } func (s *Service) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) { return s.repo.GetOrder(ctx, id) } func (s *Service) UpdateOrderStatus(ctx context.Context, id uuid.UUID, status domain.OrderStatus) error { return s.repo.UpdateOrderStatus(ctx, id, status) } // CreateShipment persists a freight label for an order if not already present. func (s *Service) CreateShipment(ctx context.Context, shipment *domain.Shipment) error { if _, err := s.repo.GetOrder(ctx, shipment.OrderID); err != nil { return err } shipment.ID = uuid.Must(uuid.NewV7()) shipment.Status = "Label gerada" return s.repo.CreateShipment(ctx, shipment) } // GetShipmentByOrderID returns freight details for an order. func (s *Service) GetShipmentByOrderID(ctx context.Context, orderID uuid.UUID) (*domain.Shipment, error) { return s.repo.GetShipmentByOrderID(ctx, orderID) } func (s *Service) CreatePaymentPreference(ctx context.Context, id uuid.UUID) (*domain.PaymentPreference, error) { order, err := s.repo.GetOrder(ctx, id) if err != nil { return nil, err } return s.pay.CreatePreference(ctx, order) } // HandlePaymentWebhook processes Mercado Pago notifications ensuring split consistency. func (s *Service) HandlePaymentWebhook(ctx context.Context, event domain.PaymentWebhookEvent) (*domain.PaymentSplitResult, error) { order, err := s.repo.GetOrder(ctx, event.OrderID) if err != nil { return nil, err } expectedMarketplaceFee := int64(float64(order.TotalCents) * (s.marketplaceCommission / 100)) marketplaceFee := event.MarketplaceFee if marketplaceFee == 0 { marketplaceFee = expectedMarketplaceFee } sellerReceivable := order.TotalCents - marketplaceFee if event.SellerAmount > 0 { sellerReceivable = event.SellerAmount } if strings.EqualFold(event.Status, "approved") || strings.EqualFold(event.Status, "paid") { if err := s.repo.UpdateOrderStatus(ctx, order.ID, domain.OrderStatusPaid); err != nil { return nil, err } } return &domain.PaymentSplitResult{ OrderID: order.ID, PaymentID: event.PaymentID, Status: event.Status, MarketplaceFee: marketplaceFee, SellerReceivable: sellerReceivable, TotalPaidAmount: event.TotalPaidAmount, }, nil } func (s *Service) CreateUser(ctx context.Context, user *domain.User, password string) error { hashed, err := bcrypt.GenerateFromPassword([]byte(s.pepperPassword(password)), bcrypt.DefaultCost) if err != nil { return err } user.ID = uuid.Must(uuid.NewV7()) user.PasswordHash = string(hashed) return s.repo.CreateUser(ctx, user) } func (s *Service) ListUsers(ctx context.Context, filter domain.UserFilter, page, pageSize int) (*domain.UserPage, error) { if page < 1 { page = 1 } if pageSize <= 0 { pageSize = 20 } filter.Limit = pageSize filter.Offset = (page - 1) * pageSize users, total, err := s.repo.ListUsers(ctx, filter) if err != nil { return nil, err } return &domain.UserPage{Users: users, Total: total, Page: page, PageSize: pageSize}, nil } func (s *Service) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) { return s.repo.GetUser(ctx, id) } func (s *Service) UpdateUser(ctx context.Context, user *domain.User, newPassword string) error { if newPassword != "" { hashed, err := bcrypt.GenerateFromPassword([]byte(s.pepperPassword(newPassword)), bcrypt.DefaultCost) if err != nil { return err } user.PasswordHash = string(hashed) } return s.repo.UpdateUser(ctx, user) } func (s *Service) DeleteUser(ctx context.Context, id uuid.UUID) error { return s.repo.DeleteUser(ctx, id) } // AddItemToCart validates stock, persists the item and returns the refreshed summary. func (s *Service) AddItemToCart(ctx context.Context, buyerID, productID uuid.UUID, quantity int64) (*domain.CartSummary, error) { if quantity <= 0 { return nil, errors.New("quantity must be greater than zero") } product, err := s.repo.GetProduct(ctx, productID) if err != nil { return nil, err } cartItems, err := s.repo.ListCartItems(ctx, buyerID) if err != nil { return nil, err } var currentQty int64 for _, it := range cartItems { if it.ProductID == productID { currentQty += it.Quantity } } if product.Stock < currentQty+quantity { return nil, errors.New("insufficient stock for requested quantity") } _, err = s.repo.AddCartItem(ctx, &domain.CartItem{ ID: uuid.Must(uuid.NewV7()), BuyerID: buyerID, ProductID: productID, Quantity: quantity, UnitCents: product.PriceCents, Batch: product.Batch, ExpiresAt: product.ExpiresAt, }) if err != nil { return nil, err } return s.cartSummary(ctx, buyerID) } // ListCart returns the current cart with B2B discounts applied. func (s *Service) ListCart(ctx context.Context, buyerID uuid.UUID) (*domain.CartSummary, error) { return s.cartSummary(ctx, buyerID) } // RemoveCartItem deletes a cart row and returns the refreshed cart summary. func (s *Service) RemoveCartItem(ctx context.Context, buyerID, cartItemID uuid.UUID) (*domain.CartSummary, error) { if err := s.repo.DeleteCartItem(ctx, cartItemID, buyerID); err != nil { return nil, err } return s.cartSummary(ctx, buyerID) } func (s *Service) cartSummary(ctx context.Context, buyerID uuid.UUID) (*domain.CartSummary, error) { items, err := s.repo.ListCartItems(ctx, buyerID) if err != nil { return nil, err } var subtotal int64 for i := range items { subtotal += items[i].UnitCents * items[i].Quantity } summary := &domain.CartSummary{ Items: items, SubtotalCents: subtotal, } if subtotal >= 100000 { // apply 5% B2B discount for large baskets summary.DiscountCents = int64(float64(subtotal) * 0.05) summary.DiscountReason = "5% B2B volume discount" } summary.TotalCents = subtotal - summary.DiscountCents return summary, nil } // CreateReview stores a buyer rating ensuring the order is delivered and owned by the requester. func (s *Service) CreateReview(ctx context.Context, buyerID, orderID uuid.UUID, rating int, comment string) (*domain.Review, error) { if rating < 1 || rating > 5 { return nil, errors.New("rating must be between 1 and 5") } order, err := s.repo.GetOrder(ctx, orderID) if err != nil { return nil, err } if order.Status != domain.OrderStatusDelivered { return nil, errors.New("only delivered orders can be reviewed") } if order.BuyerID != buyerID { return nil, errors.New("order does not belong to buyer") } review := &domain.Review{ ID: uuid.Must(uuid.NewV7()), OrderID: orderID, BuyerID: buyerID, SellerID: order.SellerID, Rating: rating, Comment: comment, } if err := s.repo.CreateReview(ctx, review); err != nil { return nil, err } return review, nil } // GetCompanyRating returns the average rating for a seller or pharmacy. func (s *Service) GetCompanyRating(ctx context.Context, companyID uuid.UUID) (*domain.CompanyRating, error) { return s.repo.GetCompanyRating(ctx, companyID) } // GetSellerDashboard aggregates commercial KPIs for the seller. func (s *Service) GetSellerDashboard(ctx context.Context, sellerID uuid.UUID) (*domain.SellerDashboard, error) { return s.repo.SellerDashboard(ctx, sellerID) } // GetAdminDashboard exposes marketplace-wide metrics for the last 30 days. func (s *Service) GetAdminDashboard(ctx context.Context) (*domain.AdminDashboard, error) { since := time.Now().AddDate(0, 0, -30) return s.repo.AdminDashboard(ctx, since) } // RegisterAccount creates a company when needed and persists a user bound to it. func (s *Service) RegisterAccount(ctx context.Context, company *domain.Company, user *domain.User, password string) error { if company != nil { if company.ID == uuid.Nil { company.ID = uuid.Must(uuid.NewV7()) company.IsVerified = false if err := s.repo.CreateCompany(ctx, company); err != nil { return err } } else { if _, err := s.repo.GetCompany(ctx, company.ID); err != nil { return err } } user.CompanyID = company.ID } return s.CreateUser(ctx, user, password) } // Authenticate validates credentials and emits a signed JWT. func (s *Service) Authenticate(ctx context.Context, email, password string) (string, time.Time, error) { user, err := s.repo.GetUserByEmail(ctx, email) if err != nil { return "", time.Time{}, err } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(s.pepperPassword(password))); err != nil { return "", time.Time{}, errors.New("invalid credentials") } expiresAt := time.Now().Add(s.tokenTTL) claims := jwt.MapClaims{ "sub": user.ID.String(), "role": user.Role, "company_id": user.CompanyID.String(), "exp": expiresAt.Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString(s.jwtSecret) if err != nil { return "", time.Time{}, err } return signed, expiresAt, nil } func (s *Service) pepperPassword(password string) string { if s.passwordPepper == "" { return password } return password + s.passwordPepper } // VerifyCompany marks a company as verified. func (s *Service) VerifyCompany(ctx context.Context, id uuid.UUID) (*domain.Company, error) { company, err := s.repo.GetCompany(ctx, id) if err != nil { return nil, err } company.IsVerified = true if err := s.repo.UpdateCompany(ctx, company); err != nil { return nil, err } return company, nil }