Update template email

This commit is contained in:
aditya.siregar
2025-03-08 00:35:23 +07:00
parent 3c80b710af
commit 18003313dd
54 changed files with 2309 additions and 199 deletions
+117
View File
@@ -0,0 +1,117 @@
package customer
import (
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/constants"
"enaklo-pos-be/internal/entity"
"github.com/pkg/errors"
"go.uber.org/zap"
"strings"
)
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)
AddPoints(ctx mycontext.Context, id int64, points 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)
}
type customerSvc struct {
repo Repository
}
func New(repo Repository) Service {
return &customerSvc{
repo: repo,
}
}
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")
}
newCustomer := &entity.Customer{
Name: req.Name,
Email: req.Email,
Phone: req.PhoneNumber,
Points: 0,
CreatedAt: constants.TimeNow(),
UpdatedAt: constants.TimeNow(),
}
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
}
func (s *customerSvc) AddPoints(ctx mycontext.Context, customerID int64, points int) error {
if points <= 0 {
return nil
}
err := s.repo.AddPoints(ctx, customerID, points)
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
}
@@ -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
}
+173
View File
@@ -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
}
+80
View File
@@ -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,
}
}
@@ -0,0 +1,33 @@
package product
import (
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
"github.com/pkg/errors"
"go.uber.org/zap"
)
func (s *productSvc) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) {
if len(ids) == 0 {
return []*entity.Product{}, nil
}
products, err := s.repo.GetProductsByIDs(ctx, ids, partnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get products by IDs",
zap.Int64s("productIDs", ids),
zap.Int64("partnerID", partnerID),
zap.Error(err))
return nil, errors.Wrap(err, "failed to get products by IDs")
}
// Validate that we found all requested products
if len(products) != len(ids) {
logger.ContextLogger(ctx).Warn("some products not found",
zap.Int("requestedCount", len(ids)),
zap.Int("foundCount", len(products)))
}
return products, nil
}
@@ -0,0 +1,56 @@
package product
import (
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
"github.com/pkg/errors"
"go.uber.org/zap"
)
func (s *productSvc) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) {
if len(productIDs) == 0 {
return &entity.ProductDetails{
Products: make(map[int64]*entity.Product),
}, nil
}
productDetails, err := s.repo.GetProductDetails(ctx, productIDs, partnerID)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get product details",
zap.Int64s("productIDs", productIDs),
zap.Int64("partnerID", partnerID),
zap.Error(err))
return nil, errors.Wrap(err, "failed to get product details")
}
if len(productDetails.Products) != len(productIDs) {
missingIDs := findMissingProductIDs(productIDs, productDetails.Products)
logger.ContextLogger(ctx).Warn("some products not found",
zap.Int("requestedCount", len(productIDs)),
zap.Int("foundCount", len(productDetails.Products)),
zap.Int64s("missingIDs", missingIDs))
if len(productDetails.Products) == 0 {
return nil, errors.New("no products found")
}
}
return productDetails, nil
}
func findMissingProductIDs(requestedIDs []int64, foundProducts map[int64]*entity.Product) []int64 {
var missingIDs []int64
for _, id := range requestedIDs {
if _, exists := foundProducts[id]; !exists {
missingIDs = append(missingIDs, id)
}
}
return missingIDs
}
func (s *productSvc) IsProductAvailable(product *entity.Product) bool {
return product.Status == "ACTIVE"
}
+26
View File
@@ -0,0 +1,26 @@
package product
import (
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
)
type Repository interface {
GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error)
GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error)
}
type Service interface {
GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error)
GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error)
}
type productSvc struct {
repo Repository
}
func New(repo Repository) Service {
return &productSvc{
repo: repo,
}
}