9119e2383e
- Introduced a new `config.yaml` file for managing server, database, cache, queue, AI, scraping, logging, and rate limiting configurations. - Updated `docker-compose.yml` to reflect the new server port (8081) and adjusted health check endpoints accordingly. - Modified `Dockerfile` to expose the new server port. - Updated `README.md` to reflect changes in server configuration and added documentation for the new configuration structure. - Added test scripts for server and Swagger documentation testing. - Refactored customer domain structure to align with new configuration settings and improve maintainability.
524 lines
15 KiB
Go
524 lines
15 KiB
Go
package customer
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
"tm/pkg/logger"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Service defines business logic for customer operations
|
|
type Service interface {
|
|
RegisterCustomer(ctx context.Context, form *RegisterCustomerForm) (*Customer, error)
|
|
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
|
|
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
|
ChangePassword(ctx context.Context, customerID uuid.UUID, form *ChangePasswordForm) error
|
|
GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error)
|
|
UpdateCustomer(ctx context.Context, id uuid.UUID, form *UpdateCustomerForm) (*Customer, error)
|
|
AddDeviceToken(ctx context.Context, customerID uuid.UUID, form *AddDeviceTokenForm) error
|
|
RemoveDeviceToken(ctx context.Context, customerID uuid.UUID, form *RemoveDeviceTokenForm) error
|
|
ListCustomers(ctx context.Context, form *ListCustomersForm) ([]*Customer, int, error)
|
|
UpdateCustomerStatus(ctx context.Context, id uuid.UUID, form *UpdateCustomerStatusForm) error
|
|
GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error)
|
|
Logout(ctx context.Context, customerID uuid.UUID, deviceToken string) error
|
|
}
|
|
|
|
// customerService implements the Service interface
|
|
type customerService struct {
|
|
repository Repository
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewCustomerService creates a new customer service
|
|
func NewCustomerService(repository Repository, logger logger.Logger) Service {
|
|
return &customerService{
|
|
repository: repository,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// RegisterCustomer registers a new customer
|
|
func (s *customerService) RegisterCustomer(ctx context.Context, form *RegisterCustomerForm) (*Customer, error) {
|
|
// Check if email already exists
|
|
existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email)
|
|
if existingCustomer != nil {
|
|
return nil, errors.New("email already exists")
|
|
}
|
|
|
|
// Check if username already exists
|
|
existingCustomer, _ = s.repository.GetByUsername(ctx, form.Username)
|
|
if existingCustomer != nil {
|
|
return nil, errors.New("username already exists")
|
|
}
|
|
|
|
// Check if mobile already exists
|
|
existingCustomer, _ = s.repository.GetByMobile(ctx, form.Mobile)
|
|
if existingCustomer != nil {
|
|
return nil, errors.New("mobile number already exists")
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
// Parse optional fields
|
|
var companyID *uuid.UUID
|
|
if form.CompanyID != nil {
|
|
parsedID, err := uuid.Parse(*form.CompanyID)
|
|
if err != nil {
|
|
return nil, errors.New("invalid company ID")
|
|
}
|
|
companyID = &parsedID
|
|
}
|
|
|
|
var gender *Gender
|
|
if form.Gender != nil {
|
|
g := Gender(*form.Gender)
|
|
gender = &g
|
|
}
|
|
|
|
// Create customer
|
|
customer := &Customer{
|
|
ID: uuid.New(),
|
|
FullName: form.FullName,
|
|
Username: form.Username,
|
|
Email: form.Email,
|
|
Mobile: form.Mobile,
|
|
Password: string(hashedPassword),
|
|
NationalID: form.NationalID,
|
|
Gender: gender,
|
|
Birthdate: form.Birthdate,
|
|
ProfileImage: form.ProfileImage,
|
|
Status: CustomerStatusActive,
|
|
CompanyID: companyID,
|
|
IsVerified: false,
|
|
DeviceTokens: []DeviceToken{},
|
|
}
|
|
|
|
// Save to database
|
|
err = s.repository.Create(ctx, customer)
|
|
if err != nil {
|
|
s.logger.Error("Failed to create customer", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"email": form.Email,
|
|
"username": form.Username,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("Customer registered successfully", map[string]interface{}{
|
|
"customer_id": customer.ID.String(),
|
|
"email": customer.Email,
|
|
"username": customer.Username,
|
|
})
|
|
|
|
return customer, nil
|
|
}
|
|
|
|
// Login authenticates a customer
|
|
func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) {
|
|
// Find customer by email or mobile
|
|
var customer *Customer
|
|
var err error
|
|
|
|
if strings.Contains(form.EmailOrMobile, "@") {
|
|
customer, err = s.repository.GetByEmail(ctx, form.EmailOrMobile)
|
|
} else {
|
|
customer, err = s.repository.GetByMobile(ctx, form.EmailOrMobile)
|
|
}
|
|
|
|
if err != nil {
|
|
s.logger.Warn("Login attempt with invalid credentials", map[string]interface{}{
|
|
"email_or_mobile": form.EmailOrMobile,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
// Check if customer is active
|
|
if customer.Status != CustomerStatusActive {
|
|
return nil, errors.New("account is inactive")
|
|
}
|
|
|
|
// Verify password
|
|
err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.Password))
|
|
if err != nil {
|
|
s.logger.Warn("Login attempt with wrong password", map[string]interface{}{
|
|
"customer_id": customer.ID.String(),
|
|
"email": customer.Email,
|
|
})
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
// Update last login
|
|
err = s.repository.UpdateLastLogin(ctx, customer.ID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update last login", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customer.ID.String(),
|
|
})
|
|
}
|
|
|
|
// Generate tokens
|
|
accessToken, refreshToken, expiresAt, err := s.generateTokens(customer.ID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to generate tokens", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customer.ID.String(),
|
|
})
|
|
return nil, errors.New("failed to generate authentication tokens")
|
|
}
|
|
|
|
s.logger.Info("Customer logged in successfully", map[string]interface{}{
|
|
"customer_id": customer.ID.String(),
|
|
"email": customer.Email,
|
|
})
|
|
|
|
return &AuthResponse{
|
|
Customer: customer.ToResponse(),
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
ExpiresAt: expiresAt,
|
|
}, nil
|
|
}
|
|
|
|
// RefreshToken refreshes the access token
|
|
func (s *customerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) {
|
|
// TODO: Implement JWT refresh token validation
|
|
// For now, we'll return an error
|
|
return nil, errors.New("refresh token functionality not implemented")
|
|
}
|
|
|
|
// ChangePassword changes customer password
|
|
func (s *customerService) ChangePassword(ctx context.Context, customerID uuid.UUID, form *ChangePasswordForm) error {
|
|
// Get customer
|
|
customer, err := s.repository.GetByID(ctx, customerID)
|
|
if err != nil {
|
|
return errors.New("customer not found")
|
|
}
|
|
|
|
// Verify old password
|
|
err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.OldPassword))
|
|
if err != nil {
|
|
return errors.New("invalid old password")
|
|
}
|
|
|
|
// 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(),
|
|
"customer_id": customerID.String(),
|
|
})
|
|
return errors.New("failed to process new password")
|
|
}
|
|
|
|
// Update password
|
|
customer.Password = string(hashedPassword)
|
|
err = s.repository.Update(ctx, customer)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customerID.String(),
|
|
})
|
|
return errors.New("failed to update password")
|
|
}
|
|
|
|
s.logger.Info("Password changed successfully", map[string]interface{}{
|
|
"customer_id": customerID.String(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetCustomerByID retrieves a customer by ID
|
|
func (s *customerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
|
|
customer, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return customer, nil
|
|
}
|
|
|
|
// UpdateCustomer updates customer information
|
|
func (s *customerService) UpdateCustomer(ctx context.Context, id uuid.UUID, form *UpdateCustomerForm) (*Customer, error) {
|
|
// Get customer
|
|
customer, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, errors.New("customer not found")
|
|
}
|
|
|
|
// Update fields if provided
|
|
if form.FullName != nil {
|
|
customer.FullName = *form.FullName
|
|
}
|
|
|
|
if form.Username != nil {
|
|
// Check if username already exists
|
|
existingCustomer, _ := s.repository.GetByUsername(ctx, *form.Username)
|
|
if existingCustomer != nil && existingCustomer.ID != id {
|
|
return nil, errors.New("username already exists")
|
|
}
|
|
customer.Username = *form.Username
|
|
}
|
|
|
|
if form.Email != nil {
|
|
// Check if email already exists
|
|
existingCustomer, _ := s.repository.GetByEmail(ctx, *form.Email)
|
|
if existingCustomer != nil && existingCustomer.ID != id {
|
|
return nil, errors.New("email already exists")
|
|
}
|
|
customer.Email = *form.Email
|
|
}
|
|
|
|
if form.Mobile != nil {
|
|
// Check if mobile already exists
|
|
existingCustomer, _ := s.repository.GetByMobile(ctx, *form.Mobile)
|
|
if existingCustomer != nil && existingCustomer.ID != id {
|
|
return nil, errors.New("mobile number already exists")
|
|
}
|
|
customer.Mobile = *form.Mobile
|
|
}
|
|
|
|
if form.NationalID != nil {
|
|
customer.NationalID = form.NationalID
|
|
}
|
|
|
|
if form.Gender != nil {
|
|
g := Gender(*form.Gender)
|
|
customer.Gender = &g
|
|
}
|
|
|
|
if form.Birthdate != nil {
|
|
customer.Birthdate = form.Birthdate
|
|
}
|
|
|
|
if form.ProfileImage != nil {
|
|
customer.ProfileImage = form.ProfileImage
|
|
}
|
|
|
|
if form.Status != nil {
|
|
customer.Status = CustomerStatus(*form.Status)
|
|
}
|
|
|
|
// 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.String(),
|
|
})
|
|
return nil, errors.New("failed to update customer")
|
|
}
|
|
|
|
s.logger.Info("Customer updated successfully", map[string]interface{}{
|
|
"customer_id": id.String(),
|
|
})
|
|
|
|
return customer, nil
|
|
}
|
|
|
|
// AddDeviceToken adds a device token for push notifications
|
|
func (s *customerService) AddDeviceToken(ctx context.Context, customerID uuid.UUID, form *AddDeviceTokenForm) error {
|
|
// Verify customer exists
|
|
_, err := s.repository.GetByID(ctx, customerID)
|
|
if err != nil {
|
|
return errors.New("customer not found")
|
|
}
|
|
|
|
// Create device token
|
|
deviceToken := DeviceToken{
|
|
Token: form.DeviceToken,
|
|
DeviceType: DeviceType(form.DeviceType),
|
|
}
|
|
|
|
// Add to database
|
|
err = s.repository.AddDeviceToken(ctx, customerID, deviceToken)
|
|
if err != nil {
|
|
s.logger.Error("Failed to add device token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customerID.String(),
|
|
"token": form.DeviceToken,
|
|
})
|
|
return errors.New("failed to add device token")
|
|
}
|
|
|
|
s.logger.Info("Device token added successfully", map[string]interface{}{
|
|
"customer_id": customerID.String(),
|
|
"token": form.DeviceToken,
|
|
"device_type": form.DeviceType,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveDeviceToken removes a device token
|
|
func (s *customerService) RemoveDeviceToken(ctx context.Context, customerID uuid.UUID, form *RemoveDeviceTokenForm) error {
|
|
// Remove from database
|
|
err := s.repository.RemoveDeviceToken(ctx, customerID, form.DeviceToken)
|
|
if err != nil {
|
|
s.logger.Error("Failed to remove device token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customerID.String(),
|
|
"token": form.DeviceToken,
|
|
})
|
|
return errors.New("failed to remove device token")
|
|
}
|
|
|
|
s.logger.Info("Device token removed successfully", map[string]interface{}{
|
|
"customer_id": customerID.String(),
|
|
"token": form.DeviceToken,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListCustomers lists customers with search and filters
|
|
func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomersForm) ([]*Customer, int, error) {
|
|
// Set defaults
|
|
limit := 20
|
|
if form.Limit != nil {
|
|
limit = *form.Limit
|
|
}
|
|
|
|
offset := 0
|
|
if form.Offset != nil {
|
|
offset = *form.Offset
|
|
}
|
|
|
|
search := ""
|
|
if form.Search != nil {
|
|
search = *form.Search
|
|
}
|
|
|
|
sortBy := "created_at"
|
|
if form.SortBy != nil {
|
|
sortBy = *form.SortBy
|
|
}
|
|
|
|
sortOrder := "desc"
|
|
if form.SortOrder != nil {
|
|
sortOrder = *form.SortOrder
|
|
}
|
|
|
|
// Get customers
|
|
customers, err := s.repository.Search(ctx, search, form.Status, form.Gender, nil, limit, offset, sortBy, sortOrder)
|
|
if err != nil {
|
|
s.logger.Error("Failed to list customers", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, 0, errors.New("failed to list customers")
|
|
}
|
|
|
|
// TODO: Implement count for pagination metadata
|
|
total := len(customers) // This should be a separate count query
|
|
|
|
return customers, total, nil
|
|
}
|
|
|
|
// UpdateCustomerStatus updates customer status
|
|
func (s *customerService) UpdateCustomerStatus(ctx context.Context, id uuid.UUID, form *UpdateCustomerStatusForm) error {
|
|
// Get customer
|
|
customer, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("customer not found")
|
|
}
|
|
|
|
// Update status
|
|
customer.Status = CustomerStatus(form.Status)
|
|
|
|
// Update in database
|
|
err = s.repository.Update(ctx, customer)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update customer status", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": id.String(),
|
|
"status": form.Status,
|
|
})
|
|
return errors.New("failed to update customer status")
|
|
}
|
|
|
|
s.logger.Info("Customer status updated successfully", map[string]interface{}{
|
|
"customer_id": id.String(),
|
|
"status": form.Status,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAllDeviceTokens retrieves all device tokens for push notifications
|
|
func (s *customerService) GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error) {
|
|
tokens, err := s.repository.GetAllDeviceTokens(ctx)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get all device tokens", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("failed to get device tokens")
|
|
}
|
|
|
|
return tokens, nil
|
|
}
|
|
|
|
// Logout removes device token and logs the action
|
|
func (s *customerService) Logout(ctx context.Context, customerID uuid.UUID, deviceToken string) error {
|
|
// Remove device token
|
|
err := s.repository.RemoveDeviceToken(ctx, customerID, deviceToken)
|
|
if err != nil {
|
|
s.logger.Error("Failed to remove device token on logout", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"customer_id": customerID.String(),
|
|
"token": deviceToken,
|
|
})
|
|
return errors.New("failed to logout")
|
|
}
|
|
|
|
s.logger.Info("Customer logged out successfully", map[string]interface{}{
|
|
"customer_id": customerID.String(),
|
|
"token": deviceToken,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateTokens generates access and refresh tokens
|
|
func (s *customerService) generateTokens(customerID uuid.UUID) (string, string, int64, error) {
|
|
// Generate access token (simple implementation for now)
|
|
accessToken, err := s.generateRandomToken(32)
|
|
if err != nil {
|
|
return "", "", 0, err
|
|
}
|
|
|
|
// Generate refresh token
|
|
refreshToken, err := s.generateRandomToken(64)
|
|
if err != nil {
|
|
return "", "", 0, err
|
|
}
|
|
|
|
// Set expiration (1 hour from now)
|
|
expiresAt := time.Now().Add(1 * time.Hour).Unix()
|
|
|
|
return accessToken, refreshToken, expiresAt, nil
|
|
}
|
|
|
|
// generateRandomToken generates a random token
|
|
func (s *customerService) generateRandomToken(length int) (string, error) {
|
|
bytes := make([]byte, length)
|
|
_, err := rand.Read(bytes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|