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 }