package order import ( "database/sql" errors2 "enaklo-pos-be/internal/common/errors" "enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/mycontext" order2 "enaklo-pos-be/internal/constants/order" "enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/repository" "enaklo-pos-be/internal/utils/generator" "encoding/json" "errors" "fmt" "go.uber.org/zap" "golang.org/x/net/context" "gorm.io/gorm" "strconv" "time" ) type Config interface { GetOrderFee(source string) float64 } type OrderService struct { repo repository.Order crypt repository.Crypto product repository.Product pg repository.PaymentGateway payment repository.Payment transaction repository.TransactionRepository txmanager repository.TransactionManager wallet repository.WalletRepository linkquRepo repository.LinkQu cfg Config } func NewOrderService( repo repository.Order, product repository.Product, crypt repository.Crypto, pg repository.PaymentGateway, payment repository.Payment, txmanager repository.TransactionManager, wallet repository.WalletRepository, cfg Config, transaction repository.TransactionRepository, linkquRepo repository.LinkQu, ) *OrderService { return &OrderService{ repo: repo, product: product, crypt: crypt, pg: pg, payment: payment, txmanager: txmanager, wallet: wallet, cfg: cfg, transaction: transaction, linkquRepo: linkquRepo, } } func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderRequest) (*entity.OrderResponse, error) { productIDs, filteredItems := s.filterOrderItems(req.OrderItems) if len(productIDs) == 0 { return nil, errors2.ErrorBadRequest } req.OrderItems = filteredItems if len(productIDs) < 1 { return nil, errors2.ErrorBadRequest } 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, Status: order2.New.String(), Amount: totalAmount, Total: totalAmount + s.cfg.GetOrderFee(req.Source), Fee: s.cfg.GetOrderFee(req.Source), PaymentType: req.PaymentMethod, CreatedBy: req.CreatedBy, OrderItems: []entity.OrderItem{}, Source: req.Source, } 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: int(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 } order, err = s.repo.FindByID(ctx, order.ID) if err != nil { logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err)) return nil, err } return &entity.OrderResponse{ Order: order, }, nil } func (s *OrderService) filterOrderItems(items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest) { var productIDs []int64 var filteredItems []entity.OrderItemRequest for _, item := range items { if item.Quantity != 0 { productIDs = append(productIDs, item.ProductID) filteredItems = append(filteredItems, item) } } return productIDs, filteredItems } func (s *OrderService) CheckInInquiry(ctx mycontext.Context, qrCode string, partnerID *int64) (*entity.CheckinResponse, error) { order, err := s.repo.FindByQRCode(ctx, qrCode) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors2.NewErrorMessage(errors2.ErrorInvalidRequest, "Not Valid QR Code") } logger.ContextLogger(ctx).Error("error when getting order by QR code", zap.Error(err)) return nil, err } if order.PartnerID != *partnerID { return nil, errors2.ErrorBadRequest } if order.Status != "PAID" { return nil, errors2.ErrorInvalidRequest } token, err := s.crypt.GenerateJWTOrder(order) if err != nil { logger.ContextLogger(ctx).Error("error when generate checkin token", zap.Error(err)) return nil, err } orderResponse := &entity.CheckinResponse{ Token: token, } return orderResponse, nil } func (s *OrderService) CheckInExecute(ctx mycontext.Context, token string, partnerID *int64) (*entity.CheckinExecute, error) { pID, orderID, err := s.crypt.ValidateJWTOrder(token) if err != nil { logger.ContextLogger(ctx).Error("error when validating JWT order", zap.Error(err)) return nil, err } if pID != *partnerID { return nil, errors2.ErrorBadRequest } order, err := s.repo.FindByID(ctx, orderID) if err != nil { logger.ContextLogger(ctx).Error("error when getting order by ID", zap.Error(err)) return nil, err } resp := &entity.CheckinExecute{ Order: order, } return resp, nil } func (s *OrderService) Execute(ctx mycontext.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, } 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.PaymentRequest{ PaymentReferenceID: generator.GenerateUUIDV4(), TotalAmount: int64(order.Total), //OrderItems: order.OrderItems, Provider: order.PaymentType, } paymentResponse, err := s.pg.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: "MIDTRANS", 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 &entity.MidtransResponse{ Token: paymentResponse.Token, RedirectURL: paymentResponse.RedirectURL, }, nil } func (s *OrderService) processQRPayment(ctx mycontext.Context, order *entity.Order, partnerID, createdBy int64) (*entity.PaymentResponse, error) { paymentRequest := entity.PaymentRequest{ PaymentReferenceID: generator.GenerateUUIDV4(), TotalAmount: int64(order.Total), Provider: "LINKQU", CustomerID: fmt.Sprintf("POS-%d", ctx.RequestedBy()), CustomerName: fmt.Sprintf("POS-%s", ctx.GetName()), } paymentResponse, err := s.pg.CreateQRISPayment(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), "qr_code": paymentResponse.QRCodeURL, }) 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: "LINKQU", 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) processVAPayment(ctx mycontext.Context, order *entity.Order, partnerID, createdBy int64) (*entity.PaymentResponse, error) { paymentRequest := entity.PaymentRequest{ PaymentReferenceID: generator.GenerateUUIDV4(), TotalAmount: int64(order.Total), Provider: "LINKQU", CustomerID: strconv.FormatInt(order.User.ID, 10), CustomerName: order.User.Name, CustomerEmail: order.User.Email, } paymentResponse, err := s.pg.CreatePaymentVA(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{ "virtual_account": paymentResponse.VirtualAccountNumber, "bank_name": paymentResponse.BankName, "bank_code": paymentResponse.BankCode, }) 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: "LINKQU", 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) } order, err := s.repo.FindByID(ctx, existingPayment.OrderID) if err != nil { return fmt.Errorf("failed to get order: %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) } transaction := &entity.Transaction{ PartnerID: existingPayment.PartnerID, TransactionType: "PAYMENT_RECEIVED", Status: "SUCCESS", CreatedBy: 0, Amount: existingPayment.Amount, Fee: order.Fee, Total: order.Total, } if _, err = s.transaction.Create(ctx, tx, transaction); err != nil { return fmt.Errorf("failed to update transaction: %w", err) } } return nil } func updatePaymentState(status string) string { switch status { case "settlement", "capture", "paid", "settle": return "PAID" case "expire", "deny", "cancel", "failure", "EXPIRED": 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) GetPaymentDistribution(ctx mycontext.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) { paymentDistribution, err := s.repo.GetPaymentTypeDistribution(ctx, req) if err != nil { logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err)) return nil, err } return paymentDistribution, 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 } func (s *OrderService) GetByID(ctx mycontext.Context, id int64, referenceID string) (*entity.Order, error) { if referenceID != "" { payment, err := s.payment.FindByReferenceID(ctx, nil, referenceID) if err != nil { logger.ContextLogger(ctx).Error("error when getting payment by IDs", zap.Error(err)) return nil, err } id = payment.OrderID } order, err := s.repo.FindByID(ctx, id) if err != nil { logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err)) return nil, err } if ctx.IsCasheer() { return order, nil } //if order.CreatedBy != ctx.RequestedBy() { // return nil, errors2.NewError(errors2.ErrorBadRequest.ErrorType(), "order not found") //} return order, nil } func (s *OrderService) GetPrintDetail(ctx mycontext.Context, id int64) (*entity.OrderPrintDetail, error) { order, err := s.repo.FindPrintDetailByID(ctx, id) if err != nil { logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err)) return nil, err } return order, nil } func (s *OrderService) ProcessLinkQuCallback(ctx context.Context, req *entity.LinkQuCallback) 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() pay, err := s.linkquRepo.CheckPaymentStatus(req.PaymentReff) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } if pay.ResponseCode != "00" { return nil } err = s.processPayment(ctx, tx, &entity.CallbackRequest{ TransactionID: req.PartnerReff, TransactionStatus: pay.Data.StatusPaid, }) if err != nil { return fmt.Errorf("failed to process payment: %w", err) } return tx.Commit().Error }