Update template email
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
|
||||
req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) {
|
||||
productIDs, filteredItems, err := s.validateOrderItems(ctx, req.OrderItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.OrderItems = filteredItems
|
||||
|
||||
productDetails, err := s.product.GetProductDetails(ctx, productIDs, req.PartnerID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderCalculation, err := s.calculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
customerID, err := s.customer.ResolveCustomer(ctx, &entity.CustomerResolutionRequest{
|
||||
ID: req.CustomerID,
|
||||
Name: req.CustomerName,
|
||||
Email: req.CustomerEmail,
|
||||
PhoneNumber: req.CustomerPhoneNumber,
|
||||
})
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to resolve customer", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inquiry := entity.NewOrderInquiry(
|
||||
req.PartnerID,
|
||||
customerID,
|
||||
orderCalculation.Subtotal,
|
||||
orderCalculation.Fee,
|
||||
orderCalculation.Total,
|
||||
req.PaymentMethod,
|
||||
req.Source,
|
||||
req.CreatedBy,
|
||||
req.CustomerName,
|
||||
req.CustomerPhoneNumber,
|
||||
req.CustomerEmail,
|
||||
)
|
||||
|
||||
for _, item := range req.OrderItems {
|
||||
product := productDetails.Products[item.ProductID]
|
||||
inquiry.AddOrderItem(item, product)
|
||||
}
|
||||
|
||||
savedInquiry, err := s.repo.CreateInquiry(ctx, inquiry)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to create order inquiry", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := s.crypt.GenerateJWTOrderInquiry(savedInquiry)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to generate token", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &entity.OrderInquiryResponse{
|
||||
OrderInquiry: savedInquiry,
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *orderSvc) validateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) {
|
||||
var productIDs []int64
|
||||
var filteredItems []entity.OrderItemRequest
|
||||
|
||||
for _, item := range items {
|
||||
if item.Quantity <= 0 {
|
||||
continue
|
||||
}
|
||||
productIDs = append(productIDs, item.ProductID)
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
|
||||
if len(productIDs) == 0 {
|
||||
return nil, nil, errors.ErrorBadRequest
|
||||
}
|
||||
|
||||
return productIDs, filteredItems, nil
|
||||
}
|
||||
|
||||
func (s *orderSvc) calculateOrderTotals(
|
||||
ctx mycontext.Context,
|
||||
items []entity.OrderItemRequest,
|
||||
productDetails *entity.ProductDetails,
|
||||
source string,
|
||||
) (*entity.OrderCalculation, error) {
|
||||
subtotal := 0.0
|
||||
|
||||
for _, item := range items {
|
||||
product, ok := productDetails.Products[item.ProductID]
|
||||
if !ok {
|
||||
return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "product not found")
|
||||
}
|
||||
subtotal += product.Price * float64(item.Quantity)
|
||||
}
|
||||
|
||||
fee := s.cfg.GetOrderFee(source)
|
||||
|
||||
return &entity.OrderCalculation{
|
||||
Subtotal: subtotal,
|
||||
Fee: fee,
|
||||
Total: subtotal + fee,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *orderSvc) validateInquiry(ctx mycontext.Context, token string) (*entity.OrderInquiry, error) {
|
||||
partnerID, inquiryID, err := s.crypt.ValidateJWTOrderInquiry(token)
|
||||
if err != nil {
|
||||
return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "inquiry is not valid or expired")
|
||||
}
|
||||
|
||||
if partnerID != *ctx.GetPartnerID() {
|
||||
return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "invalid request")
|
||||
}
|
||||
|
||||
inquiry, err := s.repo.FindInquiryByID(ctx, inquiryID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when finding inquiry", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if inquiry.Status != constants.StatusPending {
|
||||
return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "inquiry is no longer pending")
|
||||
}
|
||||
|
||||
return inquiry, nil
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"enaklo-pos-be/internal/common/logger"
|
||||
"enaklo-pos-be/internal/common/mycontext"
|
||||
"enaklo-pos-be/internal/constants"
|
||||
"enaklo-pos-be/internal/entity"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context,
|
||||
token string, paymentMethod string) (*entity.OrderResponse, error) {
|
||||
inquiry, err := s.validateInquiry(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
order := inquiry.ToOrder(paymentMethod)
|
||||
|
||||
savedOrder, err := s.repo.Create(ctx, order)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("failed to create order", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.processPostOrderActions(ctx, savedOrder, inquiry.ID, paymentMethod)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Warn("some post-order actions failed", zap.Error(err))
|
||||
}
|
||||
|
||||
return &entity.OrderResponse{
|
||||
Order: savedOrder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *orderSvc) processPostOrderActions(
|
||||
ctx mycontext.Context,
|
||||
order *entity.Order,
|
||||
inquiryID string,
|
||||
paymentMethod string,
|
||||
) error {
|
||||
err := s.repo.UpdateInquiryStatus(ctx, inquiryID, constants.StatusExecuted)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when updating inquiry status", zap.Error(err))
|
||||
}
|
||||
|
||||
trx, err := s.createTransaction(ctx, order, paymentMethod)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when creating transaction", zap.Error(err))
|
||||
}
|
||||
|
||||
if order.CustomerID != nil && *order.CustomerID > 0 {
|
||||
err = s.addCustomerPoints(ctx, *order.CustomerID, int(order.Total))
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when adding points", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
s.sendTransactionReceipt(ctx, order, trx, "CASH")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *orderSvc) createTransaction(ctx mycontext.Context, order *entity.Order, paymentMethod string) (*entity.Transaction, error) {
|
||||
transaction := &entity.Transaction{
|
||||
ID: constants.GenerateUUID(),
|
||||
OrderID: order.ID,
|
||||
Amount: order.Total,
|
||||
PaymentMethod: paymentMethod,
|
||||
Status: "SUCCESS",
|
||||
CreatedAt: constants.TimeNow(),
|
||||
PartnerID: order.PartnerID,
|
||||
TransactionType: "TRANSACTION",
|
||||
}
|
||||
|
||||
_, err := s.transaction.Create(ctx, transaction)
|
||||
|
||||
return transaction, err
|
||||
}
|
||||
|
||||
func (s *orderSvc) addCustomerPoints(ctx mycontext.Context, customerID int64, points int) error {
|
||||
return s.customer.AddPoints(ctx, customerID, points)
|
||||
}
|
||||
|
||||
func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.Order, transaction *entity.Transaction, paymentMethod string) error {
|
||||
if order.CustomerID == nil || *order.CustomerID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
customer, err := s.customer.GetCustomer(ctx, *order.CustomerID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error getting customer details", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
branchName := "Bakso 343 Rawamangun"
|
||||
|
||||
var productIDs []int64
|
||||
productIDMap := make(map[int64]bool)
|
||||
for _, item := range order.OrderItems {
|
||||
if item.ItemID > 0 && !productIDMap[item.ItemID] {
|
||||
productIDs = append(productIDs, item.ItemID)
|
||||
productIDMap[item.ItemID] = true
|
||||
}
|
||||
}
|
||||
|
||||
productMap := make(map[int64]*entity.Product)
|
||||
if len(productIDs) > 0 {
|
||||
products, err := s.product.GetProductsByIDs(ctx, productIDs, order.PartnerID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error fetching products", zap.Error(err))
|
||||
} else {
|
||||
for _, product := range products {
|
||||
productMap[product.ID] = product
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var itemsData []map[string]string
|
||||
for _, item := range order.OrderItems {
|
||||
itemName := "Item"
|
||||
|
||||
if product, exists := productMap[item.ItemID]; exists {
|
||||
itemName = product.Name
|
||||
}
|
||||
|
||||
itemsData = append(itemsData, map[string]string{
|
||||
"ItemName": itemName,
|
||||
"Quantity": fmt.Sprintf("%d", item.Quantity),
|
||||
"Price": fmt.Sprintf("Rp %s", formatCurrency(item.Price)),
|
||||
})
|
||||
}
|
||||
|
||||
transactionDate := transaction.CreatedAt.Format("02 January 2006 15:04")
|
||||
viewTransactionLink := fmt.Sprintf("https://enaklo.co.id/transaction/%s", transaction.ID)
|
||||
|
||||
emailData := map[string]interface{}{
|
||||
"UserName": customer.Name,
|
||||
"BranchName": branchName,
|
||||
"TransactionNumber": order.ID,
|
||||
"TransactionDate": transactionDate,
|
||||
"PaymentMethod": formatPaymentMethod(paymentMethod),
|
||||
"Items": itemsData,
|
||||
"TotalPayment": fmt.Sprintf("Rp %s", formatCurrency(order.Total)),
|
||||
"ViewTransactionLink": viewTransactionLink,
|
||||
}
|
||||
|
||||
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",
|
||||
Data: emailData,
|
||||
})
|
||||
}
|
||||
|
||||
func formatCurrency(amount float64) string {
|
||||
return fmt.Sprintf("%.2f", amount)
|
||||
}
|
||||
|
||||
func formatPaymentMethod(method string) string {
|
||||
methodMap := map[string]string{
|
||||
"CASH": "Tunai",
|
||||
"QRIS": "QRIS",
|
||||
"CARD": "Kartu Kredit/Debit",
|
||||
}
|
||||
|
||||
if displayName, exists := methodMap[method]; exists {
|
||||
return displayName
|
||||
}
|
||||
return method
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"enaklo-pos-be/internal/common/mycontext"
|
||||
"enaklo-pos-be/internal/entity"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error)
|
||||
FindByID(ctx mycontext.Context, id int64) (*entity.Order, error)
|
||||
CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error)
|
||||
FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error)
|
||||
UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error
|
||||
}
|
||||
|
||||
type ProductService interface {
|
||||
GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error)
|
||||
GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error)
|
||||
}
|
||||
|
||||
type CustomerService 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)
|
||||
}
|
||||
|
||||
type TransactionService interface {
|
||||
Create(ctx mycontext.Context, transaction *entity.Transaction) (*entity.Transaction, error)
|
||||
}
|
||||
|
||||
type CryptService interface {
|
||||
GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error)
|
||||
ValidateJWTOrderInquiry(tokenString string) (int64, string, error)
|
||||
}
|
||||
|
||||
type NotificationService interface {
|
||||
SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
CreateOrderInquiry(ctx mycontext.Context,
|
||||
req *entity.OrderRequest) (*entity.OrderInquiryResponse, error)
|
||||
ExecuteOrderInquiry(ctx mycontext.Context,
|
||||
token string, paymentMethod string) (*entity.OrderResponse, error)
|
||||
}
|
||||
|
||||
type Config interface {
|
||||
GetOrderFee(source string) float64
|
||||
}
|
||||
|
||||
type orderSvc struct {
|
||||
repo Repository
|
||||
product ProductService
|
||||
customer CustomerService
|
||||
transaction TransactionService
|
||||
crypt CryptService
|
||||
cfg Config
|
||||
notification NotificationService
|
||||
}
|
||||
|
||||
func New(
|
||||
repo Repository,
|
||||
product ProductService,
|
||||
customer CustomerService,
|
||||
transaction TransactionService,
|
||||
crypt CryptService,
|
||||
cfg Config,
|
||||
notification NotificationService,
|
||||
) Service {
|
||||
return &orderSvc{
|
||||
repo: repo,
|
||||
product: product,
|
||||
customer: customer,
|
||||
transaction: transaction,
|
||||
crypt: crypt,
|
||||
cfg: cfg,
|
||||
notification: notification,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user