package order import ( "encoding/json" "errors" "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 } func NewOrderService(repo repository.Order, product repository.Product, crypt repository.Crypto, midtrans repository.Midtrans, payment repository.Payment) *OrderService { return &OrderService{ repo: repo, product: product, crypt: crypt, midtrans: midtrans, payment: payment, } } 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, }) } 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 } // Check for existing payment to handle idempotency 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: strconv.FormatInt(partnerID, 10), OrderID: strconv.FormatInt(order.ID, 10), 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 }