add order and payment
This commit is contained in:
+174
-100
@@ -1,147 +1,221 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"furtuna-be/internal/common/logger"
|
||||
"furtuna-be/internal/common/mycontext"
|
||||
"furtuna-be/internal/constants/order"
|
||||
"furtuna-be/internal/constants/transaction"
|
||||
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
|
||||
prodRepo repository.Product
|
||||
studioRepo repository.Studio
|
||||
repo repository.Order
|
||||
crypt repository.Crypto
|
||||
product repository.Product
|
||||
midtrans repository.Midtrans
|
||||
payment repository.Payment
|
||||
}
|
||||
|
||||
func NewOrderService(repo repository.Order, prodRepo repository.Product, studioRepo repository.Studio) *OrderService {
|
||||
func NewOrderService(repo repository.Order, product repository.Product, crypt repository.Crypto,
|
||||
midtrans repository.Midtrans, payment repository.Payment) *OrderService {
|
||||
return &OrderService{
|
||||
repo: repo,
|
||||
prodRepo: prodRepo,
|
||||
studioRepo: studioRepo,
|
||||
repo: repo,
|
||||
product: product,
|
||||
crypt: crypt,
|
||||
midtrans: midtrans,
|
||||
payment: payment,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderService) Create(ctx mycontext.Context, req *entity.Order) error {
|
||||
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
|
||||
}
|
||||
|
||||
for i, oi := range req.OrderItem {
|
||||
if oi.ItemType.IsProduct() {
|
||||
item, err := o.prodRepo.GetProductByID(ctx, oi.ItemID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get product item", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
req.OrderItem[i].Price = item.Price
|
||||
req.OrderItem[i].CreatedBy = ctx.RequestedBy()
|
||||
} else if oi.ItemType.IsStudio() {
|
||||
//get data studio
|
||||
item, err := o.studioRepo.GetStudioByID(ctx, oi.ItemID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get studio item", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
productMap := make(map[int64]*entity.ProductDB)
|
||||
for _, product := range products {
|
||||
productMap[product.ID] = product
|
||||
}
|
||||
|
||||
req.OrderItem[i].Price = item.Price
|
||||
req.OrderItem[i].CreatedBy = ctx.RequestedBy()
|
||||
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)
|
||||
}
|
||||
|
||||
orderDB := req.ToOrderDB()
|
||||
orderDB.CreatedBy = ctx.RequestedBy()
|
||||
orderDB.Transaction.CreatedBy = ctx.RequestedBy()
|
||||
|
||||
//set status
|
||||
orderDB.Status = order.Paid
|
||||
orderDB.Transaction.Status = transaction.Paid
|
||||
|
||||
orderDB, err := o.repo.CreateOrder(ctx, orderDB)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when create order", zap.Error(err))
|
||||
return err
|
||||
order := &entity.Order{
|
||||
PartnerID: req.PartnerID,
|
||||
RefID: generator.GenerateUUID(),
|
||||
Status: order2.New.String(),
|
||||
Amount: totalAmount,
|
||||
PaymentType: req.PaymentMethod,
|
||||
CreatedBy: req.CreatedBy,
|
||||
OrderItems: []entity.OrderItem{},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *OrderService) GetByID(ctx context.Context, id int64) (*entity.Order, error) {
|
||||
orderDB, err := s.repo.GetOrderByID(ctx, id)
|
||||
order, err = s.repo.Create(ctx, order)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get order by id", zap.Error(err))
|
||||
logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderDB.ToOrder(), nil
|
||||
}
|
||||
|
||||
func (s *OrderService) GetAll(ctx context.Context, search entity.OrderSearch) ([]*entity.Order, int, error) {
|
||||
orders, total, err := s.repo.GetAllOrders(ctx, search)
|
||||
token, err := s.crypt.GenerateJWTOrder(order)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get all orders", zap.Error(err))
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return orders.ToOrderList(), total, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) GetTotalRevenue(ctx context.Context, search entity.OrderTotalRevenueSearch) (float64, int64, error) {
|
||||
rev, trans, err := s.repo.GetTotalRevenue(ctx, search)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get total revenue orders", zap.Error(err))
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return rev, trans, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) GetYearlyRevenue(ctx context.Context, year int) (entity.OrderYearlyRevenueList, error) {
|
||||
result, err := s.repo.GetYearlyRevenue(ctx, year)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get yearly revenue orders", zap.Error(err))
|
||||
logger.ContextLogger(ctx).Error("error when create token", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return &entity.OrderResponse{
|
||||
Order: order,
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) GetBranchRevenue(ctx context.Context, req entity.OrderBranchRevenueSearch) (entity.OrderBranchRevenueList, error) {
|
||||
result, err := s.repo.GetBranchRevenue(ctx, req)
|
||||
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 get branch revenue orders", zap.Error(err))
|
||||
logger.ContextLogger(ctx).Error("error when validating JWT order", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) UpdateStatus(ctx mycontext.Context, id int64, orderReq *entity.Order) (*entity.Order, error) {
|
||||
existingOrder, err := s.repo.GetOrderByID(ctx, id)
|
||||
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
|
||||
}
|
||||
|
||||
//validate state
|
||||
if !existingOrder.Status.IsNew() {
|
||||
return nil, fmt.Errorf("Invalid order status")
|
||||
}
|
||||
|
||||
if existingOrder.Status == orderReq.Status {
|
||||
return nil, fmt.Errorf("Order status already %s", existingOrder.Status)
|
||||
}
|
||||
|
||||
existingOrder.ToUpdatedOrder(ctx.RequestedBy(), *orderReq)
|
||||
|
||||
_, err = s.repo.UpdateOrder(ctx, existingOrder)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when update user", zap.Error(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
|
||||
}
|
||||
|
||||
return existingOrder.ToOrder(), nil
|
||||
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
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
|
||||
BranchSvc: branch.NewBranchService(repo.Branch),
|
||||
StudioSvc: studio.NewStudioService(repo.Studio),
|
||||
ProductSvc: product.NewProductService(repo.Product),
|
||||
OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Studio),
|
||||
OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Crypto, repo.Midtrans, repo.Payment),
|
||||
OSSSvc: oss.NewOSSService(repo.OSS),
|
||||
PartnerSvc: partner.NewPartnerService(
|
||||
repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet),
|
||||
@@ -94,13 +94,8 @@ type Product interface {
|
||||
}
|
||||
|
||||
type Order interface {
|
||||
Create(ctx mycontext.Context, req *entity.Order) error
|
||||
UpdateStatus(ctx mycontext.Context, id int64, orderReq *entity.Order) (*entity.Order, error)
|
||||
GetByID(ctx context.Context, id int64) (*entity.Order, error)
|
||||
GetAll(ctx context.Context, search entity.OrderSearch) ([]*entity.Order, int, error)
|
||||
GetTotalRevenue(ctx context.Context, search entity.OrderTotalRevenueSearch) (float64, int64, error)
|
||||
GetYearlyRevenue(ctx context.Context, year int) (entity.OrderYearlyRevenueList, error)
|
||||
GetBranchRevenue(ctx context.Context, req entity.OrderBranchRevenueSearch) (entity.OrderBranchRevenueList, error)
|
||||
CreateOrder(ctx context.Context, req *entity.OrderRequest) (*entity.OrderResponse, error)
|
||||
Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error)
|
||||
}
|
||||
|
||||
type OSSService interface {
|
||||
|
||||
Reference in New Issue
Block a user