package customer import ( "context" "errors" "time" "tm/internal/company" "tm/pkg/logger" "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 CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error) GetCustomerByID(ctx context.Context, id string) (*Customer, error) GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) DeleteCustomer(ctx context.Context, id string, deletedBy *string) error // Customer listing and search ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error) GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error) // Customer management UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *string) error UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error // Company assignments AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error RemoveCompaniesFromCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error // Mobile-specific operations (simplified) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) UpdateCustomerMobile(ctx context.Context, id string, form *UpdateCustomerMobileForm, updatedBy *string) (*Customer, error) // Business operations VerifyCustomer(ctx context.Context, id string, verifiedBy *string) error SuspendCustomer(ctx context.Context, id string, reason string, suspendedBy *string) error ActivateCustomer(ctx context.Context, id string, activatedBy *string) error // Authentication operations Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) GetProfile(ctx context.Context, customerID string) (*Customer, error) GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error) Logout(ctx context.Context, customerID 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, } } // CreateCustomer creates a new customer func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, 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 registration number already exists (for company customers) if form.Type == string(CustomerTypeCompany) && form.RegistrationNumber != nil { existingCustomer, _ = s.repository.GetByRegistrationNumber(ctx, *form.RegistrationNumber) if existingCustomer != nil { return nil, errors.New("company with this registration number already exists") } } // Check if tax ID already exists (for company customers) if form.Type == string(CustomerTypeCompany) && form.TaxID != nil { existingCustomer, _ = s.repository.GetByTaxID(ctx, *form.TaxID) if existingCustomer != nil { return nil, errors.New("company with this tax ID already exists") } } // Prepare company assignments var companyIDs []string if len(form.CompanyIDs) > 0 { // Verify that all company IDs exist 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: CustomerStatusPending, CompanyIDs: companyIDs, FirstName: form.FirstName, LastName: form.LastName, FullName: form.FullName, Username: form.Username, Email: form.Email, Password: string(hashedPassword), Phone: form.Phone, Mobile: form.Mobile, RegistrationNumber: form.RegistrationNumber, TaxID: form.TaxID, Industry: form.Industry, Address: s.convertAddressForm(form.Address), BusinessType: form.BusinessType, EmployeeCount: form.EmployeeCount, AnnualRevenue: form.AnnualRevenue, FoundedYear: form.FoundedYear, ContactPerson: s.convertContactPersonForm(form.ContactPerson), IsVerified: false, IsCompliant: false, Language: s.getDefaultLanguage(form.Language), Currency: s.getDefaultCurrency(form.Currency), Timezone: s.getDefaultTimezone(form.Timezone), CreatedBy: createdBy, } // Save to database err = s.repository.Create(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, "created_by": *createdBy, }) return customer, nil } // GetCustomerByID retrieves a customer by ID func (s *customerService) GetCustomerByID(ctx context.Context, id string) (*Customer, 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 } s.logger.Info("Customer retrieved successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, }) return customer, nil } // GetCustomerByIDWithCompanies retrieves a customer by ID and loads associated companies func (s *customerService) GetCustomerByIDWithCompanies(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 with companies", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return nil, err } // Load companies for the customer 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, }) // Continue without companies if loading fails companies = []*CompanySummary{} } customerResponse := customer.ToResponseWithCompanies(companies) s.logger.Info("Customer retrieved with companies successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, }) return customerResponse, nil } // UpdateCustomer updates a customer func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, 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 != nil && *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 != nil && *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 } // Check if registration number already exists (if changing) if form.RegistrationNumber != nil && *form.RegistrationNumber != *customer.RegistrationNumber { existingCustomer, _ := s.repository.GetByRegistrationNumber(ctx, *form.RegistrationNumber) if existingCustomer != nil { return nil, errors.New("company with this registration number already exists") } customer.RegistrationNumber = form.RegistrationNumber } // Check if tax ID already exists (if changing) if form.TaxID != nil && *form.TaxID != *customer.TaxID { existingCustomer, _ := s.repository.GetByTaxID(ctx, *form.TaxID) if existingCustomer != nil { return nil, errors.New("company with this tax ID already exists") } customer.TaxID = form.TaxID } // Update fields if provided if form.Type != nil { customer.Type = CustomerType(*form.Type) } // 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.FirstName != nil { customer.FirstName = form.FirstName } if form.LastName != nil { customer.LastName = form.LastName } if form.FullName != nil { customer.FullName = form.FullName } if form.Phone != nil { customer.Phone = form.Phone } if form.Mobile != nil { customer.Mobile = form.Mobile } if form.Industry != nil { customer.Industry = form.Industry } if form.Address != nil { customer.Address = s.convertAddressForm(form.Address) } if form.BusinessType != nil { customer.BusinessType = form.BusinessType } if form.EmployeeCount != nil { customer.EmployeeCount = form.EmployeeCount } if form.AnnualRevenue != nil { customer.AnnualRevenue = form.AnnualRevenue } if form.FoundedYear != nil { customer.FoundedYear = form.FoundedYear } if form.ContactPerson != nil { customer.ContactPerson = s.convertContactPersonForm(form.ContactPerson) } if form.Language != nil { customer.Language = *form.Language } if form.Currency != nil { customer.Currency = *form.Currency } if form.Timezone != nil { customer.Timezone = *form.Timezone } // Set updated by customer.UpdatedBy = updatedBy customer.UpdatedAt = time.Now().Unix() // 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, "updated_by": *updatedBy, }) return customer, nil } // DeleteCustomer deletes a customer (soft delete) func (s *customerService) DeleteCustomer(ctx context.Context, id string, deletedBy *string) error { // Get customer to check if exists customer, err := s.repository.GetByID(ctx, id) if err != nil { return errors.New("customer not found") } // Set updated by before deletion customer.UpdatedBy = deletedBy // 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, "deleted_by": deletedBy, }) return nil } // ListCustomers lists customers with search and filters func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) { // Set defaults limit := 20 if form.Limit != nil { limit = *form.Limit } offset := 0 if form.Offset != nil { offset = *form.Offset } search := "" if form.Search != nil { search = *form.Search } sortBy := "created_at" if form.SortBy != nil { sortBy = *form.SortBy } sortOrder := "desc" if form.SortOrder != nil { sortOrder = *form.SortOrder } // Parse company ID if provided var companyID *string if form.CompanyID != nil { companyID = form.CompanyID } // Get customers customers, err := s.repository.Search(ctx, search, form.Type, form.Status, companyID, form.Industry, form.IsVerified, form.IsCompliant, form.Language, form.Currency, limit, offset, sortBy, sortOrder) if err != nil { s.logger.Error("Failed to list customers", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("failed to list customers") } // Convert to responses var customerResponses []*CustomerResponse for _, customer := range customers { customerResponses = append(customerResponses, customer.ToResponse()) } // TODO: Implement proper count for pagination metadata total := int64(len(customers)) // This should be a separate count query totalPages := (total + int64(limit) - 1) / int64(limit) return &CustomerListResponse{ Customers: customerResponses, Total: total, Limit: limit, Offset: offset, TotalPages: int(totalPages), }, nil } // ListCustomersWithCompanies lists customers with search and filters, including companies func (s *customerService) ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) { // Set defaults limit := 20 if form.Limit != nil { limit = *form.Limit } offset := 0 if form.Offset != nil { offset = *form.Offset } search := "" if form.Search != nil { search = *form.Search } sortBy := "created_at" if form.SortBy != nil { sortBy = *form.SortBy } sortOrder := "desc" if form.SortOrder != nil { sortOrder = *form.SortOrder } // Parse company ID if provided var companyID *string if form.CompanyID != nil { companyID = form.CompanyID } // Get customers customers, err := s.repository.Search(ctx, search, form.Type, form.Status, companyID, form.Industry, form.IsVerified, form.IsCompliant, form.Language, form.Currency, limit, offset, sortBy, sortOrder) if err != nil { s.logger.Error("Failed to list customers with companies", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("failed to list customers with companies") } // Convert to responses var customerResponses []*CustomerResponse for _, customer := range customers { // Load companies for the customer companies, err := s.loadCompaniesForCustomer(ctx, customer) if err != nil { s.logger.Error("Failed to load companies for customer in list", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) // Continue without companies if loading fails companies = []*CompanySummary{} } customerResponse := customer.ToResponseWithCompanies(companies) customerResponses = append(customerResponses, customerResponse) } // TODO: Implement proper count for pagination metadata total := int64(len(customers)) // This should be a separate count query totalPages := (total + int64(limit) - 1) / int64(limit) return &CustomerListResponse{ Customers: customerResponses, Total: total, Limit: limit, Offset: offset, TotalPages: int(totalPages), }, nil } // GetCustomersByCompanyID retrieves customers by company ID with pagination func (s *customerService) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) { customers, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset) if err != nil { s.logger.Error("Failed to get customers by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) return nil, 0, errors.New("failed to get customers by company") } // Get total count total, err := s.repository.CountByCompanyID(ctx, companyID) if err != nil { s.logger.Error("Failed to count customers by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) return customers, 0, nil // Return customers even if count fails } return customers, total, nil } // GetCustomersByType retrieves customers by type with pagination func (s *customerService) GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error) { // This would need to be implemented in the repository // For now, we'll use the search method customerTypeStr := string(customerType) customers, err := s.repository.Search(ctx, "", &customerTypeStr, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc") if err != nil { s.logger.Error("Failed to get customers by type", map[string]interface{}{ "error": err.Error(), "customer_type": customerType, }) return nil, 0, errors.New("failed to get customers by type") } // Get total count total, err := s.repository.CountByType(ctx, customerType) if err != nil { s.logger.Error("Failed to count customers by type", map[string]interface{}{ "error": err.Error(), "customer_type": customerType, }) return customers, 0, nil } return customers, total, nil } // GetCustomersByStatus retrieves customers by status with pagination func (s *customerService) GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error) { // This would need to be implemented in the repository // For now, we'll use the search method statusStr := string(status) customers, err := s.repository.Search(ctx, "", nil, &statusStr, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc") if err != nil { s.logger.Error("Failed to get customers by status", map[string]interface{}{ "error": err.Error(), "status": status, }) return nil, 0, errors.New("failed to get customers by status") } // Get total count total, err := s.repository.CountByStatus(ctx, status) if err != nil { s.logger.Error("Failed to count customers by status", map[string]interface{}{ "error": err.Error(), "status": status, }) return customers, 0, nil } return customers, total, nil } // UpdateCustomerStatus updates customer status func (s *customerService) UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *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 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, "updated_by": updatedBy, }) return nil } // UpdateCustomerVerification updates customer verification status func (s *customerService) UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error { // Get customer to check if exists _, err := s.repository.GetByID(ctx, id) if err != nil { return errors.New("customer not found") } // Update verification err = s.repository.UpdateVerification(ctx, id, form.IsVerified, form.IsCompliant, form.ComplianceNotes) if err != nil { s.logger.Error("Failed to update customer verification", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return errors.New("failed to update customer verification") } s.logger.Info("Customer verification updated successfully", map[string]interface{}{ "customer_id": id, "is_verified": form.IsVerified, "is_compliant": form.IsCompliant, "updated_by": updatedBy, }) return nil } // AssignCompaniesToCustomer assigns companies to a customer func (s *customerService) AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *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.UpdatedBy = updatedBy 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, "updated_by": updatedBy, }) return nil } // RemoveCompaniesFromCustomer removes companies from a customer func (s *customerService) RemoveCompaniesFromCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *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.UpdatedBy = updatedBy 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, "updated_by": updatedBy, }) return nil } // CreateCustomerMobile creates a new customer via mobile app (simplified) func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, 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") } // Prepare company assignments var companyIDs []string if len(form.CompanyIDs) > 0 { // Verify that all company IDs exist for _, companyID := range form.CompanyIDs { _, err := s.companyService.GetCompanyByID(ctx, companyID) if err != nil { s.logger.Error("Invalid company ID provided in mobile form", map[string]interface{}{ "error": err.Error(), "company_id": companyID, }) return nil, errors.New("invalid company ID: " + companyID) } } companyIDs = form.CompanyIDs } // Create customer with mobile form customer := &Customer{ Type: CustomerType(form.Type), Status: CustomerStatusActive, CompanyIDs: companyIDs, FirstName: form.FirstName, LastName: form.LastName, FullName: form.FullName, Username: form.Username, Email: form.Email, Password: "", // Will be set after hashing Phone: form.Phone, Mobile: form.Mobile, Industry: form.Industry, IsVerified: false, IsCompliant: false, Language: "en", Currency: "USD", Timezone: "UTC", CreatedBy: createdBy, } // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost) if err != nil { s.logger.Error("Failed to hash password for mobile customer", map[string]interface{}{ "error": err.Error(), }) return nil, errors.New("failed to process password") } customer.Password = string(hashedPassword) // Save to database err = s.repository.Create(ctx, customer) if err != nil { s.logger.Error("Failed to create customer via mobile", map[string]interface{}{ "error": err.Error(), "email": form.Email, "type": form.Type, "created_by": createdBy, }) return nil, err } s.logger.Info("Customer created via mobile successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, "type": customer.Type, "company_ids": companyIDs, "created_by": *createdBy, }) return customer, nil } // UpdateCustomerMobile updates a customer via mobile app (simplified) func (s *customerService) UpdateCustomerMobile(ctx context.Context, id string, form *UpdateCustomerMobileForm, updatedBy *string) (*Customer, error) { // Get existing customer customer, err := s.repository.GetByID(ctx, id) if err != nil { return nil, errors.New("customer not found") } // Update fields if provided if form.FirstName != nil { customer.FirstName = form.FirstName } if form.LastName != nil { customer.LastName = form.LastName } if form.FullName != nil { customer.FullName = form.FullName } if form.Phone != nil { customer.Phone = form.Phone } if form.Mobile != nil { customer.Mobile = form.Mobile } // 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 mobile 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.Industry != nil { customer.Industry = form.Industry } // Set updated by customer.UpdatedBy = updatedBy // Update in database err = s.repository.Update(ctx, customer) if err != nil { s.logger.Error("Failed to update customer via mobile", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return nil, errors.New("failed to update customer") } s.logger.Info("Customer updated via mobile successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, "company_ids": customer.CompanyIDs, "updated_by": *updatedBy, }) return customer, nil } // VerifyCustomer verifies a customer func (s *customerService) VerifyCustomer(ctx context.Context, id string, verifiedBy *string) error { // Get customer to check if exists customer, err := s.repository.GetByID(ctx, id) if err != nil { return errors.New("customer not found") } // Update verification err = s.repository.UpdateVerification(ctx, id, true, customer.IsCompliant, customer.ComplianceNotes) if err != nil { s.logger.Error("Failed to verify customer", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return errors.New("failed to verify customer") } s.logger.Info("Customer verified successfully", map[string]interface{}{ "customer_id": id, "verified_by": verifiedBy, }) return nil } // SuspendCustomer suspends a customer func (s *customerService) SuspendCustomer(ctx context.Context, id string, reason string, suspendedBy *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, "suspended_by": suspendedBy, }) return nil } // ActivateCustomer activates a customer func (s *customerService) ActivateCustomer(ctx context.Context, id string, activatedBy *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, "activated_by": activatedBy, }) 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") } 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(), 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") } s.logger.Info("Access token refreshed successfully", map[string]interface{}{ "customer_id": customer.ID, "customer_type": string(customer.Type), }) return &AuthResponse{ Customer: customer.ToResponse(), 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.ToResponseWithCompanies(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 } // Helper methods for converting forms to domain objects func (s *customerService) convertAddressForm(form *AddressForm) *Address { if form == nil { return nil } return &Address{ Street: form.Street, City: form.City, State: form.State, PostalCode: form.PostalCode, Country: form.Country, AddressType: form.AddressType, IsDefault: form.IsDefault, } } func (s *customerService) convertContactPersonForm(form *ContactPersonForm) *ContactPerson { if form == nil { return nil } return &ContactPerson{ FirstName: form.FirstName, LastName: form.LastName, FullName: form.FullName, Position: form.Position, Email: form.Email, Phone: form.Phone, Mobile: form.Mobile, IsPrimary: form.IsPrimary, } } func (s *customerService) getDefaultLanguage(language *string) string { if language != nil { return *language } return "en" } func (s *customerService) getDefaultCurrency(currency *string) string { if currency != nil { return *currency } return "USD" } func (s *customerService) getDefaultTimezone(timezone *string) string { if timezone != nil { return *timezone } return "UTC" } // loadCompaniesForCustomer loads company summaries for a customer func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) { var companies []*CompanySummary // Load companies from CompanyIDs for _, companyID := range customer.CompanyIDs { company, err := s.companyService.GetCompanyByID(ctx, companyID) if err != nil { s.logger.Warn("Failed to load company for customer", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, "company_id": companyID, }) continue // Skip companies that can't be loaded } companies = append(companies, &CompanySummary{ ID: company.ID.Hex(), Name: company.Name, }) } return companies, nil }