0da0c8b8c9
Added functionality to check for existing customers and users by phone number during registration and updates. Updated the handler, service, and repository layers to include phone number checks, ensuring unique phone numbers across customers and users.
1460 lines
45 KiB
Go
1460 lines
45 KiB
Go
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 {
|
|
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,
|
|
})
|
|
|
|
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" {
|
|
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,
|
|
})
|
|
|
|
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))
|
|
}
|
|
|
|
return &CustomerListResponse{
|
|
Customers: customerResponses,
|
|
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
|
|
}, nil
|
|
}
|
|
|
|
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 {
|
|
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,
|
|
})
|
|
|
|
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,
|
|
})
|
|
|
|
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,
|
|
})
|
|
|
|
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),
|
|
})
|
|
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),
|
|
})
|
|
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.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),
|
|
})
|
|
|
|
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,
|
|
})
|
|
|
|
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,
|
|
})
|
|
}
|
|
}()
|
|
}
|