package customer import ( "context" errors2 "enaklo-pos-be/internal/common/errors" "enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/constants" "enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/utils" "github.com/pkg/errors" "go.uber.org/zap" "log" "strings" "time" ) type Repository interface { Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) AddPoints(ctx mycontext.Context, id int64, points int, reference string) error GetPointsByCustomerID( ctx mycontext.Context, customerID int64, ) (*entity.CustomerPoints, error) FindSequence(ctx mycontext.Context, partnerID int64) (int64, error) GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error) VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) } type Service interface { ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error GetCustomerPoints(ctx mycontext.Context, customerID int64) (*entity.CustomerPoints, error) GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error) GetAllCustomers(ctx mycontext.Context, req *entity.MemberSearch) (*entity.MemberList, int, error) RegistrationMember(ctx mycontext.Context, req *entity.Customer) (*entity.Customer, error) VerifyOTP(ctx mycontext.Context, verificationID, otpCode string) error } type EmailService interface { SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error } type customerSvc struct { repo Repository notification EmailService } func New(repo Repository, notification EmailService) Service { return &customerSvc{ repo: repo, notification: notification, } } func (s *customerSvc) ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) { if req.Email == "" && req.PhoneNumber == "" { return 0, nil } if req.ID != nil && *req.ID > 0 { customer, err := s.repo.FindByID(ctx, *req.ID) if err != nil { if !strings.Contains(err.Error(), "not found") { return 0, errors.Wrap(err, "failed to find customer by ID") } } else { return customer.ID, nil } } if req.PhoneNumber != "" { customer, err := s.repo.FindByPhone(ctx, req.PhoneNumber) if err != nil { if !strings.Contains(err.Error(), "not found") { return 0, errors.Wrap(err, "failed to find customer by phone") } } else { return customer.ID, nil } } if req.Email != "" { customer, err := s.repo.FindByEmail(ctx, req.Email) if err != nil { if !strings.Contains(err.Error(), "not found") { return 0, errors.Wrap(err, "failed to find customer by email") } } else { return customer.ID, nil } } if req.Name == "" { return 0, errors.New("customer name is required to create a new customer") } lastSeq, err := s.repo.FindSequence(ctx, 1) if err != nil { return 0, errors.New("failed to resolve customer sequence") } newCustomer := &entity.Customer{ Name: req.Name, Email: req.Email, Phone: req.PhoneNumber, Points: 0, CreatedAt: constants.TimeNow(), UpdatedAt: constants.TimeNow(), CustomerID: utils.GenerateMemberID(ctx, 1, lastSeq), BirthDate: req.BirthDate, Password: req.Password, } customer, err := s.repo.Create(ctx, newCustomer) if err != nil { logger.ContextLogger(ctx).Error("failed to create customer", zap.Error(err)) return 0, errors.Wrap(err, "failed to create customer") } return customer.ID, nil } func (s *customerSvc) RegistrationMember(ctx mycontext.Context, req *entity.Customer) (*entity.Customer, error) { if req.Email == "" && req.PhoneNumber == "" { return nil, errors2.ErrorPhoneNumberEmailIsRequired } if req.PhoneNumber != "" { customer, err := s.repo.FindByPhone(ctx, req.PhoneNumber) if err != nil && !strings.Contains(err.Error(), "not found") { return nil, errors2.ErrorInternalServer } if customer != nil { return nil, errors2.ErrorPhoneNumberIsAlreadyRegistered } } if req.Email != "" { customer, err := s.repo.FindByEmail(ctx, req.Email) if err != nil && !strings.Contains(err.Error(), "not found") { return nil, errors2.ErrorInternalServer } if customer != nil { return nil, errors2.ErrorEmailIsAlreadyRegistered } } newCustomer := &entity.Customer{ Name: req.Name, Email: req.Email, Phone: req.PhoneNumber, CreatedAt: constants.TimeNow(), UpdatedAt: constants.TimeNow(), BirthDate: req.BirthDate, Password: req.HashedPassword(), } customer, err := s.repo.Create(ctx, newCustomer) if err != nil { logger.ContextLogger(ctx).Error("failed to create customer", zap.Error(err)) return nil, errors2.ErrorInternalServer } errs := s.sendRegistrationOTP(ctx, &entity.MemberRegistration{ Name: customer.Name, Email: customer.Email, OTP: customer.OTP, }) if err != nil { logger.ContextLogger(ctx).Error("failed to send OTP", zap.Error(errs)) } return customer, nil } func (s *customerSvc) AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error { if points <= 0 { return nil } err := s.repo.AddPoints(ctx, customerID, points, reference) if err != nil { return errors.Wrap(err, "failed to add points to customer") } return nil } func (s *customerSvc) GetCustomerPoints(ctx mycontext.Context, customerID int64) (*entity.CustomerPoints, error) { cp, err := s.repo.GetPointsByCustomerID(ctx, customerID) if err != nil { return nil, errors.Wrap(err, "failed to add points to customer") } return cp, nil } func (s *customerSvc) GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) { customer, err := s.repo.FindByID(ctx, id) if err != nil { return nil, errors.Wrap(err, "failed to get customer") } return customer, nil } func (s *customerSvc) CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error) { logger.ContextLogger(ctx).Info("checking customer existence before registration", zap.String("email", req.Email), zap.String("phone", req.PhoneNumber)) if req.Email == "" && req.PhoneNumber == "" { return nil, errors.New("email dan phone number is mandatory") } response := &entity.CustomerCheckResponse{ Exists: false, Customer: nil, } if req.PhoneNumber != "" { customer, err := s.repo.FindByPhone(ctx, req.PhoneNumber) if err != nil { if !strings.Contains(err.Error(), "not found") { logger.ContextLogger(ctx).Error("error checking customer by phone", zap.Error(err)) return nil, errors.Wrap(err, "failed to find customer by phone") } } else { logger.ContextLogger(ctx).Info("found existing customer by phone", zap.Int64("customerId", customer.ID)) return &entity.CustomerCheckResponse{ Exists: true, Customer: customer, Message: "Nomor telepon sudah terdaftar. Silakan gunakan nomor lain atau login.", }, nil } } if req.Email != "" { customer, err := s.repo.FindByEmail(ctx, req.Email) if err != nil { if !strings.Contains(err.Error(), "not found") { logger.ContextLogger(ctx).Error("error checking customer by email", zap.Error(err)) return nil, errors.Wrap(err, "failed to find customer by email") } } else { logger.ContextLogger(ctx).Info("found existing customer by email", zap.Int64("customerId", customer.ID)) return &entity.CustomerCheckResponse{ Exists: true, Customer: customer, Message: "Email sudah terdaftar. Silakan gunakan email lain atau login.", }, nil } } return response, nil } func (s *customerSvc) GetAllCustomers(ctx mycontext.Context, req *entity.MemberSearch) (*entity.MemberList, int, error) { if req.Limit <= 0 { req.Limit = 10 } if req.Offset < 0 { req.Offset = 0 } customers, totalCount, err := s.repo.GetAllCustomers(ctx, *req) if err != nil { logger.ContextLogger(ctx).Error("failed to retrieve customers", zap.Error(err), zap.String("search", req.Search), ) return nil, 0, errors.Wrap(err, "failed to get customers") } return &customers, totalCount, nil } func (s *customerSvc) sendRegistrationOTP( ctx mycontext.Context, registration *entity.MemberRegistration, ) error { emailData := map[string]interface{}{ "UserName": registration.Name, "OTPCode": registration.OTP, } err := s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ Sender: "noreply@enaklo.co.id", Recipient: registration.Email, Subject: "Enaklo - Registration Verification Code", TemplateName: "member_registration_otp", TemplatePath: "templates/member_registration_otp.html", Data: emailData, }) if err != nil { return err } return nil } func (s *customerSvc) VerifyOTP(ctx mycontext.Context, verificationID, otpCode string) error { customerID, err := s.repo.VerifyOTP(ctx, verificationID, otpCode) if err != nil { return errors.Wrap(err, "verification failed") } customer, _ := s.repo.FindByID(ctx, customerID) go func(customer *entity.Customer) { newCtx := context.Background() defer func() { if r := recover(); r != nil { log.Printf("Recovered from panic in sendWelcomeEmail: %v", r) } }() s.sendWelcomeEmail(newCtx, customer) }(customer) return nil } func (s *customerSvc) sendWelcomeEmail( ctx context.Context, customer *entity.Customer, ) error { welcomeData := map[string]interface{}{ "UserName": customer.Name, "MemberID": customer.CustomerID, "PointsName": "EnakPoint", "PointsBalance": customer.Points, "RedeemLink": "https://enaklo.co.id/redeem", "CurrentDate": time.Now().Format("01-2006"), } return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ Sender: "noreply@enaklo.co.id", Recipient: customer.Email, Subject: "Welcome to Enaklo Membership Program", TemplateName: "welcome_member", TemplatePath: "templates/welcome_member.html", Data: welcomeData, }) }