Add configuration file and update server settings

- 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.
This commit is contained in:
n.nakhostin
2025-08-02 12:37:04 +03:30
parent 06dc1d5b7a
commit 9119e2383e
30 changed files with 7273 additions and 1106 deletions
+389 -425
View File
@@ -2,169 +2,69 @@ package customer
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"strings"
"time"
infrastructure "tm/infra"
"tm/pkg/logger"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
// CustomerService implements the CustomerService interface
type CustomerService struct {
customerRepo Repository
config *infrastructure.AuthConfig
logger logger.Logger
// 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(
customerRepo Repository,
config *infrastructure.AuthConfig,
logger logger.Logger,
) CustomerService {
return CustomerService{
customerRepo: customerRepo,
config: config,
logger: logger,
func NewCustomerService(repository Repository, logger logger.Logger) Service {
return &customerService{
repository: repository,
logger: logger,
}
}
// GetCustomers retrieves customers with pagination (for panel users)
func (s *CustomerService) GetCustomers(ctx context.Context, limit, offset int) ([]*Customer, error) {
s.logger.Info("Retrieving customers", map[string]interface{}{
"limit": limit,
"offset": offset,
})
customers, err := s.customerRepo.List(ctx, limit, offset)
if err != nil {
s.logger.Error("Failed to retrieve customers", map[string]interface{}{
"error": err.Error(),
"limit": limit,
"offset": offset,
})
return nil, errors.New("failed to retrieve customers")
// 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")
}
s.logger.Info("Customers retrieved successfully", map[string]interface{}{
"count": len(customers),
"limit": limit,
"offset": offset,
})
return customers, nil
}
// GetCustomerByID retrieves a customer by ID (for panel users)
func (s *CustomerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
s.logger.Info("Retrieving customer by ID", map[string]interface{}{
"customer_id": id.String(),
})
customer, err := s.customerRepo.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to retrieve customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return nil, errors.New("customer not found")
// Check if username already exists
existingCustomer, _ = s.repository.GetByUsername(ctx, form.Username)
if existingCustomer != nil {
return nil, errors.New("username already exists")
}
s.logger.Info("Customer retrieved successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return customer, nil
}
// GetCustomersByCompany retrieves customers by company ID with pagination (for panel users)
func (s *CustomerService) GetCustomersByCompany(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) {
s.logger.Info("Retrieving customers by company", map[string]interface{}{
"company_id": companyID.String(),
"limit": limit,
"offset": offset,
})
customers, err := s.customerRepo.GetByCompanyID(ctx, companyID, limit, offset)
if err != nil {
s.logger.Error("Failed to retrieve customers by company", map[string]interface{}{
"error": err.Error(),
"company_id": companyID.String(),
"limit": limit,
"offset": offset,
})
return nil, errors.New("failed to retrieve customers")
}
s.logger.Info("Customers by company retrieved successfully", map[string]interface{}{
"count": len(customers),
"company_id": companyID.String(),
"limit": limit,
"offset": offset,
})
return customers, nil
}
// UpdateCustomerStatus updates customer active status (for panel users)
func (s *CustomerService) UpdateCustomerStatus(ctx context.Context, customerID uuid.UUID, isActive bool, updatedBy uuid.UUID) error {
s.logger.Info("Updating customer status", map[string]interface{}{
"customer_id": customerID.String(),
"is_active": isActive,
"updated_by": updatedBy.String(),
})
// Get customer
customer, err := s.customerRepo.GetByID(ctx, customerID)
if err != nil {
s.logger.Error("Customer not found for status update", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
})
return errors.New("customer not found")
}
// Update status
customer.IsActive = isActive
// Save changes
if err := s.customerRepo.Update(ctx, customer); err != nil {
s.logger.Error("Failed to update customer status", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID.String(),
"is_active": isActive,
"updated_by": updatedBy.String(),
})
return errors.New("failed to update customer status")
}
s.logger.Info("Customer status updated successfully", map[string]interface{}{
"customer_id": customerID.String(),
"is_active": isActive,
"updated_by": updatedBy.String(),
})
return nil
}
// Register creates a new customer account
func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*AuthorizationResponse, error) {
s.logger.Info("Registering new customer", map[string]interface{}{
"email": req.Email,
})
// Check if customer already exists
existingCustomer, err := s.customerRepo.GetByEmail(ctx, req.Email)
if err == nil && existingCustomer != nil {
return nil, errors.New("customer 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 := s.hashPassword(req.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(),
@@ -172,388 +72,452 @@ func (s *CustomerService) Register(ctx context.Context, req RegisterForm) (*Auth
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(),
Email: req.Email,
Password: hashedPassword,
FirstName: req.FirstName,
LastName: req.LastName,
Mobile: req.Mobile,
IsActive: true,
IsVerified: false, // Require email verification
DeviceTokens: make([]string, 0),
UpdatedAt: time.Now().Unix(),
CreatedAt: time.Now().Unix(),
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{},
}
if err := s.customerRepo.Create(ctx, customer); err != nil {
// 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": req.Email,
"error": err.Error(),
"email": form.Email,
"username": form.Username,
})
return nil, errors.New("failed to create customer")
}
// Generate tokens
accessToken, err := s.generateAccessToken(customer)
if err != nil {
s.logger.Error("Failed to generate access token", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID.String(),
})
return nil, errors.New("failed to generate access token")
}
refreshToken, err := s.generateRefreshToken(customer)
if err != nil {
s.logger.Error("Failed to generate refresh token", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID.String(),
})
return nil, errors.New("failed to generate refresh token")
return nil, err
}
s.logger.Info("Customer registered successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
"company_id": customer.CompanyID.String(),
"username": customer.Username,
})
// TODO: Send verification email
s.sendVerificationEmail(ctx, customer)
return &AuthorizationResponse{
Customer: NewCustomerAggregate(*customer),
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
}, nil
return customer, nil
}
// Login authenticates a customer and returns tokens
func (s *CustomerService) Login(ctx context.Context, req LoginForm) (*AuthorizationResponse, error) {
s.logger.Info("Customer login attempt", map[string]interface{}{
"email": req.Email,
})
// 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)
}
// Get customer by email
customer, err := s.customerRepo.GetByEmail(ctx, req.Email)
if err != nil {
s.logger.Warn("Customer login failed - customer not found", map[string]interface{}{
"email": req.Email,
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.IsActive {
s.logger.Warn("Customer login failed - account inactive", map[string]interface{}{
"email": req.Email,
"customer_id": customer.ID.String(),
})
if customer.Status != CustomerStatusActive {
return nil, errors.New("account is inactive")
}
// Verify password
if !s.verifyPassword(req.Password, customer.Password) {
s.logger.Warn("Customer login failed - invalid password", map[string]interface{}{
"email": req.Email,
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 time
now := time.Now().Unix()
customer.LastLoginAt = &now
customer.UpdatedAt = now
if err := s.customerRepo.Update(ctx, customer); err != nil {
s.logger.Warn("Failed to update customer last login", map[string]interface{}{
// 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(),
})
// Don't fail login for this
}
// Generate tokens
accessToken, err := s.generateAccessToken(customer)
accessToken, refreshToken, expiresAt, err := s.generateTokens(customer.ID)
if err != nil {
s.logger.Error("Failed to generate access token", map[string]interface{}{
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 access token")
}
refreshToken, err := s.generateRefreshToken(customer)
if err != nil {
s.logger.Error("Failed to generate refresh token", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID.String(),
})
return nil, errors.New("failed to generate refresh token")
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,
"is_verified": customer.IsVerified,
})
return &AuthorizationResponse{
Customer: NewCustomerAggregate(*customer),
return &AuthResponse{
Customer: customer.ToResponse(),
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
ExpiresAt: expiresAt,
}, nil
}
// RefreshToken generates new tokens using refresh token
func (s *CustomerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthorizationResponse, error) {
// Parse and validate refresh token
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("invalid signing method")
}
return []byte(s.config.JWT.Secret), nil
})
if err != nil || !token.Valid {
return nil, errors.New("invalid refresh token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("invalid token claims")
}
// Verify token type
tokenType, ok := claims["type"].(string)
if !ok || tokenType != "refresh" {
return nil, errors.New("invalid token type")
}
// Verify user type
userType, ok := claims["user_type"].(string)
if !ok || userType != "customer" {
return nil, errors.New("invalid user type")
}
customerIDStr, ok := claims["customer_id"].(string)
if !ok {
return nil, errors.New("invalid customer ID in token")
}
customerID, err := uuid.Parse(customerIDStr)
if err != nil {
return nil, errors.New("invalid customer ID format")
}
// Get customer from database
customer, err := s.customerRepo.GetByID(ctx, customerID)
if err != nil {
return nil, errors.New("customer not found")
}
if !customer.IsActive {
return nil, errors.New("account is inactive")
}
// Generate new tokens
accessToken, err := s.generateAccessToken(customer)
if err != nil {
return nil, errors.New("failed to generate access token")
}
newRefreshToken, err := s.generateRefreshToken(customer)
if err != nil {
return nil, errors.New("failed to generate refresh token")
}
return &AuthorizationResponse{
Customer: NewCustomerAggregate(*customer),
AccessToken: accessToken,
RefreshToken: newRefreshToken,
ExpiresIn: int64(s.config.JWT.AccessTokenDuration.Seconds()),
}, nil
}
// ValidateToken validates a JWT token and returns the customer
func (s *CustomerService) ValidateToken(ctx context.Context, tokenString string) (*Customer, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("invalid signing method")
}
return []byte(s.config.JWT.Secret), nil
})
if err != nil || !token.Valid {
return nil, errors.New("invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("invalid token claims")
}
// Verify token type
tokenType, ok := claims["type"].(string)
if !ok || tokenType != "access" {
return nil, errors.New("invalid token type")
}
// Verify user type
userType, ok := claims["user_type"].(string)
if !ok || userType != "customer" {
return nil, errors.New("invalid user type")
}
customerIDStr, ok := claims["customer_id"].(string)
if !ok {
return nil, errors.New("invalid customer ID in token")
}
customerID, err := uuid.Parse(customerIDStr)
if err != nil {
return nil, errors.New("invalid customer ID format")
}
customer, err := s.customerRepo.GetByID(ctx, customerID)
if err != nil {
return nil, errors.New("customer not found")
}
if !customer.IsActive {
return nil, errors.New("account is inactive")
}
return customer, 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, oldPassword, newPassword string) error {
customer, err := s.customerRepo.GetByID(ctx, customerID)
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
if !s.verifyPassword(oldPassword, customer.Password) {
err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.OldPassword))
if err != nil {
return errors.New("invalid old password")
}
// Hash new password
hashedPassword, err := s.hashPassword(newPassword)
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 = hashedPassword
customer.UpdatedAt = time.Now().Unix()
if err := s.customerRepo.Update(ctx, customer); err != nil {
s.logger.Error("Failed to update customer password", map[string]interface{}{
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("Customer password changed successfully", map[string]interface{}{
s.logger.Info("Password changed successfully", map[string]interface{}{
"customer_id": customerID.String(),
})
return nil
}
// ResetPassword initiates password reset process
func (s *CustomerService) ResetPassword(ctx context.Context, email string) error {
// TODO: Implement password reset logic
// This would typically involve:
// 1. Generate reset token
// 2. Store token with expiration
// 3. Send reset email
return errors.New("password reset not implemented")
// 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
}
// VerifyEmail verifies customer's email address
func (s *CustomerService) VerifyEmail(ctx context.Context, customerID uuid.UUID, token string) error {
// TODO: Implement email verification logic
// This would typically involve:
// 1. Validate verification token
// 2. Update customer verification status
// 3. Send welcome email
return errors.New("email verification not implemented")
// 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
}
// ResendVerification resend verification email
func (s *CustomerService) ResendVerification(ctx context.Context, email string) error {
customer, err := s.customerRepo.GetByEmail(ctx, email)
// 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")
}
if customer.IsVerified {
return errors.New("email already verified")
// Create device token
deviceToken := DeviceToken{
Token: form.DeviceToken,
DeviceType: DeviceType(form.DeviceType),
}
// TODO: Send verification email
s.sendVerificationEmail(ctx, customer)
// 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
}
// Private helper methods
// 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")
}
func (s *CustomerService) hashPassword(password string) (string, error) {
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
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 string(hashedBytes), nil
}
func (s *CustomerService) verifyPassword(password, hashedPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
func (s *CustomerService) generateAccessToken(customer *Customer) (string, error) {
claims := jwt.MapClaims{
"customer_id": customer.ID.String(),
"email": customer.Email,
"company_id": customer.CompanyID.String(),
"user_type": "customer",
"exp": time.Now().Add(s.config.JWT.AccessTokenDuration).Unix(),
"iat": time.Now().Unix(),
"type": "access",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.config.JWT.Secret))
}
func (s *CustomerService) generateRefreshToken(customer *Customer) (string, error) {
claims := jwt.MapClaims{
"customer_id": customer.ID.String(),
"user_type": "customer",
"exp": time.Now().Add(s.config.JWT.RefreshTokenDuration).Unix(),
"iat": time.Now().Unix(),
"type": "refresh",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.config.JWT.Secret))
}
func (s *CustomerService) sendVerificationEmail(ctx context.Context, customer *Customer) {
// TODO: Implement email sending logic
_ = ctx
s.logger.Info("Verification email would be sent", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return hex.EncodeToString(bytes), nil
}