package customer import ( "context" "errors" "time" infrastructure "tm/infra" "tm/pkg/logger" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) // CustomerService implements the CustomerService interface type CustomerService struct { customerRepo Repository config *infrastructure.AuthConfig logger logger.Logger } // NewCustomerService creates a new customer service func NewCustomerService( customerRepo Repository, config *infrastructure.AuthConfig, logger logger.Logger, ) CustomerService { return CustomerService{ customerRepo: customerRepo, config: config, logger: logger, } } // GetCustomers retrieves customers with pagination (for panel users) func (s *CustomerService) GetCustomers(ctx context.Context, limit, offset int) ([]*Customer, error) { s.logger.Info("Retrieving customers", map[string]interface{}{ "limit": limit, "offset": offset, }) customers, err := s.customerRepo.List(ctx, limit, offset) if err != nil { s.logger.Error("Failed to retrieve customers", map[string]interface{}{ "error": err.Error(), "limit": limit, "offset": offset, }) return nil, errors.New("failed to retrieve customers") } s.logger.Info("Customers retrieved successfully", map[string]interface{}{ "count": len(customers), "limit": limit, "offset": offset, }) return customers, nil } // GetCustomerByID retrieves a customer by ID (for panel users) func (s *CustomerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) { s.logger.Info("Retrieving customer by ID", map[string]interface{}{ "customer_id": id.String(), }) customer, err := s.customerRepo.GetByID(ctx, id) if err != nil { s.logger.Error("Failed to retrieve customer", map[string]interface{}{ "error": err.Error(), "customer_id": id.String(), }) return nil, errors.New("customer not found") } s.logger.Info("Customer retrieved successfully", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, }) return customer, nil } // GetCustomersByCompany retrieves customers by company ID with pagination (for panel users) func (s *CustomerService) GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) { s.logger.Info("Retrieving customers by company", map[string]interface{}{ "company_id": companyID.String(), "limit": limit, "offset": offset, }) customers, err := s.customerRepo.GetByCompanyID(ctx, companyID, limit, offset) if err != nil { s.logger.Error("Failed to retrieve customers by company", map[string]interface{}{ "error": err.Error(), "company_id": companyID.String(), "limit": limit, "offset": offset, }) return nil, errors.New("failed to retrieve customers") } s.logger.Info("Customers by company retrieved successfully", map[string]interface{}{ "count": len(customers), "company_id": companyID.String(), "limit": limit, "offset": offset, }) return customers, nil } // UpdateCustomerStatus updates customer active status (for panel users) func (s *CustomerService) UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error { s.logger.Info("Updating customer status", map[string]interface{}{ "customer_id": customerID.String(), "is_active": isActive, "updated_by": updatedBy.String(), }) // Get customer customer, err := s.customerRepo.GetByID(ctx, customerID) if err != nil { s.logger.Error("Customer not found for status update", map[string]interface{}{ "error": err.Error(), "customer_id": customerID.String(), }) return errors.New("customer not found") } // Update status customer.IsActive = isActive // Save changes if err := s.customerRepo.Update(ctx, customer); err != nil { s.logger.Error("Failed to update customer status", map[string]interface{}{ "error": err.Error(), "customer_id": customerID.String(), "is_active": isActive, "updated_by": updatedBy.String(), }) return errors.New("failed to update customer status") } s.logger.Info("Customer status updated successfully", map[string]interface{}{ "customer_id": customerID.String(), "is_active": isActive, "updated_by": updatedBy.String(), }) return nil } // Register creates a new customer account func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*AuthorizationResponse, error) { s.logger.Info("Registering new customer", map[string]interface{}{ "email": req.Email, }) // Check if customer already exists existingCustomer, err := s.customerRepo.GetByEmail(ctx, req.Email) if err == nil && existingCustomer != nil { return nil, errors.New("customer already exists") } // Hash password hashedPassword, err := s.hashPassword(req.Password) if err != nil { s.logger.Error("Failed to hash password", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("failed to process password") } // Create customer customer := &Customer{ ID: uuid.New(), Email: req.Email, Password: hashedPassword, FirstName: req.FirstName, LastName: req.LastName, Mobile: req.Mobile, IsActive: true, IsVerified: false, // Require email verification DeviceTokens: make([]string, 0), UpdatedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(), } if err := s.customerRepo.Create(ctx, customer); err != nil { s.logger.Error("Failed to create customer", map[string]interface{}{ "error": err.Error(), "email": req.Email, }) return nil, errors.New("failed to create customer") } // Generate tokens accessToken, err := s.generateAccessToken(customer) if err != nil { s.logger.Error("Failed to generate access token", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return nil, errors.New("failed to generate access token") } refreshToken, err := s.generateRefreshToken(customer) if err != nil { s.logger.Error("Failed to generate refresh token", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return nil, errors.New("failed to generate refresh token") } s.logger.Info("Customer registered successfully", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, "company_id": customer.CompanyID.String(), }) // TODO: Send verification email s.sendVerificationEmail(ctx, customer) return &AuthorizationResponse{ Customer: NewCustomerAggregate(*customer), AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), }, nil } // Login authenticates a customer and returns tokens func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*AuthorizationResponse, error) { s.logger.Info("Customer login attempt", map[string]interface{}{ "email": req.Email, }) // Get customer by email customer, err := s.customerRepo.GetByEmail(ctx, req.Email) if err != nil { s.logger.Warn("Customer login failed - customer not found", map[string]interface{}{ "email": req.Email, }) return nil, errors.New("invalid credentials") } // Check if customer is active if !customer.IsActive { s.logger.Warn("Customer login failed - account inactive", map[string]interface{}{ "email": req.Email, "customer_id": customer.ID.String(), }) return nil, errors.New("account is inactive") } // Verify password if !s.verifyPassword(req.Password, customer.Password) { s.logger.Warn("Customer login failed - invalid password", map[string]interface{}{ "email": req.Email, "customer_id": customer.ID.String(), }) return nil, errors.New("invalid credentials") } // Update last login time now := time.Now().Unix() customer.LastLoginAt = &now customer.UpdatedAt = now if err := s.customerRepo.Update(ctx, customer); err != nil { s.logger.Warn("Failed to update customer last login", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) // Don't fail login for this } // Generate tokens accessToken, err := s.generateAccessToken(customer) if err != nil { s.logger.Error("Failed to generate access token", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return nil, errors.New("failed to generate access token") } refreshToken, err := s.generateRefreshToken(customer) if err != nil { s.logger.Error("Failed to generate refresh token", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return nil, errors.New("failed to generate refresh token") } s.logger.Info("Customer logged in successfully", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, "is_verified": customer.IsVerified, }) return &AuthorizationResponse{ Customer: NewCustomerAggregate(*customer), AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), }, nil } // RefreshToken generates new tokens using refresh token func (s *CustomerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthorizationResponse, error) { // Parse and validate refresh token token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("invalid signing method") } return []byte(s.config.JWT.Secret), nil }) if err != nil || !token.Valid { return nil, errors.New("invalid refresh token") } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, errors.New("invalid token claims") } // Verify token type tokenType, ok := claims["type"].(string) if !ok || tokenType != "refresh" { return nil, errors.New("invalid token type") } // Verify user type userType, ok := claims["user_type"].(string) if !ok || userType != "customer" { return nil, errors.New("invalid user type") } customerIDStr, ok := claims["customer_id"].(string) if !ok { return nil, errors.New("invalid customer ID in token") } customerID, err := uuid.Parse(customerIDStr) if err != nil { return nil, errors.New("invalid customer ID format") } // Get customer from database customer, err := s.customerRepo.GetByID(ctx, customerID) if err != nil { return nil, errors.New("customer not found") } if !customer.IsActive { return nil, errors.New("account is inactive") } // Generate new tokens accessToken, err := s.generateAccessToken(customer) if err != nil { return nil, errors.New("failed to generate access token") } newRefreshToken, err := s.generateRefreshToken(customer) if err != nil { return nil, errors.New("failed to generate refresh token") } return &AuthorizationResponse{ Customer: NewCustomerAggregate(*customer), AccessToken: accessToken, RefreshToken: newRefreshToken, ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()), }, nil } // ValidateToken validates a JWT token and returns the customer func (s *CustomerService) ValidateToken(ctx context.Context, tokenString string) (*Customer, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("invalid signing method") } return []byte(s.config.JWT.Secret), nil }) if err != nil || !token.Valid { return nil, errors.New("invalid token") } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, errors.New("invalid token claims") } // Verify token type tokenType, ok := claims["type"].(string) if !ok || tokenType != "access" { return nil, errors.New("invalid token type") } // Verify user type userType, ok := claims["user_type"].(string) if !ok || userType != "customer" { return nil, errors.New("invalid user type") } customerIDStr, ok := claims["customer_id"].(string) if !ok { return nil, errors.New("invalid customer ID in token") } customerID, err := uuid.Parse(customerIDStr) if err != nil { return nil, errors.New("invalid customer ID format") } customer, err := s.customerRepo.GetByID(ctx, customerID) if err != nil { return nil, errors.New("customer not found") } if !customer.IsActive { return nil, errors.New("account is inactive") } return customer, nil } // ChangePassword changes customer password func (s *CustomerService) ChangePassword(ctx context.Context, customerID uuid.UUID, oldPassword, newPassword string) error { customer, err := s.customerRepo.GetByID(ctx, customerID) if err != nil { return errors.New("customer not found") } // Verify old password if !s.verifyPassword(oldPassword, customer.Password) { return errors.New("invalid old password") } // Hash new password hashedPassword, err := s.hashPassword(newPassword) if err != nil { return errors.New("failed to process new password") } // Update password customer.Password = hashedPassword customer.UpdatedAt = time.Now().Unix() if err := s.customerRepo.Update(ctx, customer); err != nil { s.logger.Error("Failed to update customer password", map[string]interface{}{ "error": err.Error(), "customer_id": customerID.String(), }) return errors.New("failed to update password") } s.logger.Info("Customer password changed successfully", map[string]interface{}{ "customer_id": customerID.String(), }) return nil } // ResetPassword initiates password reset process func (s *CustomerService) ResetPassword(ctx context.Context, email string) error { // TODO: Implement password reset logic // This would typically involve: // 1. Generate reset token // 2. Store token with expiration // 3. Send reset email return errors.New("password reset not implemented") } // VerifyEmail verifies customer's email address func (s *CustomerService) VerifyEmail(ctx context.Context, customerID uuid.UUID, token string) error { // TODO: Implement email verification logic // This would typically involve: // 1. Validate verification token // 2. Update customer verification status // 3. Send welcome email return errors.New("email verification not implemented") } // ResendVerification resend verification email func (s *CustomerService) ResendVerification(ctx context.Context, email string) error { customer, err := s.customerRepo.GetByEmail(ctx, email) if err != nil { return errors.New("customer not found") } if customer.IsVerified { return errors.New("email already verified") } // TODO: Send verification email s.sendVerificationEmail(ctx, customer) return nil } // Private helper methods func (s *CustomerService) hashPassword(password string) (string, error) { hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(hashedBytes), nil } func (s *CustomerService) verifyPassword(password, hashedPassword string) bool { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) return err == nil } func (s *CustomerService) generateAccessToken(customer *Customer) (string, error) { claims := jwt.MapClaims{ "customer_id": customer.ID.String(), "email": customer.Email, "company_id": customer.CompanyID.String(), "user_type": "customer", "exp": time.Now().Add(s.config.JWT.AccessTokenDuration).Unix(), "iat": time.Now().Unix(), "type": "access", } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(s.config.JWT.Secret)) } func (s *CustomerService) generateRefreshToken(customer *Customer) (string, error) { claims := jwt.MapClaims{ "customer_id": customer.ID.String(), "user_type": "customer", "exp": time.Now().Add(s.config.JWT.RefreshTokenDuration).Unix(), "iat": time.Now().Unix(), "type": "refresh", } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(s.config.JWT.Secret)) } func (s *CustomerService) sendVerificationEmail(ctx context.Context, customer *Customer) { // TODO: Implement email sending logic _ = ctx s.logger.Info("Verification email would be sent", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, }) }