2024-08-02 02:27:57 +07:00

350 lines
10 KiB
Go

package order
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"furtuna-be/internal/common/logger"
"furtuna-be/internal/common/mycontext"
order2 "furtuna-be/internal/constants/order"
"furtuna-be/internal/entity"
"furtuna-be/internal/repository"
"furtuna-be/internal/utils/generator"
"go.uber.org/zap"
"golang.org/x/net/context"
"gorm.io/gorm"
"strconv"
"time"
)
type OrderService struct {
repo repository.Order
crypt repository.Crypto
product repository.Product
midtrans repository.Midtrans
payment repository.Payment
txmanager repository.TransactionManager
wallet repository.WalletRepository
}
func NewOrderService(
repo repository.Order,
product repository.Product, crypt repository.Crypto,
midtrans repository.Midtrans, payment repository.Payment,
txmanager repository.TransactionManager,
wallet repository.WalletRepository) *OrderService {
return &OrderService{
repo: repo,
product: product,
crypt: crypt,
midtrans: midtrans,
payment: payment,
txmanager: txmanager,
wallet: wallet,
}
}
func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderRequest) (*entity.OrderResponse, error) {
productIDs := make([]int64, len(req.OrderItems))
for i, item := range req.OrderItems {
productIDs[i] = item.ProductID
}
products, err := s.product.GetProductsByIDs(ctx, productIDs, req.PartnerID)
if err != nil {
logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err))
return nil, err
}
productMap := make(map[int64]*entity.ProductDB)
for _, product := range products {
productMap[product.ID] = product
}
totalAmount := 0.0
for _, item := range req.OrderItems {
product, ok := productMap[item.ProductID]
if !ok {
logger.ContextLogger(ctx).Error("product not found", zap.Int64("productID", item.ProductID))
return nil, errors.New("product not found")
}
totalAmount += product.Price * float64(item.Quantity)
}
order := &entity.Order{
PartnerID: req.PartnerID,
RefID: generator.GenerateUUID(),
Status: order2.New.String(),
Amount: totalAmount,
PaymentType: req.PaymentMethod,
SiteID: ctx.GetSiteID(),
CreatedBy: req.CreatedBy,
OrderItems: []entity.OrderItem{},
}
for _, item := range req.OrderItems {
order.OrderItems = append(order.OrderItems, entity.OrderItem{
ItemID: item.ProductID,
ItemType: productMap[item.ProductID].Type,
Price: productMap[item.ProductID].Price,
Quantity: item.Quantity,
CreatedBy: req.CreatedBy,
Product: productMap[item.ProductID].ToProduct(),
})
}
order, err = s.repo.Create(ctx, order)
if err != nil {
logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err))
return nil, err
}
token, err := s.crypt.GenerateJWTOrder(order)
if err != nil {
logger.ContextLogger(ctx).Error("error when create token", zap.Error(err))
return nil, err
}
return &entity.OrderResponse{
Order: order,
Token: token,
}, nil
}
func (s *OrderService) Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error) {
partnerID, orderID, err := s.crypt.ValidateJWTOrder(req.Token)
if err != nil {
logger.ContextLogger(ctx).Error("error when validating JWT order", zap.Error(err))
return nil, err
}
order, err := s.repo.FindByID(ctx, orderID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
logger.ContextLogger(ctx).Error("order not found", zap.Int64("orderID", orderID))
return nil, errors.New("order not found")
}
logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err))
return nil, err
}
payment, err := s.payment.FindByOrderAndPartnerID(ctx, orderID, partnerID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
logger.ContextLogger(ctx).Error("error getting payment data from db", zap.Error(err))
return nil, err
}
if payment != nil {
return s.createExecuteOrderResponse(order, payment), nil
}
if order.PartnerID != partnerID {
logger.ContextLogger(ctx).Error("partner ID mismatch", zap.Int64("orderID", orderID), zap.Int64("tokenPartnerID", partnerID), zap.Int64("orderPartnerID", order.PartnerID))
return nil, errors.New("partner ID mismatch")
}
if order.Status != "NEW" {
return nil, errors.New("invalid state")
}
resp := &entity.ExecuteOrderResponse{
Order: order,
}
if order.PaymentType != "CASH" {
paymentResponse, err := s.processNonCashPayment(ctx, order, partnerID, req.CreatedBy)
if err != nil {
return nil, err
}
resp.PaymentToken = paymentResponse.Token
resp.RedirectURL = paymentResponse.RedirectURL
}
order.SetExecutePaymentStatus()
order, err = s.repo.Update(ctx, order)
if err != nil {
logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err))
return nil, err
}
return resp, nil
}
func (s *OrderService) createExecuteOrderResponse(order *entity.Order, payment *entity.Payment) *entity.ExecuteOrderResponse {
var metadata map[string]string
if err := json.Unmarshal(payment.RequestMetadata, &metadata); err != nil {
logger.ContextLogger(context.Background()).Error("error unmarshaling request metadata", zap.Error(err))
return &entity.ExecuteOrderResponse{
Order: order,
}
}
return &entity.ExecuteOrderResponse{
Order: order,
PaymentToken: metadata["payment_token"],
RedirectURL: metadata["payment_redirect_url"],
}
}
func (s *OrderService) processNonCashPayment(ctx context.Context, order *entity.Order, partnerID, createdBy int64) (*entity.MidtransResponse, error) {
paymentRequest := entity.MidtransRequest{
PaymentReferenceID: generator.GenerateUUIDV4(),
TotalAmount: int64(order.Amount),
OrderItems: order.OrderItems,
}
paymentResponse, err := s.midtrans.CreatePayment(paymentRequest)
if err != nil {
logger.ContextLogger(ctx).Error("error when creating payment", zap.Error(err))
return nil, err
}
requestMetadata, err := json.Marshal(map[string]string{
"partner_id": strconv.FormatInt(partnerID, 10),
"created_by": strconv.FormatInt(createdBy, 10),
"payment_token": paymentResponse.Token,
"payment_redirect_url": paymentResponse.RedirectURL,
})
if err != nil {
logger.ContextLogger(ctx).Error("error when marshaling request metadata", zap.Error(err))
return nil, err
}
payment := &entity.Payment{
PartnerID: partnerID,
OrderID: order.ID,
ReferenceID: paymentRequest.PaymentReferenceID,
Channel: "XENDIT",
PaymentType: order.PaymentType,
Amount: order.Amount,
State: "PENDING",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
RequestMetadata: requestMetadata,
}
_, err = s.payment.Create(ctx, payment)
if err != nil {
logger.ContextLogger(ctx).Error("error when creating payment record", zap.Error(err))
return nil, err
}
return paymentResponse, nil
}
func (s *OrderService) ProcessCallback(ctx context.Context, req *entity.CallbackRequest) error {
tx, err := s.txmanager.Begin(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
err = s.processPayment(ctx, tx, req)
if err != nil {
return fmt.Errorf("failed to process payment: %w", err)
}
return tx.Commit().Error
}
func (s *OrderService) processPayment(ctx context.Context, tx *gorm.DB, req *entity.CallbackRequest) error {
existingPayment, err := s.payment.FindByReferenceID(ctx, tx, req.TransactionID)
if err != nil {
return fmt.Errorf("failed to retrieve payment: %w", err)
}
existingPayment.State = updatePaymentState(req.TransactionStatus)
_, err = s.payment.UpdateWithTx(ctx, tx, existingPayment)
if err != nil {
return fmt.Errorf("failed to update payment: %w", err)
}
if err := s.updateOrderStatus(ctx, tx, existingPayment.State, existingPayment.OrderID); err != nil {
return fmt.Errorf("failed to update order status: %w", err)
}
if existingPayment.State == "PAID" {
if err := s.updateWalletBalance(ctx, tx, existingPayment.PartnerID, existingPayment.Amount); err != nil {
return fmt.Errorf("failed to update wallet balance: %w", err)
}
}
return nil
}
func updatePaymentState(status string) string {
switch status {
case "settlement", "capture":
return "PAID"
case "expire", "deny", "cancel", "failure":
return "EXPIRED"
default:
return status
}
}
func (s *OrderService) updateOrderStatus(ctx context.Context, tx *gorm.DB, status string, orderID int64) error {
if status != "PENDING" {
return s.repo.SetOrderStatus(ctx, tx, orderID, status)
}
return nil
}
func (s *OrderService) updateWalletBalance(ctx context.Context, tx *gorm.DB, partnerID int64, amount float64) error {
wallet, err := s.wallet.GetByPartnerID(ctx, tx, partnerID)
if err != nil {
return fmt.Errorf("failed to get wallet: %w", err)
}
wallet.Balance += amount
_, err = s.wallet.Update(ctx, tx, wallet)
return err
}
func (s *OrderService) GetAllHistoryOrders(ctx mycontext.Context, req entity.OrderSearch) ([]*entity.HistoryOrder, int, error) {
historyOrders, total, err := s.repo.GetAllHystoryOrders(ctx, req)
if err != nil {
logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err))
return nil, 0, err
}
data := historyOrders.ToHistoryOrderList()
return data, total, nil
}
func (s OrderService) CountSoldOfTicket(ctx mycontext.Context, req entity.OrderSearch) (*entity.TicketSold, error) {
ticket, err := s.repo.CountSoldOfTicket(ctx, req)
if err != nil {
logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err))
return nil, err
}
data := ticket.ToTicketSold()
return data, nil
}
func (s OrderService) GetDailySales(ctx mycontext.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error) {
dailySales, err := s.repo.GetDailySalesMetrics(ctx, req)
if err != nil {
logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err))
return nil, err
}
return dailySales, nil
}
func (s OrderService) SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.Order, error) {
amount, err := s.repo.SumAmount(ctx, req)
if err != nil {
logger.ContextLogger(ctx).Error("error when get amount cash orders", zap.Error(err))
return nil, err
}
data := amount.ToSumAmount()
return data, nil
}