318 lines
9.3 KiB
Go
318 lines
9.3 KiB
Go
package order
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"furtuna-be/internal/common/logger"
|
|
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 context.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,
|
|
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 context.Context, tokenString string, req entity.HistoryOrderSearch) ([]*entity.HistoryOrder, int, error) {
|
|
claims, err := s.crypt.ParseAndValidateJWT(tokenString)
|
|
|
|
if err != nil {
|
|
logger.ContextLogger(ctx).Error("error when get data token", zap.Error(err))
|
|
return nil, 0, err
|
|
}
|
|
|
|
historyOrders, total, err := s.repo.GetAllHystoryOrders(ctx, *claims, 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
|
|
}
|