1455 lines
45 KiB
Go
1455 lines
45 KiB
Go
package customer
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
"tm/internal/company"
|
|
"tm/pkg/glm"
|
|
"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
|
|
|
|
// Authentication operations
|
|
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
|
|
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
|
GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
|
|
Logout(ctx context.Context, id string, accessToken string) error
|
|
|
|
// 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
|
|
glmSDK *glm.SDK
|
|
}
|
|
|
|
// 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, glmSDK *glm.SDK) Service {
|
|
return &customerService{
|
|
repository: repository,
|
|
logger: logger,
|
|
authService: authService,
|
|
companyService: companyService,
|
|
redisClient: redisClient,
|
|
notification: notificationSDK,
|
|
validator: validator,
|
|
glmSDK: glmSDK,
|
|
}
|
|
}
|
|
|
|
// 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 validator
|
|
if !s.validator.IsValidUsername(form.Username) {
|
|
return nil, errors.New("invalid username format")
|
|
}
|
|
|
|
// Verify that all company IDs exist
|
|
var Companies []string
|
|
if len(form.Companies) > 0 {
|
|
for _, companyID := range form.Companies {
|
|
_, err := s.companyService.GetByID(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)
|
|
}
|
|
}
|
|
Companies = form.Companies
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
|
|
// Generate keywords using GLM AI
|
|
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) {
|
|
customers, total, err := s.repository.Search(ctx, form, pagination)
|
|
if err != nil {
|
|
s.logger.Error("Failed to search customers", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("failed to search customers")
|
|
}
|
|
|
|
var customerResponses []*CustomerResponse
|
|
for _, customer := range customers {
|
|
companies, err := s.loadCompaniesForCustomer(ctx, &customer)
|
|
if err != nil {
|
|
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customer.ID,
|
|
})
|
|
}
|
|
|
|
customerResponses = append(customerResponses, customer.ToResponse(companies))
|
|
}
|
|
|
|
return &CustomerListResponse{
|
|
Customers: customerResponses,
|
|
Meta: pagination.Response(total),
|
|
}, nil
|
|
}
|
|
|
|
func (s *customerService) SearchNotification(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]*CustomerNotificationResponse, error) {
|
|
customers, _, 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 customers {
|
|
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
|
|
}
|
|
|
|
// Handle company assignments
|
|
companies := make([]*CompanySummary, 0)
|
|
companiesChanged := false
|
|
if len(form.Companies) > 0 {
|
|
// Check if companies changed
|
|
if len(form.Companies) != len(customer.Companies) {
|
|
companiesChanged = true
|
|
} else {
|
|
companyMap := make(map[string]bool)
|
|
for _, id := range customer.Companies {
|
|
companyMap[id] = true
|
|
}
|
|
for _, id := range form.Companies {
|
|
if !companyMap[id] {
|
|
companiesChanged = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
customer.Companies = form.Companies
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
|
|
infoChanged := false
|
|
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) {
|
|
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
|
|
customerID := customer.ID.Hex()
|
|
go func() {
|
|
ctx := context.Background()
|
|
// Get fresh customer data for processing
|
|
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
|
|
}
|
|
|
|
// If GLM SDK is not available, keep manual keywords and set status to ready
|
|
if s.glmSDK == nil {
|
|
s.logger.Info("GLM SDK not available, keeping manual keywords", map[string]interface{}{
|
|
"customer_id": customerID,
|
|
})
|
|
cust.KeywordsStatus = KeywordsStatusReady
|
|
} else {
|
|
// Regenerate keywords based on updated customer info
|
|
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,
|
|
})
|
|
// Keep manual keywords but set status to failed
|
|
cust.KeywordsStatus = KeywordsStatusFailed
|
|
} else if len(keywords) > 0 {
|
|
// Update with AI-generated keywords
|
|
cust.Keywords = keywords
|
|
cust.KeywordsStatus = KeywordsStatusReady
|
|
} else {
|
|
// Empty keywords from GLM, keep manual keywords and set status to ready
|
|
s.logger.Warn("GLM returned empty keywords, keeping manual keywords", map[string]interface{}{
|
|
"customer_id": customerID,
|
|
})
|
|
cust.KeywordsStatus = KeywordsStatusReady
|
|
}
|
|
}
|
|
|
|
// Update customer with new status
|
|
err = s.repository.Update(ctx, cust)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update customer keywords status", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customerID,
|
|
})
|
|
} else {
|
|
s.logger.Info("Keywords processed successfully", map[string]interface{}{
|
|
"customer_id": customerID,
|
|
"status": cust.KeywordsStatus,
|
|
})
|
|
}
|
|
}()
|
|
} 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,
|
|
})
|
|
|
|
return customer.ToResponse(companies), 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 generates keywords for a customer using GLM AI based on their information
|
|
func (s *customerService) generateKeywords(ctx context.Context, customer *Customer) ([]string, error) {
|
|
// If GLM SDK is not available, return empty keywords
|
|
if s.glmSDK == nil {
|
|
s.logger.Warn("GLM SDK not available, skipping keyword generation", map[string]interface{}{
|
|
"customer_id": customer.ID,
|
|
})
|
|
return []string{}, nil
|
|
}
|
|
|
|
// Build customer information string for AI
|
|
var customerInfo strings.Builder
|
|
if customer.FullName != nil {
|
|
customerInfo.WriteString(fmt.Sprintf("Name: %s\n", *customer.FullName))
|
|
}
|
|
customerInfo.WriteString(fmt.Sprintf("Type: %s\n", customer.Type))
|
|
customerInfo.WriteString(fmt.Sprintf("Role: %s\n", customer.Role))
|
|
if customer.Phone != nil {
|
|
customerInfo.WriteString(fmt.Sprintf("Phone: %s\n", *customer.Phone))
|
|
}
|
|
|
|
// Get company information if available
|
|
if len(customer.Companies) > 0 {
|
|
customerInfo.WriteString("Companies: ")
|
|
for i, companyID := range customer.Companies {
|
|
comp, err := s.companyService.GetByID(ctx, companyID)
|
|
if err == nil {
|
|
if i > 0 {
|
|
customerInfo.WriteString(", ")
|
|
}
|
|
customerInfo.WriteString(comp.Name)
|
|
if comp.Industry != "" {
|
|
customerInfo.WriteString(fmt.Sprintf(" (Industry: %s)", comp.Industry))
|
|
}
|
|
}
|
|
}
|
|
customerInfo.WriteString("\n")
|
|
}
|
|
|
|
systemPrompt := `You are a keyword extraction assistant for a tender management system.
|
|
Analyze the customer information and generate 5-10 relevant keywords that would help match them with appropriate tenders.
|
|
Keywords should be:
|
|
- Relevant to tender/procurement categories
|
|
- Based on customer type, role, and company information
|
|
- Professional and specific
|
|
- Suitable for matching with tender descriptions
|
|
|
|
Return ONLY a comma-separated list of keywords, no explanations or additional text.`
|
|
|
|
userMessage := fmt.Sprintf("Generate keywords for this customer:\n\n%s", customerInfo.String())
|
|
|
|
s.logger.Info("Generating keywords using GLM AI", map[string]interface{}{
|
|
"customer_id": customer.ID,
|
|
})
|
|
|
|
response, err := s.glmSDK.QuickChatWithSystem(ctx, systemPrompt, userMessage)
|
|
if err != nil {
|
|
s.logger.Error("Failed to generate keywords using GLM AI", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customer.ID,
|
|
})
|
|
return []string{}, fmt.Errorf("failed to generate keywords: %w", err)
|
|
}
|
|
|
|
// Parse comma-separated keywords
|
|
keywordsStr := strings.TrimSpace(response)
|
|
if keywordsStr == "" {
|
|
return []string{}, nil
|
|
}
|
|
|
|
// Split by comma and clean up
|
|
keywordList := strings.Split(keywordsStr, ",")
|
|
keywords := make([]string, 0, len(keywordList))
|
|
for _, kw := range keywordList {
|
|
kw = strings.TrimSpace(kw)
|
|
if kw != "" {
|
|
keywords = append(keywords, kw)
|
|
}
|
|
}
|
|
|
|
s.logger.Info("Keywords generated successfully", map[string]interface{}{
|
|
"customer_id": customer.ID,
|
|
"keywords": keywords,
|
|
"count": len(keywords),
|
|
})
|
|
|
|
return keywords, nil
|
|
}
|