Update Member
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"enaklo-pos-be/internal/services/balance"
|
||||
"enaklo-pos-be/internal/services/discovery"
|
||||
service "enaklo-pos-be/internal/services/license"
|
||||
"enaklo-pos-be/internal/services/member"
|
||||
"enaklo-pos-be/internal/services/order"
|
||||
"enaklo-pos-be/internal/services/oss"
|
||||
"enaklo-pos-be/internal/services/partner"
|
||||
@@ -42,9 +43,10 @@ type ServiceManagerImpl struct {
|
||||
Balance Balance
|
||||
DiscoverService DiscoverService
|
||||
|
||||
OrderV2Svc orderSvc.Service
|
||||
CustomerV2Svc customerSvc.Service
|
||||
ProductV2Svc productSvc.Service
|
||||
OrderV2Svc orderSvc.Service
|
||||
CustomerV2Svc customerSvc.Service
|
||||
ProductV2Svc productSvc.Service
|
||||
MemberRegistrationSvc member.RegistrationService
|
||||
}
|
||||
|
||||
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
|
||||
@@ -62,12 +64,14 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
|
||||
OSSSvc: oss.NewOSSService(repo.OSS),
|
||||
PartnerSvc: partner.NewPartnerService(
|
||||
repo.Partner, users.NewUserService(repo.User), repo.Trx, repo.Wallet, repo.User),
|
||||
SiteSvc: site.NewSiteService(repo.Site, repo.User),
|
||||
LicenseSvc: service.NewLicenseService(repo.License),
|
||||
Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx),
|
||||
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
|
||||
DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery, repo.Product),
|
||||
OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService),
|
||||
SiteSvc: site.NewSiteService(repo.Site, repo.User),
|
||||
LicenseSvc: service.NewLicenseService(repo.License),
|
||||
Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx),
|
||||
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
|
||||
DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery, repo.Product),
|
||||
OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService),
|
||||
MemberRegistrationSvc: member.NewMemberRegistrationService(repo.MemberRepository, repo.EmailService, custSvcV2),
|
||||
CustomerV2Svc: custSvcV2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"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"
|
||||
"strings"
|
||||
@@ -16,12 +17,16 @@ type Repository interface {
|
||||
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) error
|
||||
FindSequence(ctx mycontext.Context, partnerID int64) (int64, error)
|
||||
GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error)
|
||||
AddPoints(ctx mycontext.Context, customerID int64, points int) 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)
|
||||
}
|
||||
|
||||
type customerSvc struct {
|
||||
@@ -76,13 +81,20 @@ func (s *customerSvc) ResolveCustomer(ctx mycontext.Context, req *entity.Custome
|
||||
return 0, errors.New("customer name is required to create a new customer")
|
||||
}
|
||||
|
||||
lastSeq, err := s.repo.FindSequence(ctx, *ctx.GetPartnerID())
|
||||
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(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Phone: req.PhoneNumber,
|
||||
Points: 0,
|
||||
CreatedAt: constants.TimeNow(),
|
||||
UpdatedAt: constants.TimeNow(),
|
||||
CustomerID: utils.GenerateMemberID(ctx, *ctx.GetPartnerID(), lastSeq),
|
||||
BirthDate: req.BirthDate,
|
||||
}
|
||||
|
||||
customer, err := s.repo.Create(ctx, newCustomer)
|
||||
@@ -115,3 +127,78 @@ func (s *customerSvc) GetCustomer(ctx mycontext.Context, id int64) (*entity.Cust
|
||||
|
||||
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: "Customer already exists with this phone number",
|
||||
}, 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: "Customer already exists with this email",
|
||||
}, 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package member
|
||||
@@ -136,6 +136,9 @@ func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.O
|
||||
|
||||
emailData := map[string]interface{}{
|
||||
"UserName": customer.Name,
|
||||
"PointsName": "PoinLo",
|
||||
"PointsBalance": "20",
|
||||
"RedeemLink": "enaklo.co.id",
|
||||
"BranchName": branchName,
|
||||
"TransactionNumber": order.ID,
|
||||
"TransactionDate": transactionDate,
|
||||
@@ -148,9 +151,9 @@ func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.O
|
||||
return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{
|
||||
Sender: "noreply@enaklo.co.id",
|
||||
Recipient: customer.Email,
|
||||
Subject: "Enaklo - Resi Pembelian",
|
||||
TemplateName: "transaction_receipt",
|
||||
TemplatePath: "templates/transaction_receipt.html",
|
||||
Subject: "Enaklo - Membership Statement",
|
||||
TemplateName: "monthly_points",
|
||||
TemplatePath: "templates/monthly_points.html",
|
||||
Data: emailData,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user