package customer import ( "context" "crypto/rand" "encoding/json" "errors" "fmt" "math/big" "strings" "time" "tm/internal/company" "tm/pkg/audit" "tm/pkg/logger" "tm/pkg/notification" "tm/pkg/redis" "tm/pkg/response" "tm/pkg/authorization" "go.mongodb.org/mongo-driver/v2/bson" "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) SearchNotification(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]*CustomerNotificationResponse, error) GetCustomersByIDs(ctx context.Context, ids []string) ([]*CustomerNotificationResponse, error) GetCustomersByRole(ctx context.Context, role string) ([]*CustomerNotificationResponse, error) GetCustomersByCompanies(ctx context.Context, companies []string) ([]*CustomerNotificationResponse, 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 AdminResetPassword(ctx context.Context, actorID, targetID string) (*AdminResetPasswordResponse, 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) UpdateProfileKeywords(ctx context.Context, customerID string, keywords []string) (*CustomerResponse, error) Logout(ctx context.Context, id string, accessToken string) error // Forgot password operations RequestResetPassword(ctx context.Context, form *RequestResetPasswordForm) (*RequestResetPasswordResponse, error) VerifyOTP(ctx context.Context, form *VerifyOTPForm) (*VerifyOTPResponse, error) ResetPassword(ctx context.Context, form *ResetPasswordForm) (*ResetPasswordResponse, error) // Role assignment operations AssignRole(ctx context.Context, customerID string, form *AssignRoleForm) (*AssignRoleResponse, error) AssignCompanies(ctx context.Context, customerID string, Companies []string) error } // customerService implements the Service interface type customerService struct { repository Repository logger logger.Logger authService authorization.AuthorizationService companyService company.Service redisClient redis.Client notification notification.SDK validator ValidationService auditLogger audit.Logger } // New creates a new customer service func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client, notificationSDK notification.SDK, validator ValidationService, auditLogger audit.Logger) Service { return &customerService{ repository: repository, logger: logger, authService: authService, companyService: companyService, redisClient: redisClient, notification: notificationSDK, validator: validator, auditLogger: auditLogger, } } // 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") } // Check if username already exists existingCustomer, _ = s.repository.GetByUsername(ctx, strings.ToLower(form.Username)) if existingCustomer != nil { return nil, errors.New("customer with this username already exists") } if form.Phone != nil && *form.Phone != "" { existingCustomer, _ = s.repository.GetByPhone(ctx, *form.Phone) if existingCustomer != nil { return nil, errors.New("customer with this phone number already exists") } } // Check validator if !s.validator.IsValidUsername(form.Username) { return nil, errors.New("invalid username format") } var Companies []string if len(form.Companies) > 0 { validated, err := s.validateCompanyIDs(ctx, form.Companies) if err != nil { return nil, err } Companies = validated } // 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), Role: CustomerRole(form.Role), Status: CustomerStatusActive, Companies: Companies, FullName: form.FullName, Username: strings.ToLower(form.Username), Email: form.Email, Password: string(hashedPassword), Phone: form.Phone, } // Keyword auto-generation is handled by the AI service when integrated; backend leaves keywords empty on register. customer.KeywordsStatus = KeywordsStatusInProgress keywords, err := s.generateKeywords(ctx, customer) if err != nil { s.logger.Warn("Failed to generate keywords, continuing without keywords", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) keywords = []string{} customer.KeywordsStatus = KeywordsStatusFailed } else { customer.Keywords = keywords customer.KeywordsStatus = KeywordsStatusReady } // Save to database err = s.repository.Register(ctx, customer) if err != nil { if mapped := mapCustomerWriteError(err); mapped != err { return nil, mapped } s.logger.Error("Failed to create customer", map[string]interface{}{ "error": err.Error(), "email": form.Email, "username": form.Username, }) return nil, err } 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 created successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, "type": customer.Type, "company_ids": Companies, }) s.auditLogger.Log(ctx, audit.Entry{ Action: audit.ActionCustomerCreate, TargetType: audit.TargetTypeCustomer, TargetID: customer.ID.Hex(), Success: true, Metadata: map[string]string{"role": string(customer.Role)}, }) go s.notification.SendEmail(context.Background(), notification.Model{ Recipient: customer.Email, Title: "Welcome to Opplens", Message: fmt.Sprintf("Welcome to Opplens\nUsername: %s\nPassword: %s", strings.ToLower(customer.Username), form.Password), Type: "info", Priority: "important", UserID: customer.ID.Hex(), }) return customer.ToResponse(companies), 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 { if err.Error() == "customer not found" { s.auditLogger.Log(ctx, audit.Entry{ Action: audit.ActionCustomerRead, TargetType: audit.TargetTypeCustomer, TargetID: id, Success: false, Metadata: map[string]string{"reason": "not_found"}, }) return nil, errors.New("customer not found") } 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.Companies) > 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, }) s.auditLogger.Log(ctx, audit.Entry{ Action: audit.ActionCustomerRead, TargetType: audit.TargetTypeCustomer, TargetID: id, Success: true, }) return customer.ToResponse(companies), nil } // Search searches for customers func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error) { page, 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, err } var customerResponses []*CustomerResponse for _, customer := range page.Items { 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)) } s.auditLogger.Log(ctx, audit.Entry{ Action: audit.ActionCustomerList, Success: true, Metadata: audit.MergeMetadata( customerSearchFilterMetadata(form), audit.PaginationMetadata(pagination.Limit, pagination.Offset, page.TotalCount), ), }) return &CustomerListResponse{ Customers: customerResponses, Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset), }, nil } func customerSearchFilterMetadata(form *SearchCustomersForm) map[string]string { if form == nil { return nil } return audit.MergeMetadata( audit.FlagMetadata("has_search", form.Search != nil && *form.Search != ""), audit.FlagMetadata("has_full_name", form.FullName != nil && *form.FullName != ""), audit.FlagMetadata("has_email_filter", form.Email != nil && *form.Email != ""), audit.OptionalStringMetadata("status", form.Status), audit.OptionalStringMetadata("role", form.Role), audit.OptionalStringMetadata("type", form.Type), audit.OptionalStringMetadata("company_id", form.CompanyID), ) } func (s *customerService) SearchNotification(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]*CustomerNotificationResponse, error) { page, 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 []*CustomerNotificationResponse for _, customer := range page.Items { customerResponses = append(customerResponses, customer.ToNotificationResponse()) } return customerResponses, nil } // GetCustomersByIDs retrieves customers by IDs func (s *customerService) GetCustomersByIDs(ctx context.Context, ids []string) ([]*CustomerNotificationResponse, error) { customers, err := s.repository.GetByIDs(ctx, ids) if err != nil { return nil, err } var customerResponses []*CustomerNotificationResponse for _, customer := range customers { customerResponses = append(customerResponses, customer.ToNotificationResponse()) } return customerResponses, nil } // GetCustomersByRole retrieves customers by role func (s *customerService) GetCustomersByRole(ctx context.Context, role string) ([]*CustomerNotificationResponse, error) { customers, err := s.repository.GetByRole(ctx, CustomerRole(role)) if err != nil { s.logger.Error("Failed to get customers by role", map[string]interface{}{ "error": err.Error(), "role": role, }) return nil, err } var customerResponses []*CustomerNotificationResponse for _, customer := range customers { customerResponses = append(customerResponses, customer.ToNotificationResponse()) } return customerResponses, nil } // GetCustomersByCompanies retrieves customers by companies func (s *customerService) GetCustomersByCompanies(ctx context.Context, companies []string) ([]*CustomerNotificationResponse, error) { customers, err := s.repository.GetByCompanies(ctx, companies) if err != nil { s.logger.Error("Failed to get customers by companies", map[string]interface{}{ "error": err.Error(), "companies": companies, }) return nil, err } var customerResponses []*CustomerNotificationResponse for _, customer := range customers { customerResponses = append(customerResponses, customer.ToNotificationResponse()) } return customerResponses, 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 validator if !s.validator.IsValidUsername(form.Username) { return nil, errors.New("invalid username format") } changeUsername := false // Check if username already exists (if changing username) if form.Username != "" && strings.ToLower(form.Username) != customer.Username { existingCustomer, _ := s.repository.GetByUsername(ctx, strings.ToLower(form.Username)) if existingCustomer != nil { return nil, errors.New("customer with this username already exists") } customer.Username = strings.ToLower(form.Username) changeUsername = true } companiesChanged := false if form.Companies != nil { companyIDs, validateErr := s.validateCompanyIDs(ctx, *form.Companies) if validateErr != nil { return nil, validateErr } companiesChanged = !companyIDsEqual(customer.Companies, companyIDs) customer.Companies = companyIDs } infoChanged := false if form.Type != "" && customer.Type != CustomerType(form.Type) { customer.Type = CustomerType(form.Type) infoChanged = true } if form.FullName != nil && (customer.FullName == nil || *form.FullName != *customer.FullName) { customer.FullName = form.FullName infoChanged = true } if form.Phone != nil && (customer.Phone == nil || *form.Phone != *customer.Phone) { if *form.Phone != "" { existingCustomer, _ := s.repository.GetByPhone(ctx, *form.Phone) if existingCustomer != nil && existingCustomer.ID.Hex() != id { return nil, errors.New("customer with this phone number already exists") } } customer.Phone = form.Phone infoChanged = true } // Update role if provided if form.Role != "" && customer.Role != CustomerRole(form.Role) { customer.Role = CustomerRole(form.Role) infoChanged = true } // Handle keywords if form.Keywords != nil { // Keywords explicitly provided by customer, set status to in_progress // Customer's manual keywords are saved, but we'll enhance them with AI customer.Keywords = form.Keywords customer.KeywordsStatus = KeywordsStatusInProgress // Save customer with in_progress status first, then process asynchronously s.processKeywordsAsync(customer.ID.Hex()) } else if infoChanged || companiesChanged { // Customer info changed, regenerate keywords customer.KeywordsStatus = KeywordsStatusInProgress keywords, err := s.generateKeywords(ctx, customer) if err != nil { s.logger.Warn("Failed to regenerate keywords, keeping existing keywords", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) customer.KeywordsStatus = KeywordsStatusFailed } else { customer.Keywords = keywords customer.KeywordsStatus = KeywordsStatusReady } } // Update in database err = s.repository.Update(ctx, customer) if err != nil { if mapped := mapCustomerWriteError(err); mapped != err { return nil, mapped } s.logger.Error("Failed to update customer", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return nil, errors.New("failed to update customer") } if changeUsername { go s.notification.SendEmail(context.Background(), notification.Model{ Recipient: customer.Email, Title: "Username Updated", Message: fmt.Sprintf("Your username has been updated to %s", customer.Username), Type: "info", Priority: "important", UserID: customer.ID.Hex(), }) } s.logger.Info("Customer updated successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, }) s.auditLogger.Log(ctx, audit.Entry{ Action: audit.ActionCustomerUpdate, TargetType: audit.TargetTypeCustomer, TargetID: id, Success: true, }) responseCompanies, err := s.loadCompaniesForCustomer(ctx, customer) if err != nil { s.logger.Error("Failed to load companies for customer response", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) responseCompanies = []*CompanySummary{} } return customer.ToResponse(responseCompanies), 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, }) s.auditLogger.Log(ctx, audit.Entry{ Action: audit.ActionCustomerDelete, TargetType: audit.TargetTypeCustomer, TargetID: id, Success: true, }) return nil } // UpdateStatus updates customer status func (s *customerService) UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error { // Get customer to check if exists customer, 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, }) s.auditLogger.Log(ctx, audit.Entry{ Action: audit.ActionCustomerStatusChange, TargetType: audit.TargetTypeCustomer, TargetID: id, Success: true, Metadata: map[string]string{"status": form.Status}, }) go s.notification.SendEmail(context.Background(), notification.Model{ Recipient: customer.Email, Title: "Account Status Updated", Message: fmt.Sprintf("Your account has been %s", strings.ToUpper(form.Status)), Type: "warning", Priority: "important", UserID: customer.ID.Hex(), }) return nil } // AssignCompanies assigns companies to a customer func (s *customerService) AssignCompanies(ctx context.Context, customerID string, Companies []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 validCompanies []string for _, companyID := range Companies { _, err = s.companyService.GetByID(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 } validCompanies = append(validCompanies, companyID) } // Update customer's company IDs customer.Companies = make([]string, 0) customer.Companies = append(customer.Companies, validCompanies...) 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": validCompanies, }) return nil } // RemoveCompanies removes companies from a customer func (s *customerService) RemoveCompanies(ctx context.Context, customerID string, Companies []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 updatedCompanies []string for _, existingID := range customer.Companies { shouldRemove := false for _, removeID := range Companies { if existingID == removeID { shouldRemove = true break } } if !shouldRemove { updatedCompanies = append(updatedCompanies, existingID) } } // Update customer's company IDs customer.Companies = updatedCompanies customer.UpdatedAt = time.Now().Unix() err = s.repository.Update(ctx, customer) if err != nil { s.logger.Error("Failed to update customer after removing companies", 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": Companies, }) 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": strings.ToLower(form.Username), }) // Get customer by username customer, err := s.repository.GetByUsername(ctx, strings.ToLower(form.Username)) if err != nil { s.logger.Warn("Customer login failed - username not found", map[string]interface{}{ "username": strings.ToLower(form.Username), }) s.auditLogger.Log(ctx, audit.Entry{ Action: audit.ActionAuthLoginFailure, ActorType: audit.ActorTypeCustomer, TargetType: audit.TargetTypeCustomer, Success: false, }) 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": strings.ToLower(form.Username), "customer_id": customer.ID, "status": string(customer.Status), }) s.auditLogger.Log(ctx, audit.Entry{ ActorID: customer.ID.Hex(), ActorType: audit.ActorTypeCustomer, TargetType: audit.TargetTypeCustomer, TargetID: customer.ID.Hex(), Action: audit.ActionAuthLoginFailure, Success: false, Metadata: map[string]string{"reason": "inactive"}, }) 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, }) s.auditLogger.Log(ctx, audit.Entry{ ActorID: customer.ID.Hex(), ActorType: audit.ActorTypeCustomer, TargetType: audit.TargetTypeCustomer, TargetID: customer.ID.Hex(), Action: audit.ActionAuthLoginFailure, Success: false, }) return nil, errors.New("invalid credentials") } // Generate JWT tokens using authorization service companyID := "" if len(customer.Companies) > 0 { companyID = customer.Companies[0] // Use first company ID for JWT token } tokenPair, err := s.authService.GenerateTokenPair( customer.ID.Hex(), customer.Email, string(customer.Role), // Use customer's actual role 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(), form.DeviceToken) 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), }) s.auditLogger.Log(ctx, audit.Entry{ ActorID: customer.ID.Hex(), ActorType: audit.ActorTypeCustomer, TargetType: audit.TargetTypeCustomer, TargetID: customer.ID.Hex(), Action: audit.ActionAuthLoginSuccess, Success: true, Metadata: map[string]string{"role": string(customer.Role)}, }) 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 := bson.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 } // 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 } // UpdateProfileKeywords updates customer profile keywords and triggers AI enhancement asynchronously. func (s *customerService) UpdateProfileKeywords(ctx context.Context, customerID string, keywords []string) (*CustomerResponse, error) { customer, err := s.repository.GetByID(ctx, customerID) if err != nil { s.logger.Error("Failed to get customer for profile keyword update", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) return nil, errors.New("customer not found") } customer.Keywords = keywords customer.KeywordsStatus = KeywordsStatusInProgress if err := s.repository.Update(ctx, customer); err != nil { s.logger.Error("Failed to save profile keywords", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) return nil, errors.New("failed to update profile keywords") } s.processKeywordsAsync(customerID) companies, err := s.loadCompaniesForCustomer(ctx, customer) if err != nil { s.logger.Error("Failed to load companies after profile keyword update", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) companies = []*CompanySummary{} } return customer.ToResponse(companies), 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, }) // get customer by id customer, err := s.repository.GetByID(ctx, customerID) if err != nil { s.logger.Error("Failed to get customer for logout", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) } // 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 return errors.New("failed to invalidate access token") } // delete device token from customer customer.DeviceToken = make([]string, 0) err = s.repository.Update(ctx, customer) if err != nil { s.logger.Error("Failed to update customer device token", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) } s.logger.Info("Customer logout successful", map[string]interface{}{ "customer_id": customerID, }) s.auditLogger.Log(ctx, audit.Entry{ ActorID: customerID, ActorType: audit.ActorTypeCustomer, TargetType: audit.TargetTypeCustomer, TargetID: customerID, Action: audit.ActionAuthLogout, Success: true, }) return nil } // RequestResetPassword handles password reset request func (s *customerService) RequestResetPassword(ctx context.Context, form *RequestResetPasswordForm) (*RequestResetPasswordResponse, error) { s.logger.Info("Password reset request", map[string]interface{}{ "email": form.Email, }) // Check if customer exists customer, err := s.repository.GetByEmail(ctx, form.Email) if err != nil { s.logger.Warn("Password reset request for non-existent email", map[string]interface{}{ "email": form.Email, }) // Return success even if email doesn't exist for security return &RequestResetPasswordResponse{ Message: "If the email exists, a password reset code has been sent", Success: true, }, nil } // Check if customer is active if customer.Status != CustomerStatusActive { s.logger.Warn("Password reset request for inactive customer", map[string]interface{}{ "email": form.Email, "customer_id": customer.ID, "status": string(customer.Status), }) return &RequestResetPasswordResponse{ Message: "If the email exists, a password reset code has been sent", Success: true, }, nil } // Generate 6-digit OTP otpCode, err := s.generateOTP() if err != nil { s.logger.Error("Failed to generate OTP", map[string]interface{}{ "error": err.Error(), "email": form.Email, }) return nil, errors.New("failed to generate verification code") } // Store OTP in Redis with 10 minutes expiry otpKey := fmt.Sprintf("otp:%s", form.Email) otpData := OTP{ Code: otpCode, Email: form.Email, ExpiresAt: time.Now().Add(10 * time.Minute).Unix(), CreatedAt: time.Now().Unix(), } otpDataJSON, err := json.Marshal(otpData) if err != nil { s.logger.Error("Failed to marshal OTP data", map[string]interface{}{ "error": err.Error(), "email": form.Email, }) return nil, errors.New("failed to process reset request") } err = s.redisClient.Set(ctx, otpKey, string(otpDataJSON), 10*time.Minute) if err != nil { s.logger.Error("Failed to store OTP in Redis", map[string]interface{}{ "error": err.Error(), "email": form.Email, }) return nil, errors.New("failed to process reset request") } // Send OTP via email go s.notification.SendEmail(context.Background(), notification.Model{ Recipient: form.Email, Title: "Password Reset", Message: fmt.Sprintf("Your password reset code is: %s", otpCode), Type: "info", Priority: "important", UserID: customer.ID.Hex(), }) s.logger.Info("Password reset OTP sent successfully", map[string]interface{}{ "email": form.Email, "customer_id": customer.ID, }) return &RequestResetPasswordResponse{ Message: "Password reset code sent to your email", Success: true, }, nil } // VerifyOTP handles OTP verification func (s *customerService) VerifyOTP(ctx context.Context, form *VerifyOTPForm) (*VerifyOTPResponse, error) { s.logger.Info("OTP verification attempt", map[string]interface{}{ "email": form.Email, }) // Get OTP from Redis otpKey := fmt.Sprintf("otp:%s", form.Email) otpDataStr, err := s.redisClient.Get(ctx, otpKey) if err != nil { s.logger.Warn("OTP verification failed - OTP not found or expired", map[string]interface{}{ "email": form.Email, }) return nil, errors.New("invalid or expired verification code") } var otpData OTP err = json.Unmarshal([]byte(otpDataStr), &otpData) if err != nil { s.logger.Error("Failed to unmarshal OTP data", map[string]interface{}{ "error": err.Error(), "email": form.Email, }) return nil, errors.New("invalid verification code format") } if otpData.Code != form.Code { s.logger.Warn("OTP verification failed - wrong code", map[string]interface{}{ "email": form.Email, }) return nil, errors.New("invalid verification code") } // Generate reset token resetToken, err := s.generateResetToken() if err != nil { s.logger.Error("Failed to generate reset token", map[string]interface{}{ "error": err.Error(), "email": form.Email, }) return nil, errors.New("failed to generate reset token") } // Store reset token in Redis with 30 minutes expiry tokenKey := fmt.Sprintf("reset_token:%s", resetToken) tokenData := ResetToken{ Token: resetToken, Email: form.Email, ExpiresAt: time.Now().Add(30 * time.Minute).Unix(), CreatedAt: time.Now().Unix(), } tokenDataJSON, err := json.Marshal(tokenData) if err != nil { s.logger.Error("Failed to marshal reset token data", map[string]interface{}{ "error": err.Error(), "email": form.Email, }) return nil, errors.New("failed to process verification") } err = s.redisClient.Set(ctx, tokenKey, string(tokenDataJSON), 30*time.Minute) if err != nil { s.logger.Error("Failed to store reset token in Redis", map[string]interface{}{ "error": err.Error(), "email": form.Email, }) return nil, errors.New("failed to process verification") } // Delete OTP from Redis after successful verification err = s.redisClient.Del(ctx, otpKey) if err != nil { s.logger.Warn("Failed to delete OTP after verification", map[string]interface{}{ "error": err.Error(), "email": form.Email, }) } s.logger.Info("OTP verified successfully", map[string]interface{}{ "email": form.Email, }) return &VerifyOTPResponse{ Message: "OTP verified successfully", Token: resetToken, Success: true, }, nil } // ResetPassword handles password reset func (s *customerService) ResetPassword(ctx context.Context, form *ResetPasswordForm) (*ResetPasswordResponse, error) { s.logger.Info("Password reset attempt", map[string]interface{}{ "token": form.Token, }) // Validate password strength if !s.isValidPassword(form.NewPassword) { return nil, errors.New("password must be at least 5 characters with uppercase, lowercase, and special character") } // Get reset token from Redis tokenKey := fmt.Sprintf("reset_token:%s", form.Token) tokenDataStr, err := s.redisClient.Get(ctx, tokenKey) if err != nil { s.logger.Warn("Password reset failed - invalid or expired token", map[string]interface{}{ "token": form.Token, }) return nil, errors.New("invalid or expired reset token") } var tokenData ResetToken err = json.Unmarshal([]byte(tokenDataStr), &tokenData) if err != nil { s.logger.Error("Failed to unmarshal reset token data", map[string]interface{}{ "error": err.Error(), "token": form.Token, }) return nil, errors.New("invalid reset token format") } if tokenData.Token != form.Token { s.logger.Warn("Password reset failed - wrong token", map[string]interface{}{ "token": form.Token, }) return nil, errors.New("invalid reset token") } // Get customer customer, err := s.repository.GetByEmail(ctx, tokenData.Email) if err != nil { s.logger.Error("Password reset failed - customer not found", map[string]interface{}{ "error": err.Error(), "email": tokenData.Email, }) return nil, errors.New("customer not found") } // Hash new password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.NewPassword), bcrypt.DefaultCost) if err != nil { s.logger.Error("Failed to hash new password", map[string]interface{}{ "error": err.Error(), "email": tokenData.Email, }) return nil, errors.New("failed to process new password") } // Update customer password customer.Password = string(hashedPassword) customer.UpdatedAt = time.Now().Unix() err = s.repository.Update(ctx, customer) if err != nil { s.logger.Error("Failed to update customer password", map[string]interface{}{ "error": err.Error(), "email": tokenData.Email, "customer_id": customer.ID, }) return nil, errors.New("failed to reset password") } // Delete reset token from Redis err = s.redisClient.Del(ctx, tokenKey) if err != nil { s.logger.Warn("Failed to delete reset token after password reset", map[string]interface{}{ "error": err.Error(), "email": tokenData.Email, }) } s.logger.Info("Password reset successfully", map[string]interface{}{ "email": tokenData.Email, "customer_id": customer.ID, }) go s.notification.SendEmail(context.Background(), notification.Model{ Recipient: tokenData.Email, Title: "Password Reset Success", Message: "Your password has been reset successfully", Type: "info", Priority: "important", UserID: customer.ID.Hex(), }) return &ResetPasswordResponse{ Message: "Password reset successfully", Success: true, }, nil } // generateOTP generates a 6-digit OTP func (s *customerService) generateOTP() (string, error) { // Generate 6 random digits otp := "" for i := 0; i < 6; i++ { num, err := rand.Int(rand.Reader, big.NewInt(10)) if err != nil { return "", err } otp += num.String() } return otp, nil } // generateResetToken generates a random reset token func (s *customerService) generateResetToken() (string, error) { // Generate 32 random bytes tokenBytes := make([]byte, 32) _, err := rand.Read(tokenBytes) if err != nil { return "", err } return fmt.Sprintf("%x", tokenBytes), nil } // isValidPassword validates password strength func (s *customerService) isValidPassword(password string) bool { if len(password) < 5 { return false } hasUpper := false hasLower := false hasSpecial := false for _, char := range password { if char >= 'A' && char <= 'Z' { hasUpper = true } else if char >= 'a' && char <= 'z' { hasLower = true } else if char == '!' || char == '@' || char == '#' || char == '$' || char == '%' || char == '^' || char == '&' || char == '*' { hasSpecial = true } } return hasUpper && hasLower && hasSpecial } func (s *customerService) validateCompanyIDs(ctx context.Context, companyIDs []string) ([]string, error) { if len(companyIDs) == 0 { return []string{}, nil } validated := make([]string, 0, len(companyIDs)) seen := make(map[string]struct{}, len(companyIDs)) for _, companyID := range companyIDs { if companyID == "" { continue } if _, exists := seen[companyID]; exists { continue } if _, err := bson.ObjectIDFromHex(companyID); err != nil { return nil, errors.New("invalid company ID format: " + companyID) } if _, err := s.companyService.GetByID(ctx, companyID); 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) } seen[companyID] = struct{}{} validated = append(validated, companyID) } return validated, nil } func companyIDsEqual(existing, updated []string) bool { if len(existing) != len(updated) { return false } counts := make(map[string]int, len(existing)) for _, id := range existing { counts[id]++ } for _, id := range updated { counts[id]-- if counts[id] < 0 { return false } } for _, count := range counts { if count != 0 { return false } } return true } // loadCompaniesForCustomer loads company summaries for a customer func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) { if len(customer.Companies) == 0 { return []*CompanySummary{}, nil } // Load companies in batch using the new GetCompaniesByIDs method companies, err := s.companyService.GetByIDs(ctx, customer.Companies) 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.Companies, }) return []*CompanySummary{}, err } // Convert to CompanySummary format var companySummaries []*CompanySummary for _, company := range companies { companySummaries = append(companySummaries, &CompanySummary{ ID: company.ID, Name: company.Name, }) } s.logger.Info("Companies loaded for customer", map[string]interface{}{ "customer_id": customer.ID, "requested_count": len(customer.Companies), "loaded_count": len(companySummaries), }) return companySummaries, nil } // AssignRole assigns a role to a customer func (s *customerService) AssignRole(ctx context.Context, customerID string, form *AssignRoleForm) (*AssignRoleResponse, error) { s.logger.Info("Assigning role to customer", map[string]interface{}{ "customer_id": customerID, "role": form.Role, }) // Get customer to check if exists customer, err := s.repository.GetByID(ctx, customerID) if err != nil { s.logger.Error("Failed to get customer for role assignment", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) return nil, errors.New("customer not found") } // Validate role role := CustomerRole(form.Role) if role != CustomerRoleAdmin && role != CustomerRoleAnalyst { s.logger.Error("Invalid role provided", map[string]interface{}{ "customer_id": customerID, "role": form.Role, }) return nil, errors.New("invalid role. Must be 'admin' or 'analyst'") } // Update customer role customer.Role = role customer.UpdatedAt = time.Now().Unix() err = s.repository.Update(ctx, customer) if err != nil { s.logger.Error("Failed to update customer role", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, "role": form.Role, }) return nil, errors.New("failed to assign role to customer") } s.logger.Info("Role assigned to customer successfully", map[string]interface{}{ "customer_id": customerID, "role": form.Role, }) return &AssignRoleResponse{ Message: "Role assigned successfully", Success: true, }, nil } // generateKeywords is reserved for future integration with the AI service; the backend does not generate keywords. func (s *customerService) generateKeywords(ctx context.Context, customer *Customer) ([]string, error) { _ = ctx _ = customer return []string{}, nil } func (s *customerService) processKeywordsAsync(customerID string) { go func() { ctx := context.Background() cust, err := s.repository.GetByID(ctx, customerID) if err != nil { s.logger.Error("Failed to get customer for keyword processing", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) return } keywords, err := s.generateKeywords(ctx, cust) if err != nil { s.logger.Warn("Failed to process keywords after manual update", map[string]interface{}{ "error": err.Error(), "customer_id": customerID, }) cust.KeywordsStatus = KeywordsStatusFailed } else if len(keywords) > 0 { cust.Keywords = keywords cust.KeywordsStatus = KeywordsStatusReady } else { s.logger.Debug("No auto-generated keywords from backend, keeping existing keywords", map[string]interface{}{ "customer_id": customerID, }) cust.KeywordsStatus = KeywordsStatusReady } if updateErr := s.repository.Update(ctx, cust); updateErr != nil { s.logger.Error("Failed to update customer keywords status", map[string]interface{}{ "error": updateErr.Error(), "customer_id": customerID, }) } else { s.logger.Info("Keywords processed successfully", map[string]interface{}{ "customer_id": customerID, "status": cust.KeywordsStatus, }) } }() }