Update template

This commit is contained in:
aditya.siregar 2025-05-06 15:21:12 +07:00
parent 5111fedfa8
commit 4be9c7d73c
7 changed files with 139 additions and 68 deletions

View File

@ -55,6 +55,13 @@ type Customer struct {
OTP string OTP string
} }
type CustomerPoints struct {
ID uint64
CustomerID uint64
TotalPoints int
AvailablePoints int
}
type AuthenticateUser struct { type AuthenticateUser struct {
ID int64 ID int64
Token string Token string

View File

@ -18,6 +18,10 @@ type CustomerRepo interface {
FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error)
FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error)
AddPoints(ctx mycontext.Context, id int64, points int, reference string) 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) FindSequence(ctx mycontext.Context, partnerID int64) (int64, error)
GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error) GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error)
VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error)
@ -172,6 +176,31 @@ func (r *customerRepository) AddPoints(ctx mycontext.Context, customerID int64,
return nil return nil
} }
func (r *customerRepository) GetPointsByCustomerID(
ctx mycontext.Context,
customerID int64,
) (*entity.CustomerPoints, error) {
var cp models.CustomerPointsDB
if err := r.db.Where("customer_id = ?", customerID).First(&cp).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("customer not found")
}
return nil, errors.Wrap(err, "failed to find customer point by customer_id")
}
return r.toDomainCustomerPoint(cp), nil
}
func (r *customerRepository) toDomainCustomerPoint(dbModel models.CustomerPointsDB) *entity.CustomerPoints {
return &entity.CustomerPoints{
ID: dbModel.ID,
CustomerID: dbModel.CustomerID,
TotalPoints: dbModel.TotalPoints,
AvailablePoints: dbModel.AvailablePoints,
}
}
func (r *customerRepository) toCustomerDBModel(customer *entity.Customer) models.CustomerDB { func (r *customerRepository) toCustomerDBModel(customer *entity.Customer) models.CustomerDB {
return models.CustomerDB{ return models.CustomerDB{
ID: customer.ID, ID: customer.ID,

View File

@ -21,6 +21,10 @@ type Repository interface {
FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error)
FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error)
AddPoints(ctx mycontext.Context, id int64, points int, reference string) 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) FindSequence(ctx mycontext.Context, partnerID int64) (int64, error)
GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error) GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error)
VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error)
@ -29,6 +33,7 @@ type Repository interface {
type Service interface { type Service interface {
ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error)
AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) 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) GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error)
CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error) CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error)
GetAllCustomers(ctx mycontext.Context, req *entity.MemberSearch) (*entity.MemberList, int, error) GetAllCustomers(ctx mycontext.Context, req *entity.MemberSearch) (*entity.MemberList, int, error)
@ -188,6 +193,15 @@ func (s *customerSvc) AddPoints(ctx mycontext.Context, customerID int64, points
return nil 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) { func (s *customerSvc) GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) {
customer, err := s.repo.FindByID(ctx, id) customer, err := s.repo.FindByID(ctx, id)
if err != nil { if err != nil {

View File

@ -52,7 +52,7 @@ func (s *orderSvc) processPostOrderActions(
} }
if order.CustomerID != nil && *order.CustomerID > 0 { if order.CustomerID != nil && *order.CustomerID > 0 {
err = s.addCustomerPoints(ctx, *order.CustomerID, int(order.Total/1000), fmt.Sprintf("TRX #%s", trx.ID)) err = s.addCustomerPoints(ctx, *order.CustomerID, int(order.Total/50000), fmt.Sprintf("TRX #%s", trx.ID))
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("error when adding points", zap.Error(err)) logger.ContextLogger(ctx).Error("error when adding points", zap.Error(err))
} }
@ -84,10 +84,21 @@ func (s *orderSvc) addCustomerPoints(ctx mycontext.Context, customerID int64, po
} }
func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.Order, transaction *entity.Transaction, paymentMethod string) error { func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.Order, transaction *entity.Transaction, paymentMethod string) error {
newPoint := int(order.Total / 50000)
if newPoint <= 0 {
return nil
}
if order.CustomerID == nil || *order.CustomerID == 0 { if order.CustomerID == nil || *order.CustomerID == 0 {
return nil return nil
} }
customerPoint, err := s.customer.GetCustomerPoints(ctx, *order.CustomerID)
if err != nil {
return nil
}
customer, err := s.customer.GetCustomer(ctx, *order.CustomerID) customer, err := s.customer.GetCustomer(ctx, *order.CustomerID)
if err != nil { if err != nil {
logger.ContextLogger(ctx).Error("error getting customer details", zap.Error(err)) logger.ContextLogger(ctx).Error("error getting customer details", zap.Error(err))
@ -133,14 +144,15 @@ func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.O
} }
transactionDate := transaction.CreatedAt.Format("02 January 2006 15:04") transactionDate := transaction.CreatedAt.Format("02 January 2006 15:04")
viewTransactionLink := fmt.Sprintf("https://enaklo.co.id/transaction/%s", transaction.ID) viewTransactionLink := "https://web.enaklo.co.id"
emailData := map[string]interface{}{ emailData := map[string]interface{}{
"UserName": customer.Name, "UserName": customer.Name,
"PointsName": "ELP", "PointsName": "Point",
"PointsBalance": int(order.Total / 1000), "PointsBalance": newPoint,
"NewPoints": int(order.Total / 1000), "NewPoints": newPoint,
"RedeemLink": "enaklo.co.id", "TotalPoints": customerPoint.TotalPoints,
"RedeemLink": "web.enaklo.co.id",
"BranchName": branchName, "BranchName": branchName,
"TransactionNumber": order.ID, "TransactionNumber": order.ID,
"TransactionDate": transactionDate, "TransactionDate": transactionDate,
@ -149,6 +161,8 @@ func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.O
"TotalPayment": fmt.Sprintf("Rp %s", formatCurrency(order.Total)), "TotalPayment": fmt.Sprintf("Rp %s", formatCurrency(order.Total)),
"ViewTransactionLink": viewTransactionLink, "ViewTransactionLink": viewTransactionLink,
"ExpiryDate": order.CreatedAt.Format("02 January 2006"), "ExpiryDate": order.CreatedAt.Format("02 January 2006"),
"UndianDate": "17 Mei 2025",
"WebURL": "https://web.enaklo.co.id/undian",
} }
return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{

View File

@ -44,6 +44,7 @@ type CustomerService interface {
ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error)
AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error
GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error)
GetCustomerPoints(ctx mycontext.Context, customerID int64) (*entity.CustomerPoints, error)
} }
type TransactionService interface { type TransactionService interface {

View File

@ -175,10 +175,9 @@
<div class="info-box"> <div class="info-box">
<strong>Cara Menyelesaikan Pendaftaran:</strong><br> <strong>Cara Menyelesaikan Pendaftaran:</strong><br>
1. Tunjukkan email ini kepada staf kasir Enaklo<br> 1. Masukkan kode OTP yang dikirim ke email Anda pada layar ini<br>
2. Staf akan memverifikasi identitas Anda<br> 2. Verifikasi identitas Anda dan selesaikan pendaftaran<br>
3. Staf akan memasukkan kode OTP ini ke sistem POS<br> 3. Pendaftaran member Anda akan segera aktif!
4. Pendaftaran member Anda akan segera aktif!
</div> </div>
<div class="text"> <div class="text">

View File

@ -10,85 +10,68 @@
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
background-color: #f5f5f5; background-color: #f5f5f5;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
} }
.container {
padding: 40px 20px;
background-color: #f5f5f5;
}
.content { .content {
background-color: #ffffff; background-color: #ffffff;
margin: 0px auto;
max-width: 600px; max-width: 600px;
padding: 0; margin: 0 auto;
border-radius: 12px; border-radius: 12px;
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
} }
.header { .header {
background-color: #d90000; background-color: #d90000;
padding: 25px 30px; padding: 25px 30px;
text-align: center; text-align: center;
} }
.logo { .logo {
width: 120px; width: 120px;
margin-bottom: 15px; margin-bottom: 15px;
} }
.greeting { .greeting {
color: white; color: #ffffff;
font-size: 22px; font-size: 22px;
font-weight: bold; font-weight: bold;
margin: 0; margin: 0;
} }
.body-content { .body-content {
padding: 30px; padding: 30px;
} }
.card {
.points-card {
background-color: #fff8f0;
border-radius: 10px; border-radius: 10px;
padding: 20px; padding: 20px;
margin-bottom: 25px; margin-bottom: 20px;
}
.new-points-card {
background-color: #fff8f0;
border-left: 5px solid #f46f02; border-left: 5px solid #f46f02;
} }
.total-points-card {
.points-heading { background-color: #e8f5e9;
font-size: 16px; border-left: 5px solid #4caf50;
color: #666;
margin: 0 0 5px 0;
} }
.card-heading {
.points-value {
font-weight: bold;
font-size: 28px;
color: #f46f02;
margin: 0 0 5px 0;
}
.expiry {
font-size: 14px; font-size: 14px;
color: #888; color: #666666;
margin: 0 0 5px;
}
.card-value {
font-size: 26px;
font-weight: bold;
margin: 0; margin: 0;
} }
.subtitle {
.total-points-card {
background-color: #f5f5f5;
border-radius: 10px;
padding: 20px;
margin-bottom: 25px;
}
.message {
font-size: 16px; font-size: 16px;
line-height: 1.5;
color: #333333; color: #333333;
line-height: 1.5;
margin-bottom: 25px; margin-bottom: 25px;
} }
.cta-button { .cta-button {
display: block; display: block;
width: 100%; width: 100%;
@ -100,45 +83,69 @@
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px;
}
.info {
font-size: 14px;
color: #555555;
line-height: 1.5;
margin-bottom: 25px;
} }
.footer { .footer {
font-size: 12px; font-size: 12px;
line-height: 1.5;
text-align: center;
color: #808080; color: #808080;
margin-top: 30px; text-align: center;
padding-top: 20px; padding: 20px;
border-top: 1px solid #eeeeee; border-top: 1px solid #eeeeee;
} }
a {
color: #f46f02;
text-decoration: none;
}
</style> </style>
</head> </head>
<body> <body>
<div style="padding: 40px 20px; background-color: #f5f5f5;"> <div class="container">
<div class="content"> <div class="content">
<!-- HEADER -->
<div class="header"> <div class="header">
<img src="https://res.cloudinary.com/dl0wpumax/image/upload/c_thumb,w_200,g_face/v1741363977/61747686_5_vtz0n4.png" alt="Enaklo Logo" class="logo"> <img src="https://res.cloudinary.com/dl0wpumax/image/upload/c_thumb,w_200,g_face/v1741363977/61747686_5_vtz0n4.png"
alt="Enaklo Logo" class="logo">
<h1 class="greeting">Hai {{ .UserName }}!</h1> <h1 class="greeting">Hai {{ .UserName }}!</h1>
</div> </div>
<!-- BODY -->
<div class="body-content"> <div class="body-content">
<div class="points-card">
<p class="points-heading">Kamu dapat poin</p> <!-- New Points Earned -->
<p class="points-value">{{ .NewPoints }} {{ .PointsName }}</p> <div class="card new-points-card">
<p class="expiry">Berlaku sampai {{ .ExpiryDate }}</p> <p class="card-heading">Poin baru yang kamu dapat</p>
<p class="card-value">{{ .NewPoints }} {{ .PointsName }}</p>
</div> </div>
<div class="message"> <div class="card total-points-card">
Kamu bisa pakai poinnya untuk memotong jumlah transaksimu atau ditukarkan dengan Deals. Yuk, pakai sekarang! <p class="card-heading">Total Poin Undian</p>
<p class="card-value">{{ .TotalPoints }} {{ .PointsName }}</p>
</div> </div>
<a href="{{ .RedeemLink }}" class="cta-button">Pakai Sekarang</a> <p class="subtitle">
Poin kamu akan digunakan untuk undian pada tanggal <strong>{{ .UndianDate }}</strong>.
</p>
<p class="info">
Nantikan undian pada tanggal <strong>{{ .UndianDate }}</strong>
dan kunjungi <a href="{{ .WebURL }}">{{ .WebURL }}</a> untuk informasi lebih lanjut.
</p>
<!-- FOOTER -->
<div class="footer"> <div class="footer">
Email ini dikirim secara otomatis. Mohon jangan membalas email ini.<br> Email ini dikirim otomatis. Mohon jangan membalas email ini.<br>
Butuh bantuan? Hubungi tim support kami di <a href="mailto:support@enaklo.com" style="color: #f46f02;">support@enaklo.com</a>. Butuh bantuan? Hubungi tim support kami di
<a href="mailto:support@enaklo.com">support@enaklo.com</a>.
</div> </div>
</div> </div>
</div> </div>
</div> </div>