package customer import ( "context" "errors" "time" "tm/internal/company" "tm/pkg/logger" "tm/pkg/response" "tm/pkg/authorization" "go.mongodb.org/mongo-driver/bson/primitive" "golang.org/x/crypto/bcrypt" ) // Service defines business logic for customer operations type Service interface { // Core customer operations Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error) GetByID(ctx context.Context, id string) (*CustomerResponse, error) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error) Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error) Delete(ctx context.Context, id string) error UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error // Authentication operations Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) Logout(ctx context.Context, id string, accessToken string) error } // customerService implements the Service interface type customerService struct { repository Repository logger logger.Logger authService authorization.AuthorizationService companyService company.Service } // NewCustomerService creates a new customer service func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service) Service { return &customerService{ repository: repository, logger: logger, authService: authService, companyService: companyService, } } // Register creates a new customer func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error) { // Check if email already exists existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email) if existingCustomer != nil { return nil, errors.New("customer with this email already exists") } // Verify that all company IDs exist var companyIDs []string if len(form.CompanyIDs) > 0 { for _, companyID := range form.CompanyIDs { _, err := s.companyService.GetCompanyByID(ctx, companyID) if err != nil { s.logger.Error("Invalid company ID provided", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) return nil, errors.New("invalid company ID: " + companyID) } } companyIDs = form.CompanyIDs } // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost) 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{ Type: CustomerType(form.Type), Status: CustomerStatusActive, CompanyIDs: companyIDs, FullName: form.FullName, Username: form.Username, Email: form.Email, Password: string(hashedPassword), Phone: form.Phone, } // Save to database err = s.repository.Register(ctx, customer) if err != nil { s.logger.Error("Failed to create customer", map[string]interface{}{ "error": err.Error(), "email": form.Email, "username": form.Username, }) return nil, err } s.logger.Info("Customer created successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, "type": customer.Type, "company_ids": companyIDs, }) return customer.ToResponse(nil), nil } // GetByID retrieves a customer by ID func (s *customerService) GetByID(ctx context.Context, id string) (*CustomerResponse, error) { customer, err := s.repository.GetByID(ctx, id) if err != nil { s.logger.Error("Failed to get customer by ID", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return nil, err } var companies []*CompanySummary if len(customer.CompanyIDs) > 0 { companies, err = s.loadCompaniesForCustomer(ctx, customer) if err != nil { s.logger.Error("Failed to load companies for customer", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) } } s.logger.Info("Customer retrieved successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, }) return customer.ToResponse(companies), nil } // Search searches for customers func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error) { customers, total, err := s.repository.Search(ctx, form, pagination) if err != nil { s.logger.Error("Failed to search customers", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("failed to search customers") } var customerResponses []*CustomerResponse for _, customer := range customers { companies, err := s.loadCompaniesForCustomer(ctx, &customer) if err != nil { s.logger.Error("Failed to load companies for customer", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) } customerResponses = append(customerResponses, customer.ToResponse(companies)) } return &CustomerListResponse{ Customers: customerResponses, Meta: pagination.Response(total), }, nil } // Update updates a customer func (s *customerService) Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error) { // Get existing customer customer, err := s.repository.GetByID(ctx, id) if err != nil { s.logger.Error("Failed to get customer for update", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return nil, errors.New("customer not found") } // Check if email already exists (if changing email) if form.Email != "" && form.Email != customer.Email { existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email) if existingCustomer != nil { return nil, errors.New("customer with this email already exists") } customer.Email = form.Email } // Check if username already exists (if changing username) if form.Username != "" && form.Username != customer.Username { existingCustomer, _ := s.repository.GetByUsername(ctx, form.Username) if existingCustomer != nil { return nil, errors.New("customer with this username already exists") } customer.Username = form.Username } // Handle company assignments if len(form.CompanyIDs) > 0 { // Verify that all company IDs exist var validCompanyIDs []string for _, companyID := range form.CompanyIDs { _, err := s.companyService.GetCompanyByID(ctx, companyID) if err != nil { s.logger.Error("Invalid company ID provided in update", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) return nil, errors.New("invalid company ID: " + companyID) } validCompanyIDs = append(validCompanyIDs, companyID) } customer.CompanyIDs = validCompanyIDs } if form.FullName != nil { customer.FullName = form.FullName } if form.Phone != nil { customer.Phone = form.Phone } // Update in database err = s.repository.Update(ctx, customer) if err != nil { s.logger.Error("Failed to update customer", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return nil, errors.New("failed to update customer") } s.logger.Info("Customer updated successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, }) return customer.ToResponse(nil), nil } // Delete deletes a customer (soft delete) func (s *customerService) Delete(ctx context.Context, id string) error { // Get customer to check if exists _, err := s.repository.GetByID(ctx, id) if err != nil { s.logger.Error("Failed to get customer for delete", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return errors.New("customer not found") } // Delete customer (soft delete) err = s.repository.Delete(ctx, id) if err != nil { s.logger.Error("Failed to delete customer", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return errors.New("failed to delete customer") } s.logger.Info("Customer deleted successfully", map[string]interface{}{ "customer_id": id, }) return nil } // UpdateStatus updates customer status func (s *customerService) UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error { // Get customer to check if exists _, err := s.repository.GetByID(ctx, id) if err != nil { return errors.New("customer not found") } // Update status status := CustomerStatus(form.Status) err = s.repository.UpdateStatus(ctx, id, status) if err != nil { s.logger.Error("Failed to update customer status", map[string]interface{}{ "error": err.Error(), "customer_id": id, "status": form.Status, }) return errors.New("failed to update customer status") } s.logger.Info("Customer status updated successfully", map[string]interface{}{ "customer_id": id, "status": form.Status, }) return nil } // AssignCompanies assigns companies to a customer func (s *customerService) AssignCompanies(ctx context.Context, customerID string, companyIDs []string) error { // Get customer to check if exists customer, err := s.repository.GetByID(ctx, customerID) if err != nil { return errors.New("customer not found") } // Verify companies exist and update customer's company IDs var validCompanyIDs []string for _, companyID := range companyIDs { _, err = s.companyService.GetCompanyByID(ctx, companyID) if err != nil { s.logger.Error("Company not found during assignment", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, "company_id": companyID, }) continue // Skip invalid company IDs } validCompanyIDs = append(validCompanyIDs, companyID) } // Update customer's company IDs customer.CompanyIDs = append(customer.CompanyIDs, validCompanyIDs...) customer.UpdatedAt = time.Now().Unix() err = s.repository.Update(ctx, customer) if err != nil { s.logger.Error("Failed to update customer with company assignments", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) return errors.New("failed to assign companies to customer") } s.logger.Info("Companies assigned to customer successfully", map[string]interface{}{ "customer_id": customerID, "company_ids": validCompanyIDs, }) return nil } // RemoveCompaniesFromCustomer removes companies from a customer func (s *customerService) RemoveCompanies(ctx context.Context, customerID string, companyIDs []string) error { // Get customer to check if exists customer, err := s.repository.GetByID(ctx, customerID) if err != nil { return errors.New("customer not found") } // Remove company IDs from customer's list var updatedCompanyIDs []string for _, existingID := range customer.CompanyIDs { shouldRemove := false for _, removeID := range companyIDs { if existingID == removeID { shouldRemove = true break } } if !shouldRemove { updatedCompanyIDs = append(updatedCompanyIDs, existingID) } } // Update customer's company IDs customer.CompanyIDs = updatedCompanyIDs customer.UpdatedAt = time.Now().Unix() err = s.repository.Update(ctx, customer) if err != nil { s.logger.Error("Failed to update customer after removing company assignments", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) return errors.New("failed to remove companies from customer") } s.logger.Info("Companies removed from customer successfully", map[string]interface{}{ "customer_id": customerID, "company_ids": companyIDs, }) return nil } // Suspend suspends a customer func (s *customerService) Suspend(ctx context.Context, id string, reason string) error { // Get customer to check if exists _, err := s.repository.GetByID(ctx, id) if err != nil { return errors.New("customer not found") } // Update status to suspended err = s.repository.UpdateStatus(ctx, id, CustomerStatusSuspended) if err != nil { s.logger.Error("Failed to suspend customer", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return errors.New("failed to suspend customer") } s.logger.Info("Customer suspended successfully", map[string]interface{}{ "customer_id": id, "reason": reason, }) return nil } // Activate activates a customer func (s *customerService) Activate(ctx context.Context, id string) error { // Get customer to check if exists _, err := s.repository.GetByID(ctx, id) if err != nil { return errors.New("customer not found") } // Update status to active err = s.repository.UpdateStatus(ctx, id, CustomerStatusActive) if err != nil { s.logger.Error("Failed to activate customer", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return errors.New("failed to activate customer") } s.logger.Info("Customer activated successfully", map[string]interface{}{ "customer_id": id, }) return nil } // Login handles customer authentication func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) { s.logger.Info("Customer login attempt", map[string]interface{}{ "username": form.Username, }) // Get customer by username customer, err := s.repository.GetByUsername(ctx, form.Username) if err != nil { s.logger.Warn("Customer login failed - username not found", map[string]interface{}{ "username": form.Username, }) return nil, errors.New("invalid credentials") } // Check if customer is active if customer.Status != CustomerStatusActive { s.logger.Warn("Customer login failed - account not active", map[string]interface{}{ "username": form.Username, "customer_id": customer.ID, "status": string(customer.Status), }) return nil, errors.New("account is not active") } // Verify password err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.Password)) if err != nil { s.logger.Warn("Customer login failed - wrong password", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, }) return nil, errors.New("invalid credentials") } // Generate JWT tokens using authorization service companyID := "" if len(customer.CompanyIDs) > 0 { companyID = customer.CompanyIDs[0] // Use first company ID for JWT token } tokenPair, err := s.authService.GenerateTokenPair( customer.ID.Hex(), customer.Email, "customer", // Role for customers companyID, ) if err != nil { s.logger.Error("Failed to generate JWT tokens", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) return nil, errors.New("failed to generate authentication tokens") } err = s.repository.Login(ctx, customer.ID.Hex()) if err != nil { s.logger.Error("Failed to update customer last login", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) } companies, err := s.loadCompaniesForCustomer(ctx, customer) if err != nil { s.logger.Error("Failed to load companies for customer", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) } s.logger.Info("Customer login successful", map[string]interface{}{ "username": form.Username, "customer_id": customer.ID, "customer_type": string(customer.Type), }) return &AuthResponse{ Customer: customer.ToResponse(companies), AccessToken: tokenPair.AccessToken, RefreshToken: tokenPair.RefreshToken, ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now }, nil } // RefreshToken handles token refresh for customers func (s *customerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) { s.logger.Info("Customer token refresh attempt", map[string]interface{}{ "refresh_token": refreshToken[:10] + "...", // Log only first 10 chars for security }) // Validate refresh token and generate new access token newAccessToken, err := s.authService.RefreshAccessToken(refreshToken) if err != nil { s.logger.Warn("Failed to refresh access token", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("invalid or expired refresh token") } // Parse the new access token to get customer information validationResult, err := s.authService.ValidateAccessToken(newAccessToken) if err != nil { s.logger.Error("Failed to validate new access token", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("failed to generate valid access token") } if !validationResult.Valid { s.logger.Error("New access token validation failed", map[string]interface{}{ "error": validationResult.Error, }) return nil, errors.New("failed to generate valid access token") } // Get customer information customerID, err := primitive.ObjectIDFromHex(validationResult.Claims.UserID) if err != nil { s.logger.Error("Failed to parse customer ID from token", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("invalid token format") } customer, err := s.repository.GetByID(ctx, customerID.Hex()) if err != nil { s.logger.Error("Failed to get customer for token refresh", map[string]interface{}{ "error": err.Error(), "customer_id": customerID.Hex(), }) return nil, errors.New("customer not found") } // Check if customer is still active if customer.Status != CustomerStatusActive { return nil, errors.New("customer account is inactive") } companies, err := s.loadCompaniesForCustomer(ctx, customer) if err != nil { s.logger.Error("Failed to load companies for customer", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) } s.logger.Info("Access token refreshed successfully", map[string]interface{}{ "customer_id": customer.ID, "customer_type": string(customer.Type), }) return &AuthResponse{ Customer: customer.ToResponse(companies), AccessToken: newAccessToken, RefreshToken: refreshToken, // Return the same refresh token ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now }, nil } // GetProfile retrieves customer profile information func (s *customerService) GetProfile(ctx context.Context, customerID string) (*Customer, error) { s.logger.Info("Getting customer profile", map[string]interface{}{ "customer_id": customerID, }) customer, err := s.repository.GetByID(ctx, customerID) if err != nil { s.logger.Error("Failed to get customer profile", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) return nil, errors.New("customer not found") } s.logger.Info("Customer profile retrieved successfully", map[string]interface{}{ "customer_id": customerID, "customer_type": string(customer.Type), }) return customer, nil } // GetProfileWithCompanies retrieves customer profile information including companies func (s *customerService) GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error) { s.logger.Info("Getting customer profile with companies", map[string]interface{}{ "customer_id": customerID, }) customer, err := s.repository.GetByID(ctx, customerID) if err != nil { s.logger.Error("Failed to get customer profile with companies", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) return nil, errors.New("customer not found") } // Load companies for the customer companies, err := s.loadCompaniesForCustomer(ctx, customer) if err != nil { s.logger.Error("Failed to load companies for customer profile", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) // Continue without companies if loading fails companies = []*CompanySummary{} } customerResponse := customer.ToResponse(companies) s.logger.Info("Customer profile retrieved with companies successfully", map[string]interface{}{ "customer_id": customerID, "customer_type": string(customer.Type), }) return customerResponse, nil } // Logout handles customer logout func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error { s.logger.Info("Customer logout", map[string]interface{}{ "customer_id": customerID, "access_token": accessToken[:10] + "...", // Log only first 10 chars for security }) // Invalidate the access token err := s.authService.InvalidateToken(accessToken) if err != nil { s.logger.Error("Failed to invalidate access token", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) // Don't return error here as the customer should still be logged out } s.logger.Info("Customer logout successful", map[string]interface{}{ "customer_id": customerID, }) return nil } // loadCompaniesForCustomer loads company summaries for a customer func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) { if len(customer.CompanyIDs) == 0 { return []*CompanySummary{}, nil } // Load companies in batch using the new GetCompaniesByIDs method companies, err := s.companyService.GetCompaniesByIDs(ctx, customer.CompanyIDs) if err != nil { s.logger.Error("Failed to load companies for customer", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, "company_ids": customer.CompanyIDs, }) return []*CompanySummary{}, err } // Convert to CompanySummary format var companySummaries []*CompanySummary for _, company := range companies { companySummaries = append(companySummaries, &CompanySummary{ ID: company.ID.Hex(), Name: company.Name, }) } s.logger.Info("Companies loaded for customer", map[string]interface{}{ "customer_id": customer.ID, "requested_count": len(customer.CompanyIDs), "loaded_count": len(companySummaries), }) return companySummaries, nil }