Files
tm_back/internal/customer/service.go
T
n.nakhostin ee830f8c1b Implement Company Search and Update API Documentation
- Introduced a new search functionality for companies, allowing advanced filtering capabilities including tags, business criteria, and location.
- Updated the API documentation to reflect changes in the search endpoint, enhancing clarity for API consumers.
- Refactored existing company-related API endpoints for consistency, including renaming and restructuring routes.
- Enhanced response structures to return company entities directly, simplifying the response handling in API endpoints.
- Removed unused query parameters and handlers, streamlining the company management functionality.
2025-09-08 11:23:15 +03:30

725 lines
22 KiB
Go

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