Update Member

This commit is contained in:
aditya.siregar
2025-03-15 15:51:18 +08:00
parent 18003313dd
commit c41826bb1b
29 changed files with 1840 additions and 65 deletions
+51
View File
@@ -0,0 +1,51 @@
package member
import (
"context"
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/constants"
"enaklo-pos-be/internal/entity"
"time"
)
type RegistrationService interface {
InitiateRegistration(ctx mycontext.Context, request *entity.MemberRegistrationRequest) (*entity.MemberRegistrationResponse, error)
VerifyOTP(ctx mycontext.Context, token string, otp string) (*entity.MemberVerificationResponse, error)
GetRegistrationStatus(ctx mycontext.Context, token string) (*entity.MemberRegistrationStatus, error)
ResendOTP(ctx mycontext.Context, token string) (*entity.ResendOTPResponse, error)
}
type memberSvc struct {
repo Repository
notification NotificationService
customerSvc CustomerService
}
type Repository interface {
CreateRegistration(ctx mycontext.Context, registration *entity.MemberRegistration) (*entity.MemberRegistration, error)
GetRegistrationByToken(ctx mycontext.Context, token string) (*entity.MemberRegistration, error)
UpdateRegistrationStatus(ctx mycontext.Context, token string, status constants.RegistrationStatus) error
UpdateRegistrationOTP(ctx mycontext.Context, token string, otp string, expiresAt time.Time) error
}
type NotificationService interface {
SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error
}
type CustomerService interface {
ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error)
GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error)
CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error)
}
func NewMemberRegistrationService(
repo Repository,
notification NotificationService,
customerSvc CustomerService,
) RegistrationService {
return &memberSvc{
repo: repo,
notification: notification,
customerSvc: customerSvc,
}
}
@@ -0,0 +1,262 @@
package member
import (
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/constants"
"enaklo-pos-be/internal/entity"
"errors"
"go.uber.org/zap"
"golang.org/x/exp/rand"
"time"
)
func (s *memberSvc) InitiateRegistration(
ctx mycontext.Context,
request *entity.MemberRegistrationRequest,
) (*entity.MemberRegistrationResponse, error) {
customerResolution := &entity.CustomerResolutionRequest{
Email: request.Email,
PhoneNumber: request.Phone,
}
checkResult, err := s.customerSvc.CustomerCheck(ctx, customerResolution)
if checkResult.Exists {
return nil, errors.New(checkResult.Message)
}
otp := generateOTP(6)
token := constants.GenerateUUID()
registration := &entity.MemberRegistration{
ID: constants.GenerateUUID(),
Token: token,
Name: request.Name,
Email: request.Email,
Phone: request.Phone,
BirthDate: request.BirthDate,
OTP: otp,
Status: constants.RegistrationPending,
ExpiresAt: constants.TimeNow().Add(10 * time.Minute), // OTP expires in 10 minutes
CreatedAt: constants.TimeNow(),
UpdatedAt: constants.TimeNow(),
BranchID: request.BranchID,
CashierID: request.CashierID,
}
savedRegistration, err := s.repo.CreateRegistration(ctx, registration)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create member registration", zap.Error(err))
return nil, err
}
err = s.sendRegistrationOTP(ctx, savedRegistration)
if err != nil {
logger.ContextLogger(ctx).Warn("failed to send OTP", zap.Error(err))
}
return &entity.MemberRegistrationResponse{
Token: token,
Status: savedRegistration.Status.String(),
ExpiresAt: savedRegistration.ExpiresAt,
Message: "Registration initiated. Please verify with OTP sent to your email.",
}, nil
}
func (s *memberSvc) VerifyOTP(
ctx mycontext.Context,
token string,
otp string,
) (*entity.MemberVerificationResponse, error) {
logger.ContextLogger(ctx).Info("verifying OTP for member registration", zap.String("token", token))
registration, err := s.repo.GetRegistrationByToken(ctx, token)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get registration", zap.Error(err))
return nil, errors.New("invalid registration token")
}
if registration.Status == constants.RegistrationSuccess {
return nil, errors.New("registration already completed")
}
if registration.ExpiresAt.Before(constants.TimeNow()) {
return nil, errors.New("registration expired")
}
if registration.OTP != otp {
return nil, errors.New("invalid OTP")
}
customerResolution := &entity.CustomerResolutionRequest{
Name: registration.Name,
Email: registration.Email,
PhoneNumber: registration.Phone,
BirthDate: registration.BirthDate,
}
customerID, err := s.customerSvc.ResolveCustomer(ctx, customerResolution)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create customer", zap.Error(err))
return nil, errors.New("failed to create member record")
}
err = s.repo.UpdateRegistrationStatus(ctx, token, constants.RegistrationSuccess)
if err != nil {
logger.ContextLogger(ctx).Warn("failed to update registration status", zap.Error(err))
}
customer, err := s.customerSvc.GetCustomer(ctx, customerID)
if err != nil {
logger.ContextLogger(ctx).Warn("failed to get created customer", zap.Error(err))
return &entity.MemberVerificationResponse{
CustomerID: customerID,
Name: registration.Name,
Email: registration.Email,
Phone: registration.Phone,
Status: "Registration completed successfully",
}, nil
}
err = s.sendWelcomeEmail(ctx, customer)
if err != nil {
logger.ContextLogger(ctx).Warn("failed to send welcome email", zap.Error(err))
}
return &entity.MemberVerificationResponse{
CustomerID: customer.ID,
Name: customer.Name,
Email: customer.Email,
Phone: customer.Phone,
Points: customer.Points,
Status: "Registration completed successfully",
}, nil
}
func (s *memberSvc) GetRegistrationStatus(
ctx mycontext.Context,
token string,
) (*entity.MemberRegistrationStatus, error) {
logger.ContextLogger(ctx).Info("checking registration status", zap.String("token", token))
registration, err := s.repo.GetRegistrationByToken(ctx, token)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get registration", zap.Error(err))
return nil, errors.New("invalid registration token")
}
return &entity.MemberRegistrationStatus{
Token: registration.Token,
Status: registration.Status.String(),
ExpiresAt: registration.ExpiresAt,
IsExpired: registration.ExpiresAt.Before(constants.TimeNow()),
CreatedAt: registration.CreatedAt,
}, nil
}
func (s *memberSvc) ResendOTP(
ctx mycontext.Context,
token string,
) (*entity.ResendOTPResponse, error) {
logger.ContextLogger(ctx).Info("resending OTP", zap.String("token", token))
// Get registration by token
registration, err := s.repo.GetRegistrationByToken(ctx, token)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get registration", zap.Error(err))
return nil, errors.New("invalid registration token")
}
if registration.Status == constants.RegistrationSuccess {
return nil, errors.New("registration already completed")
}
newOTP := generateOTP(6)
newExpiresAt := constants.TimeNow().Add(10 * time.Minute)
err = s.repo.UpdateRegistrationOTP(ctx, token, newOTP, newExpiresAt)
if err != nil {
logger.ContextLogger(ctx).Error("failed to update OTP", zap.Error(err))
return nil, errors.New("failed to generate new OTP")
}
registration.OTP = newOTP
registration.ExpiresAt = newExpiresAt
err = s.sendRegistrationOTP(ctx, registration)
if err != nil {
logger.ContextLogger(ctx).Warn("failed to send OTP", zap.Error(err))
}
return &entity.ResendOTPResponse{
Token: token,
ExpiresAt: newExpiresAt,
Message: "OTP has been resent to your email and phone",
}, nil
}
func (s *memberSvc) 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
}
//if registration.Phone != "" {
// smsMessage := fmt.Sprintf("Your Enaklo registration code is: %s. Please provide this code to our staff to complete your registration.", registration.OTP)
// _ = s.notification.SendSMS(ctx, registration.Phone, smsMessage)
//}
return nil
}
func (s *memberSvc) sendWelcomeEmail(
ctx mycontext.Context,
customer *entity.Customer,
) error {
welcomeData := map[string]interface{}{
"UserName": customer.Name,
"MemberID": customer.CustomerID,
"PointsName": "PoinLo",
"PointsBalance": customer.Points,
"RedeemLink": "https://enaklo.co.id/redeem",
"CurrentDate": time.Now().Format("01-20006"),
}
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,
})
}
func generateOTP(length int) string {
rand.Seed(uint64(time.Now().Nanosecond()))
digits := "0123456789"
otp := ""
for i := 0; i < length; i++ {
otp += string(digits[rand.Intn(len(digits))])
}
return otp
}