package usecase import ( "context" "errors" "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) 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 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 } // 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 } // NewService wires use cases together. func NewService(repo Repository, pay PaymentGateway, jwtSecret string, tokenTTL time.Duration) *Service { return &Service{repo: repo, pay: pay, jwtSecret: []byte(jwtSecret), tokenTTL: tokenTTL} } 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) 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) } 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) } func (s *Service) CreateUser(ctx context.Context, user *domain.User, password string) error { hashed, err := bcrypt.GenerateFromPassword([]byte(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(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) } // 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(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 } // 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 }