mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Feat[BE]: implement FIFO stock management for marketing delivery products, including migration scripts and updates to related services and DTOs
This commit is contained in:
@@ -15,10 +15,10 @@ import (
|
||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -30,12 +30,12 @@ type DeliveryOrdersService interface {
|
||||
}
|
||||
|
||||
type deliveryOrdersService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
MarketingRepo marketingRepo.MarketingRepository
|
||||
MarketingProductRepo marketingRepo.MarketingProductRepository
|
||||
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
FifoSvc commonSvc.FifoService
|
||||
}
|
||||
|
||||
func NewDeliveryOrdersService(
|
||||
@@ -43,15 +43,16 @@ func NewDeliveryOrdersService(
|
||||
marketingProductRepo marketingRepo.MarketingProductRepository,
|
||||
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
validate *validator.Validate,
|
||||
) DeliveryOrdersService {
|
||||
return &deliveryOrdersService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
MarketingRepo: marketingRepo,
|
||||
MarketingProductRepo: marketingProductRepo,
|
||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
FifoSvc: fifoSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +109,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get marketings: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
for i := range marketings {
|
||||
@@ -116,7 +116,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err)
|
||||
continue
|
||||
}
|
||||
marketings[i].LatestApproval = latestApproval
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
||||
itemDeliveryDate = &parsedDate
|
||||
}
|
||||
|
||||
deliveryProduct.Qty = requestedProduct.Qty
|
||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
||||
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
||||
@@ -256,7 +256,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
||||
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
||||
|
||||
if requestedProduct.Qty > 0 {
|
||||
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil {
|
||||
|
||||
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -354,8 +355,9 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
itemDeliveryDate = deliveryProduct.DeliveryDate
|
||||
}
|
||||
|
||||
oldQty := deliveryProduct.Qty
|
||||
deliveryProduct.Qty = requestedProduct.Qty
|
||||
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
|
||||
|
||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
||||
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
||||
@@ -363,14 +365,18 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
deliveryProduct.DeliveryDate = itemDeliveryDate
|
||||
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
||||
|
||||
qtyChange := requestedProduct.Qty - oldQty
|
||||
if qtyChange > 0 {
|
||||
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil {
|
||||
return err
|
||||
if requestedProduct.Qty != oldRequestedQty {
|
||||
|
||||
if oldRequestedQty > 0 {
|
||||
if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if qtyChange < 0 {
|
||||
if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil {
|
||||
return err
|
||||
|
||||
if requestedProduct.Qty > 0 {
|
||||
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,50 +399,79 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
return s.getMarketingWithDeliveries(c, id)
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error {
|
||||
func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64) error {
|
||||
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
||||
}
|
||||
|
||||
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||
if deliveryProduct == nil || deliveryProduct.Id == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
|
||||
}
|
||||
|
||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
||||
UsableID: deliveryProduct.Id,
|
||||
ProductWarehouseID: marketingProduct.ProductWarehouseId,
|
||||
Quantity: requestedQty,
|
||||
AllowPending: false,
|
||||
Tx: tx,
|
||||
})
|
||||
|
||||
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||
|
||||
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
|
||||
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||
pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
||||
if err2 != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
|
||||
|
||||
if pw == nil || pw.Quantity < requestedQty {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { if pw != nil { return pw.Quantity } else { return 0 } }(), requestedQty))
|
||||
}
|
||||
|
||||
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if pw.Quantity < qtyDeliver {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver))
|
||||
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||
}
|
||||
|
||||
pw.Quantity = pw.Quantity - qtyDeliver
|
||||
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error {
|
||||
func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct) error {
|
||||
if deliveryProduct == nil || deliveryProduct.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
||||
}
|
||||
|
||||
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
||||
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
|
||||
}
|
||||
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
|
||||
currentUsage = 0
|
||||
}
|
||||
|
||||
pw.Quantity = pw.Quantity + qtyRestore
|
||||
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
|
||||
if currentUsage == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
||||
UsableID: deliveryProduct.Id,
|
||||
Tx: tx,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -307,15 +307,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
|
||||
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
mdp := &entity.MarketingDeliveryProduct{
|
||||
MarketingProductId: old.Id,
|
||||
Qty: 0,
|
||||
UnitPrice: 0,
|
||||
TotalWeight: 0,
|
||||
AvgWeight: 0,
|
||||
TotalPrice: 0,
|
||||
DeliveryDate: nil,
|
||||
VehicleNumber: rp.VehicleNumber,
|
||||
UsageQty: 0,
|
||||
PendingQty: 0,
|
||||
}
|
||||
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
|
||||
@@ -340,7 +342,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
}
|
||||
if err == nil {
|
||||
|
||||
if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 {
|
||||
if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id))
|
||||
}
|
||||
|
||||
@@ -602,13 +604,14 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
|
||||
|
||||
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
|
||||
MarketingProductId: marketingProduct.Id,
|
||||
Qty: 0,
|
||||
UnitPrice: 0,
|
||||
TotalWeight: 0,
|
||||
AvgWeight: 0,
|
||||
TotalPrice: 0,
|
||||
DeliveryDate: nil,
|
||||
VehicleNumber: rp.VehicleNumber,
|
||||
UsageQty: 0,
|
||||
PendingQty: 0,
|
||||
}
|
||||
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user