This commit is contained in:
aditya.siregar
2025-04-10 11:21:08 +07:00
parent c642c5c61b
commit 09c9a4d59d
30 changed files with 1797 additions and 758 deletions
-623
View File
@@ -1,623 +0,0 @@
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
}
+5 -5
View File
@@ -7,7 +7,6 @@ import (
"enaklo-pos-be/internal/services/discovery"
service "enaklo-pos-be/internal/services/license"
"enaklo-pos-be/internal/services/member"
"enaklo-pos-be/internal/services/order"
"enaklo-pos-be/internal/services/oss"
"enaklo-pos-be/internal/services/partner"
"enaklo-pos-be/internal/services/product"
@@ -18,6 +17,7 @@ import (
customerSvc "enaklo-pos-be/internal/services/v2/customer"
"enaklo-pos-be/internal/services/v2/inprogress_order"
orderSvc "enaklo-pos-be/internal/services/v2/order"
"enaklo-pos-be/internal/services/v2/partner_settings"
productSvc "enaklo-pos-be/internal/services/v2/product"
"gorm.io/gorm"
@@ -35,7 +35,6 @@ type ServiceManagerImpl struct {
UserSvc User
StudioSvc Studio
ProductSvc Product
OrderSvc Order
OSSSvc OSSService
PartnerSvc Partner
SiteSvc Site
@@ -55,15 +54,16 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
custSvcV2 := customerSvc.New(repo.CustomerRepo, repo.EmailService)
productSvcV2 := productSvc.New(repo.ProductRepo)
inprogressOrder := inprogress_order.NewInProgressOrderService(repo.InProgressOrderRepo)
partnerSettings := partner_settings.NewPartnerSettingsService(repo.PartnerSetting)
orderService := orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings)
inprogressOrder := inprogress_order.NewInProgressOrderService(repo.OrderRepo, orderService, productSvcV2)
return &ServiceManagerImpl{
AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License),
EventSvc: event.NewEventService(repo.Event),
UserSvc: users.NewUserService(repo.User),
StudioSvc: studio.NewStudioService(repo.Studio),
ProductSvc: product.NewProductService(repo.Product),
OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Crypto, repo.PG, repo.Payment, repo.Trx, repo.Wallet, &cfg.Order, repo.Transaction, repo.LinkQu),
OSSSvc: oss.NewOSSService(repo.OSS),
PartnerSvc: partner.NewPartnerService(
repo.Partner, users.NewUserService(repo.User), repo.Trx, repo.Wallet, repo.User),
@@ -72,7 +72,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx),
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery, repo.Product),
OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService),
OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings),
MemberRegistrationSvc: member.NewMemberRegistrationService(repo.MemberRepository, repo.EmailService, custSvcV2),
CustomerV2Svc: custSvcV2,
InProgressSvc: inprogressOrder,
@@ -3,28 +3,98 @@ package inprogress_order
import (
"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/services/v2/order"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type InProgressOrderService interface {
Save(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error)
GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error)
Save(ctx mycontext.Context, order *entity.OrderRequest) (*entity.Order, error)
GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error)
}
type OrderRepository interface {
CreateOrUpdate(ctx mycontext.Context, order *entity.Order) (*entity.Order, error)
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error)
}
type OrderCalculator interface {
CalculateOrderTotals(
ctx mycontext.Context,
items []entity.OrderItemRequest,
productDetails *entity.ProductDetails,
source string,
) (*entity.OrderCalculation, error)
ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error)
}
type inProgressOrderSvc struct {
repo repository.InProgressOrderRepository
repo OrderRepository
orderCalculator OrderCalculator
product order.ProductService
}
func NewInProgressOrderService(repo repository.InProgressOrderRepository) InProgressOrderService {
func NewInProgressOrderService(repo OrderRepository, calculator OrderCalculator, product order.ProductService) InProgressOrderService {
return &inProgressOrderSvc{
repo: repo,
repo: repo,
orderCalculator: calculator,
product: product,
}
}
func (s *inProgressOrderSvc) Save(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) {
func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) {
productIDs, filteredItems, err := s.orderCalculator.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.orderCalculator.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source)
if err != nil {
return nil, err
}
orderItems := make([]entity.OrderItem, len(req.OrderItems))
for i, item := range req.OrderItems {
product, exists := productDetails.Products[item.ProductID]
productName := ""
if exists {
productName = product.Name
}
orderItems[i] = entity.OrderItem{
ItemID: item.ProductID,
ItemName: productName,
Quantity: item.Quantity,
Price: product.Price,
ItemType: product.Type,
}
}
order := &entity.Order{
ID: req.ID,
PartnerID: req.PartnerID,
CustomerID: req.CustomerID,
CustomerName: req.CustomerName,
CreatedBy: req.CreatedBy,
OrderItems: orderItems,
TableNumber: req.TableNumber,
OrderType: req.OrderType,
Total: orderCalculation.Total,
Tax: orderCalculation.Tax,
Amount: orderCalculation.Subtotal,
Status: order2.Pending.String(),
Source: req.Source,
}
createdOrder, err := s.repo.CreateOrUpdate(ctx, order)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create in-progress order",
@@ -36,8 +106,8 @@ func (s *inProgressOrderSvc) Save(ctx mycontext.Context, order *entity.InProgres
return createdOrder, nil
}
func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) {
orders, err := s.repo.GetListByPartnerID(ctx, partnerID, limit, offset)
func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) {
orders, err := s.repo.GetListByPartnerID(ctx, partnerID, limit, offset, order2.Pending.String())
if err != nil {
logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID",
zap.Error(err),
@@ -7,11 +7,12 @@ import (
"enaklo-pos-be/internal/constants"
"enaklo-pos-be/internal/entity"
"go.uber.org/zap"
"math"
)
func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) {
productIDs, filteredItems, err := s.validateOrderItems(ctx, req.OrderItems)
productIDs, filteredItems, err := s.ValidateOrderItems(ctx, req.OrderItems)
if err != nil {
return nil, err
}
@@ -23,7 +24,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
return nil, err
}
orderCalculation, err := s.calculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source)
orderCalculation, err := s.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source)
if err != nil {
return nil, err
}
@@ -43,7 +44,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
req.PartnerID,
customerID,
orderCalculation.Subtotal,
orderCalculation.Fee,
orderCalculation.Tax,
orderCalculation.Total,
req.PaymentMethod,
req.Source,
@@ -79,7 +80,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
}, nil
}
func (s *orderSvc) validateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) {
func (s *orderSvc) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) {
var productIDs []int64
var filteredItems []entity.OrderItemRequest
@@ -98,7 +99,7 @@ func (s *orderSvc) validateOrderItems(ctx mycontext.Context, items []entity.Orde
return productIDs, filteredItems, nil
}
func (s *orderSvc) calculateOrderTotals(
func (s *orderSvc) CalculateOrderTotals(
ctx mycontext.Context,
items []entity.OrderItemRequest,
productDetails *entity.ProductDetails,
@@ -114,12 +115,23 @@ func (s *orderSvc) calculateOrderTotals(
subtotal += product.Price * float64(item.Quantity)
}
fee := s.cfg.GetOrderFee(source)
partnerID := ctx.GetPartnerID()
setting, err := s.partnerSetting.GetSettings(ctx, *partnerID)
if err != nil {
return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "failed to get partner settings")
}
tax := 0.0
if setting.TaxEnabled {
tax = (setting.TaxPercentage / 100) * subtotal
tax = math.Round(tax/100) * 100
}
return &entity.OrderCalculation{
Subtotal: subtotal,
Fee: fee,
Total: subtotal + fee,
Tax: tax,
Total: subtotal + tax,
}, nil
}
@@ -145,3 +157,79 @@ func (s *orderSvc) validateInquiry(ctx mycontext.Context, token string) (*entity
return inquiry, nil
}
func (s *orderSvc) GetOrderPaymentAnalysis(
ctx mycontext.Context,
partnerID int64,
req entity.SearchRequest,
) (*entity.OrderPaymentAnalysis, error) {
paymentBreakdown, err := s.repo.GetOrderPaymentMethodBreakdown(ctx, partnerID, req)
if err != nil {
return nil, err
}
var totalAmount float64
var totalTransactions int64
for _, breakdown := range paymentBreakdown {
totalAmount += breakdown.TotalAmount
totalTransactions += breakdown.TotalTransactions
}
return &entity.OrderPaymentAnalysis{
TotalAmount: totalAmount,
TotalTransactions: totalTransactions,
PaymentMethodBreakdown: paymentBreakdown,
}, nil
}
func (s *orderSvc) GetRevenueOverview(
ctx mycontext.Context,
partnerID int64,
year int,
granularity string,
status string,
) ([]entity.RevenueOverviewItem, error) {
req := entity.RevenueOverviewRequest{
PartnerID: partnerID,
Year: year,
Granularity: granularity,
Status: status,
}
return s.repo.GetRevenueOverview(ctx, req)
}
func (s *orderSvc) GetSalesByCategory(
ctx mycontext.Context,
partnerID int64,
period string,
status string,
) ([]entity.SalesByCategoryItem, error) {
req := entity.SalesByCategoryRequest{
PartnerID: partnerID,
Period: period,
Status: status,
}
return s.repo.GetSalesByCategory(ctx, req)
}
func (s *orderSvc) GetPopularProducts(
ctx mycontext.Context,
partnerID int64,
period string,
status string,
limit int,
sortBy string,
) ([]entity.PopularProductItem, error) {
req := entity.PopularProductsRequest{
PartnerID: partnerID,
Period: period,
Status: status,
Limit: limit,
SortBy: sortBy,
}
return s.repo.GetPopularProducts(ctx, req)
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context,
token string, paymentMethod, paymentProvider, inprogressOrderID string) (*entity.OrderResponse, error) {
token string, paymentMethod, paymentProvider string, inprogressOrderID int64) (*entity.OrderResponse, error) {
inquiry, err := s.validateInquiry(ctx, token)
if err != nil {
return nil, err
+79 -15
View File
@@ -12,6 +12,24 @@ type Repository interface {
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
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
GetOrderPaymentMethodBreakdown(
ctx mycontext.Context,
partnerID int64,
req entity.SearchRequest,
) ([]entity.PaymentMethodBreakdown, error)
GetRevenueOverview(
ctx mycontext.Context,
req entity.RevenueOverviewRequest,
) ([]entity.RevenueOverviewItem, error)
GetSalesByCategory(
ctx mycontext.Context,
req entity.SalesByCategoryRequest,
) ([]entity.SalesByCategoryItem, error)
GetPopularProducts(
ctx mycontext.Context,
req entity.PopularProductsRequest,
) ([]entity.PopularProductItem, error)
}
type ProductService interface {
@@ -42,21 +60,65 @@ type Service interface {
CreateOrderInquiry(ctx mycontext.Context,
req *entity.OrderRequest) (*entity.OrderInquiryResponse, error)
ExecuteOrderInquiry(ctx mycontext.Context,
token string, paymentMethod, paymentProvider, inProgressOrderID string) (*entity.OrderResponse, error)
token string, paymentMethod, paymentProvider string, inProgressOrderID int64) (*entity.OrderResponse, error)
GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
CalculateOrderTotals(
ctx mycontext.Context,
items []entity.OrderItemRequest,
productDetails *entity.ProductDetails,
source string,
) (*entity.OrderCalculation, error)
ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error)
GetOrderPaymentAnalysis(
ctx mycontext.Context,
partnerID int64,
req entity.SearchRequest,
) (*entity.OrderPaymentAnalysis, error)
GetRevenueOverview(
ctx mycontext.Context,
partnerID int64,
year int,
granularity string,
status string,
) ([]entity.RevenueOverviewItem, error)
GetSalesByCategory(
ctx mycontext.Context,
partnerID int64,
period string,
status string,
) ([]entity.SalesByCategoryItem, error)
GetPopularProducts(
ctx mycontext.Context,
partnerID int64,
period string,
status string,
limit int,
sortBy string,
) ([]entity.PopularProductItem, error)
}
type Config interface {
GetOrderFee(source string) float64
}
type PartnerSettings interface {
GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error)
}
type InProgressOrderRepository interface {
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error)
}
type orderSvc struct {
repo Repository
product ProductService
customer CustomerService
transaction TransactionService
crypt CryptService
cfg Config
notification NotificationService
repo Repository
product ProductService
customer CustomerService
transaction TransactionService
crypt CryptService
cfg Config
notification NotificationService
partnerSetting PartnerSettings
inprogressOrder InProgressOrderRepository
}
func New(
@@ -67,14 +129,16 @@ func New(
crypt CryptService,
cfg Config,
notification NotificationService,
partnerSetting PartnerSettings,
) Service {
return &orderSvc{
repo: repo,
product: product,
customer: customer,
transaction: transaction,
crypt: crypt,
cfg: cfg,
notification: notification,
repo: repo,
product: product,
customer: customer,
transaction: transaction,
crypt: crypt,
cfg: cfg,
notification: notification,
partnerSetting: partnerSetting,
}
}
@@ -0,0 +1,10 @@
package order
import (
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
)
func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) {
return s.repo.GetOrderHistoryByPartnerID(ctx, partnerID, request)
}
@@ -0,0 +1,149 @@
package partner_settings
import (
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository"
"encoding/json"
"github.com/pkg/errors"
)
type PartnerSettingsService interface {
GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error)
UpdateSettings(ctx mycontext.Context, settings *entity.PartnerSettings) error
GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error)
AddPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error
UpdatePaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error
DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error
ReorderPaymentMethods(ctx mycontext.Context, partnerID int64, methodIDs []int64) error
GetBusinessHours(ctx mycontext.Context, partnerID int64) (*entity.BusinessHoursSetting, error)
UpdateBusinessHours(ctx mycontext.Context, partnerID int64, hours *entity.BusinessHoursSetting) error
}
type partnerSettingsService struct {
settingsRepo repository.PartnerSettingsRepository
}
func NewPartnerSettingsService(settingsRepo repository.PartnerSettingsRepository) PartnerSettingsService {
return &partnerSettingsService{
settingsRepo: settingsRepo,
}
}
func (s *partnerSettingsService) GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) {
return s.settingsRepo.GetByPartnerID(ctx, partnerID)
}
func (s *partnerSettingsService) UpdateSettings(ctx mycontext.Context, settings *entity.PartnerSettings) error {
if settings == nil {
return errors.New("settings cannot be nil")
}
// Validate tax percentage
if settings.TaxEnabled && (settings.TaxPercentage < 0 || settings.TaxPercentage > 100) {
return errors.New("tax percentage must be between 0 and 100")
}
return s.settingsRepo.Upsert(ctx, settings)
}
func (s *partnerSettingsService) GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) {
return s.settingsRepo.GetPaymentMethods(ctx, partnerID)
}
func (s *partnerSettingsService) AddPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error {
if method == nil {
return errors.New("payment method cannot be nil")
}
method.ID = 0
return s.settingsRepo.UpsertPaymentMethod(ctx, method)
}
func (s *partnerSettingsService) UpdatePaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error {
if method == nil {
return errors.New("payment method cannot be nil")
}
if method.ID <= 0 {
return errors.New("invalid payment method ID")
}
return s.settingsRepo.UpsertPaymentMethod(ctx, method)
}
func (s *partnerSettingsService) DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error {
if id <= 0 {
return errors.New("invalid payment method ID")
}
return s.settingsRepo.DeletePaymentMethod(ctx, id, partnerID)
}
func (s *partnerSettingsService) ReorderPaymentMethods(ctx mycontext.Context, partnerID int64, methodIDs []int64) error {
if len(methodIDs) == 0 {
return errors.New("method IDs cannot be empty")
}
return s.settingsRepo.UpdatePaymentMethodOrder(ctx, partnerID, methodIDs)
}
// GetBusinessHours retrieves parsed business hours for a partner
func (s *partnerSettingsService) GetBusinessHours(ctx mycontext.Context, partnerID int64) (*entity.BusinessHoursSetting, error) {
settings, err := s.settingsRepo.GetByPartnerID(ctx, partnerID)
if err != nil {
return nil, err
}
// Create default hours if not set
if settings.BusinessHours == "" {
defaultHours := createDefaultBusinessHours()
return defaultHours, nil
}
var hours entity.BusinessHoursSetting
if err := json.Unmarshal([]byte(settings.BusinessHours), &hours); err != nil {
return nil, errors.Wrap(err, "failed to parse business hours")
}
return &hours, nil
}
func (s *partnerSettingsService) UpdateBusinessHours(ctx mycontext.Context, partnerID int64, hours *entity.BusinessHoursSetting) error {
if hours == nil {
return errors.New("business hours cannot be nil")
}
settings, err := s.settingsRepo.GetByPartnerID(ctx, partnerID)
if err != nil {
return err
}
// Serialize hours to JSON
hoursJSON, err := json.Marshal(hours)
if err != nil {
return errors.Wrap(err, "failed to serialize business hours")
}
settings.BusinessHours = string(hoursJSON)
return s.settingsRepo.Upsert(ctx, settings)
}
func createDefaultBusinessHours() *entity.BusinessHoursSetting {
defaultDay := entity.DayHours{
Open: "08:00",
Close: "22:00",
}
return &entity.BusinessHoursSetting{
Monday: defaultDay,
Tuesday: defaultDay,
Wednesday: defaultDay,
Thursday: defaultDay,
Friday: defaultDay,
Saturday: defaultDay,
Sunday: defaultDay,
}
}