package processor import ( "context" "fmt" "time" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" "apskel-pos-be/internal/util" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) type CustomerAuthProcessor interface { CheckPhoneNumber(ctx context.Context, req *contract.CheckPhoneRequest) (*models.CheckPhoneResponse, error) StartRegistration(ctx context.Context, req *contract.RegisterStartRequest) (*models.RegisterStartResponse, error) VerifyOtp(ctx context.Context, req *contract.RegisterVerifyOtpRequest) (*models.RegisterVerifyOtpResponse, error) SetPassword(ctx context.Context, req *contract.RegisterSetPasswordRequest) (*models.RegisterSetPasswordResponse, error) Login(ctx context.Context, req *contract.CustomerLoginRequest) (*models.CustomerLoginResponse, error) ResendOtp(ctx context.Context, req *contract.ResendOtpRequest) (*models.ResendOtpResponse, error) } type customerAuthProcessor struct { customerAuthRepo repository.CustomerAuthRepository otpProcessor OtpProcessor otpRepo repository.OtpRepository jwtSecret string tokenTTLMinutes int } func NewCustomerAuthProcessor(customerAuthRepo repository.CustomerAuthRepository, otpProcessor OtpProcessor, otpRepo repository.OtpRepository, jwtSecret string, tokenTTLMinutes int) CustomerAuthProcessor { return &customerAuthProcessor{ customerAuthRepo: customerAuthRepo, otpProcessor: otpProcessor, otpRepo: otpRepo, jwtSecret: jwtSecret, tokenTTLMinutes: tokenTTLMinutes, } } func (p *customerAuthProcessor) CheckPhoneNumber(ctx context.Context, req *contract.CheckPhoneRequest) (*models.CheckPhoneResponse, error) { // Check if phone number exists in database exists, err := p.customerAuthRepo.CheckPhoneNumberExists(ctx, req.PhoneNumber) if err != nil { return nil, fmt.Errorf("failed to check phone number: %w", err) } if !exists { // Phone number not registered return &models.CheckPhoneResponse{ Status: "NOT_REGISTERED", Message: "Phone number not registered. Please continue registration.", Data: &models.CheckPhoneResponseData{ PhoneNumber: req.PhoneNumber, }, }, nil } // Phone number exists, get customer details customer, err := p.customerAuthRepo.GetCustomerByPhoneNumber(ctx, req.PhoneNumber) if err != nil { return nil, fmt.Errorf("failed to get customer: %w", err) } if customer == nil { return nil, fmt.Errorf("customer not found") } // Check if customer has password set if customer.PasswordHash == nil || *customer.PasswordHash == "" { // Customer exists but no password set, send OTP for password setup otpSession, err := p.otpProcessor.CreateOtpSession(ctx, req.PhoneNumber, "password_setup") if err != nil { return nil, fmt.Errorf("failed to create OTP session: %w", err) } // Send OTP via WhatsApp if err := p.otpProcessor.SendOtpViaWhatsApp(req.PhoneNumber, otpSession.Code, "password setup"); err != nil { return nil, fmt.Errorf("failed to send OTP: %w", err) } return &models.CheckPhoneResponse{ Status: "OTP_REQUIRED", Message: "OTP sent for password setup.", Data: &models.CheckPhoneResponseData{ OtpToken: otpSession.Token, ExpiresIn: 300, }, }, nil } // Customer exists and has password set, validate password if provided if req.Password == "" { return &models.CheckPhoneResponse{ Status: "PASSWORD_REQUIRED", Message: "Password is required for login.", }, nil } // Validate password if err := bcrypt.CompareHashAndPassword([]byte(*customer.PasswordHash), []byte(req.Password)); err != nil { return &models.CheckPhoneResponse{ Status: "INVALID_PASSWORD", Message: "Invalid password.", }, nil } // Generate JWT tokens using customer JWT util accessToken, refreshToken, _, err := util.GenerateCustomerTokens(customer, p.jwtSecret, p.tokenTTLMinutes) if err != nil { return nil, fmt.Errorf("failed to generate tokens: %w", err) } return &models.CheckPhoneResponse{ Status: "SUCCESS", Message: "Login successful.", Data: &models.CheckPhoneResponseData{ AccessToken: accessToken, RefreshToken: refreshToken, User: &models.CustomerUserData{ ID: customer.ID, Name: customer.Name, PhoneNumber: *customer.PhoneNumber, BirthDate: customer.BirthDate.Format("2006-01-02"), }, }, }, nil } func (p *customerAuthProcessor) StartRegistration(ctx context.Context, req *contract.RegisterStartRequest) (*models.RegisterStartResponse, error) { // Check if phone number already exists exists, err := p.customerAuthRepo.CheckPhoneNumberExists(ctx, req.PhoneNumber) if err != nil { return nil, fmt.Errorf("failed to check phone number: %w", err) } if exists { return nil, fmt.Errorf("phone number already registered") } // Generate registration token and create OTP session registrationToken := uuid.New().String() // Create OTP session for registration otpSession, err := p.otpProcessor.CreateOtpSession(ctx, req.PhoneNumber, "registration") if err != nil { return nil, fmt.Errorf("failed to create OTP session: %w", err) } // Store registration data in OTP session metadata registrationData := map[string]interface{}{ "registration_token": registrationToken, "name": req.Name, "birth_date": req.BirthDate, "step": "otp_sent", } // Update OTP session with registration metadata otpSession.Metadata = registrationData if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil { return nil, fmt.Errorf("failed to update OTP session with registration data: %w", err) } // Send OTP via WhatsApp if err := p.otpProcessor.SendOtpViaWhatsApp(req.PhoneNumber, otpSession.Code, "registration"); err != nil { return nil, fmt.Errorf("failed to send OTP: %w", err) } return &models.RegisterStartResponse{ Status: "PENDING_OTP", Message: "OTP sent to phone number for verification.", Data: &models.RegisterStartResponseData{ RegistrationToken: registrationToken, OtpToken: otpSession.Token, ExpiresIn: 300, }, }, nil } func (p *customerAuthProcessor) VerifyOtp(ctx context.Context, req *contract.RegisterVerifyOtpRequest) (*models.RegisterVerifyOtpResponse, error) { otpSession, err := p.otpRepo.GetOtpSessionByRegistrationToken(ctx, req.RegistrationToken) if err != nil { return nil, fmt.Errorf("failed to get OTP session: %w", err) } if otpSession == nil { return nil, fmt.Errorf("invalid or expired registration token") } if otpSession.IsExpired() { return nil, fmt.Errorf("registration token expired") } if !p.otpProcessor.ValidateOtpCode(req.OtpCode) { return &models.RegisterVerifyOtpResponse{ Status: "FAILED", Message: "Invalid OTP format.", }, nil } if otpSession.Code != req.OtpCode { otpSession.IncrementAttempts() if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil { fmt.Printf("Warning: failed to update OTP session attempts: %v\n", err) } return &models.RegisterVerifyOtpResponse{ Status: "FAILED", Message: "Invalid OTP code.", }, nil } if otpSession.IsUsed || otpSession.IsMaxAttemptsReached() { return &models.RegisterVerifyOtpResponse{ Status: "FAILED", Message: "OTP code already used or max attempts reached.", }, nil } // Mark OTP as used otpSession.MarkAsUsed() // Update registration step in metadata if otpSession.Metadata == nil { otpSession.Metadata = make(map[string]interface{}) } otpSession.Metadata["step"] = "otp_verified" if err := p.otpRepo.UpdateOtpSession(ctx, otpSession); err != nil { return &models.RegisterVerifyOtpResponse{ Status: "FAILED", Message: "Failed to update OTP session.", }, nil } return &models.RegisterVerifyOtpResponse{ Status: "OTP_VERIFIED", Message: "OTP verified, continue to set password.", Data: &models.RegisterVerifyOtpResponseData{ RegistrationToken: req.RegistrationToken, }, }, nil } func (p *customerAuthProcessor) SetPassword(ctx context.Context, req *contract.RegisterSetPasswordRequest) (*models.RegisterSetPasswordResponse, error) { if req.Password != req.ConfirmPassword { return nil, fmt.Errorf("passwords do not match") } // Get OTP session by registration token from metadata otpSession, err := p.otpRepo.GetOtpSessionByRegistrationToken(ctx, req.RegistrationToken) if err != nil { return nil, fmt.Errorf("failed to get OTP session: %w", err) } if otpSession == nil { return nil, fmt.Errorf("invalid or expired registration token") } if otpSession.IsExpired() { return nil, fmt.Errorf("registration token expired") } step, ok := otpSession.Metadata["step"].(string) if !ok || step != "otp_verified" { return nil, fmt.Errorf("OTP verification required before setting password") } // Hash password passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("failed to hash password: %w", err) } passwordHashStr := string(passwordHash) // Extract registration data from OTP session metadata name, ok := otpSession.Metadata["name"].(string) if !ok { return nil, fmt.Errorf("invalid registration data: name not found") } birthDateStr, ok := otpSession.Metadata["birth_date"].(string) if !ok { return nil, fmt.Errorf("invalid registration data: birth_date not found") } // Parse birth date birthDate, err := time.Parse("2006-01-02", birthDateStr) if err != nil { return nil, fmt.Errorf("invalid birth date format: %w", err) } defaultOrgID := uuid.MustParse("87bec7c1-e274-4f66-bac5-84e632208470") // This should be configurable customer := &entities.Customer{ OrganizationID: defaultOrgID, Name: name, PhoneNumber: &otpSession.PhoneNumber, BirthDate: &birthDate, PasswordHash: &passwordHashStr, IsActive: true, } if err := p.customerAuthRepo.CreateCustomer(ctx, customer); err != nil { return nil, fmt.Errorf("failed to create customer: %w", err) } accessToken, refreshToken, _, err := util.GenerateCustomerTokens(customer, p.jwtSecret, p.tokenTTLMinutes) if err != nil { return nil, fmt.Errorf("failed to generate tokens: %w", err) } return &models.RegisterSetPasswordResponse{ Status: "REGISTERED", Message: "Registration completed successfully.", Data: &models.RegisterSetPasswordResponseData{ AccessToken: accessToken, RefreshToken: refreshToken, User: &models.CustomerUserData{ ID: customer.ID, Name: customer.Name, PhoneNumber: *customer.PhoneNumber, BirthDate: birthDate.Format("2006-01-02"), }, }, }, nil } func (p *customerAuthProcessor) Login(ctx context.Context, req *contract.CustomerLoginRequest) (*models.CustomerLoginResponse, error) { // Get customer by phone number customer, err := p.customerAuthRepo.GetCustomerByPhoneNumber(ctx, req.PhoneNumber) if err != nil { return nil, fmt.Errorf("failed to get customer: %w", err) } if customer == nil { return nil, fmt.Errorf("customer not found") } if customer.PasswordHash == nil { return nil, fmt.Errorf("customer not properly registered") } // Verify password if err := bcrypt.CompareHashAndPassword([]byte(*customer.PasswordHash), []byte(req.Password)); err != nil { return nil, fmt.Errorf("invalid password") } // Generate JWT tokens using customer JWT util accessToken, refreshToken, _, err := util.GenerateCustomerTokens(customer, p.jwtSecret, p.tokenTTLMinutes) if err != nil { return nil, fmt.Errorf("failed to generate tokens: %w", err) } return &models.CustomerLoginResponse{ Status: "SUCCESS", Message: "Login successful.", Data: &models.CustomerLoginResponseData{ AccessToken: accessToken, RefreshToken: refreshToken, User: &models.CustomerUserData{ ID: customer.ID, Name: customer.Name, PhoneNumber: *customer.PhoneNumber, BirthDate: customer.BirthDate.Format("2006-01-02"), }, }, }, nil } func (p *customerAuthProcessor) ResendOtp(ctx context.Context, req *contract.ResendOtpRequest) (*models.ResendOtpResponse, error) { // Check if resend is allowed canResend, secondsUntilNext, err := p.otpProcessor.CanResendOtp(ctx, req.PhoneNumber, req.Purpose) if err != nil { return nil, fmt.Errorf("failed to check resend eligibility: %w", err) } if !canResend { return &models.ResendOtpResponse{ Status: "RESEND_NOT_ALLOWED", Message: fmt.Sprintf("Please wait %d seconds before requesting a new OTP", secondsUntilNext), Data: &models.ResendOtpResponseData{ NextResendIn: secondsUntilNext, }, }, nil } // For registration purpose, check if phone number is already registered if req.Purpose == "registration" { exists, err := p.customerAuthRepo.CheckPhoneNumberExists(ctx, req.PhoneNumber) if err != nil { return nil, fmt.Errorf("failed to check phone number: %w", err) } if exists { return &models.ResendOtpResponse{ Status: "PHONE_ALREADY_REGISTERED", Message: "Phone number is already registered. Please use login instead.", }, nil } } // For login purpose, check if phone number is registered if req.Purpose == "login" { exists, err := p.customerAuthRepo.CheckPhoneNumberExists(ctx, req.PhoneNumber) if err != nil { return nil, fmt.Errorf("failed to check phone number: %w", err) } if !exists { return &models.ResendOtpResponse{ Status: "PHONE_NOT_REGISTERED", Message: "Phone number is not registered. Please register first.", }, nil } } // Resend OTP otpSession, err := p.otpProcessor.ResendOtpSession(ctx, req.PhoneNumber, req.Purpose) if err != nil { return nil, fmt.Errorf("failed to resend OTP: %w", err) } // Calculate next resend time (60 seconds from now) nextResendIn := 60 return &models.ResendOtpResponse{ Status: "SUCCESS", Message: "OTP resent successfully.", Data: &models.ResendOtpResponseData{ OtpToken: otpSession.Token, ExpiresIn: 300, // 5 minutes NextResendIn: nextResendIn, }, }, nil } // Helper functions - OTP generation is now handled by OtpProcessor