2025-03-08 00:35:23 +07:00
|
|
|
package customer
|
|
|
|
|
|
|
|
|
|
import (
|
2025-04-05 11:28:06 +08:00
|
|
|
"context"
|
|
|
|
|
errors2 "enaklo-pos-be/internal/common/errors"
|
2025-03-08 00:35:23 +07:00
|
|
|
"enaklo-pos-be/internal/common/logger"
|
|
|
|
|
"enaklo-pos-be/internal/common/mycontext"
|
|
|
|
|
"enaklo-pos-be/internal/constants"
|
|
|
|
|
"enaklo-pos-be/internal/entity"
|
2025-03-15 15:51:18 +08:00
|
|
|
"enaklo-pos-be/internal/utils"
|
2025-03-08 00:35:23 +07:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
"go.uber.org/zap"
|
2025-04-05 11:28:06 +08:00
|
|
|
"log"
|
2025-03-08 00:35:23 +07:00
|
|
|
"strings"
|
2025-04-05 11:28:06 +08:00
|
|
|
"time"
|
2025-03-08 00:35:23 +07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
2025-04-05 11:28:06 +08:00
|
|
|
AddPoints(ctx mycontext.Context, id int64, points int, reference string) error
|
2025-03-15 15:51:18 +08:00
|
|
|
FindSequence(ctx mycontext.Context, partnerID int64) (int64, error)
|
|
|
|
|
GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error)
|
2025-04-05 11:28:06 +08:00
|
|
|
VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error)
|
2025-03-08 00:35:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Service interface {
|
|
|
|
|
ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error)
|
2025-04-05 11:28:06 +08:00
|
|
|
AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error
|
2025-03-08 00:35:23 +07:00
|
|
|
GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error)
|
2025-03-15 15:51:18 +08:00
|
|
|
CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error)
|
|
|
|
|
GetAllCustomers(ctx mycontext.Context, req *entity.MemberSearch) (*entity.MemberList, int, error)
|
2025-04-05 11:28:06 +08:00
|
|
|
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
|
2025-03-08 00:35:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type customerSvc struct {
|
2025-04-05 11:28:06 +08:00
|
|
|
repo Repository
|
|
|
|
|
notification EmailService
|
2025-03-08 00:35:23 +07:00
|
|
|
}
|
|
|
|
|
|
2025-04-05 11:28:06 +08:00
|
|
|
func New(repo Repository, notification EmailService) Service {
|
2025-03-08 00:35:23 +07:00
|
|
|
return &customerSvc{
|
2025-04-05 11:28:06 +08:00
|
|
|
repo: repo,
|
|
|
|
|
notification: notification,
|
2025-03-08 00:35:23 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-15 15:51:18 +08:00
|
|
|
lastSeq, err := s.repo.FindSequence(ctx, *ctx.GetPartnerID())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, errors.New("failed to resolve customer sequence")
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-08 00:35:23 +07:00
|
|
|
newCustomer := &entity.Customer{
|
2025-03-15 15:51:18 +08:00
|
|
|
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,
|
2025-03-08 00:35:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-05 11:28:06 +08:00
|
|
|
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 {
|
2025-03-08 00:35:23 +07:00
|
|
|
if points <= 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-05 11:28:06 +08:00
|
|
|
err := s.repo.AddPoints(ctx, customerID, points, reference)
|
2025-03-08 00:35:23 +07:00
|
|
|
if err != nil {
|
|
|
|
|
return errors.Wrap(err, "failed to add points to customer")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 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
|
|
|
|
|
}
|
2025-03-15 15:51:18 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2025-04-05 11:28:06 +08:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|