fix: all implemented fifo v2

This commit is contained in:
Hafizh A. Y
2026-03-02 12:44:20 +07:00
parent dd61b66af0
commit d5a1751868
11 changed files with 319 additions and 369 deletions
@@ -0,0 +1,87 @@
package service
import (
"context"
"fmt"
"strings"
"time"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
const (
transferLayingInFunctionCode = "TRANSFER_TO_LAYING_IN"
transferLayingStockableLane = "STOCKABLE"
transferLayingSourceTable = "laying_transfer_targets"
)
func reflowTransferLayingScope(
ctx context.Context,
fifoStockV2Svc commonSvc.FifoStockV2Service,
tx *gorm.DB,
productWarehouseID uint,
asOf *time.Time,
) error {
if fifoStockV2Svc == nil {
return fmt.Errorf("FIFO v2 service is not available")
}
if tx == nil {
return fmt.Errorf("transaction is required")
}
if productWarehouseID == 0 {
return fmt.Errorf("product warehouse id is required")
}
flagGroupCode, err := resolveTransferLayingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
if err != nil {
return err
}
if strings.TrimSpace(flagGroupCode) == "" {
return fmt.Errorf("flag group code is not found for product warehouse %d", productWarehouseID)
}
_, err = fifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode,
ProductWarehouseID: productWarehouseID,
AsOf: asOf,
Tx: tx,
})
return err
}
func resolveTransferLayingFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
type row struct {
FlagGroupCode string `gorm:"column:flag_group_code"`
}
var selected row
err := tx.WithContext(ctx).
Table("fifo_stock_v2_route_rules rr").
Select("rr.flag_group_code").
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("rr.is_active = TRUE").
Where("rr.lane = ?", transferLayingStockableLane).
Where("rr.function_code = ?", transferLayingInFunctionCode).
Where("rr.source_table = ?", transferLayingSourceTable).
Where(`
EXISTS (
SELECT 1
FROM product_warehouses pw
JOIN flags f ON f.flagable_id = pw.product_id
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE pw.id = ?
AND f.flagable_type = ?
AND fm.flag_group_code = rr.flag_group_code
)
`, productWarehouseID, entity.FlagableTypeProduct).
Order("rr.id ASC").
Limit(1).
Take(&selected).Error
if err != nil {
return "", err
}
return strings.TrimSpace(selected.FlagGroupCode), nil
}
@@ -19,7 +19,6 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"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"
@@ -51,7 +50,7 @@ type transferLayingService struct {
WarehouseRepo rWarehouse.WarehouseRepository
StockLogRepo rStockLogs.StockLogRepository
ApprovalService commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
}
func NewTransferLayingService(
@@ -64,7 +63,7 @@ func NewTransferLayingService(
productWarehouseRepo rInventory.ProductWarehouseRepository,
warehouseRepo rWarehouse.WarehouseRepository,
approvalService commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
fifoStockV2Svc commonSvc.FifoStockV2Service,
validate *validator.Validate,
) TransferLayingService {
return &transferLayingService{
@@ -80,7 +79,7 @@ func NewTransferLayingService(
WarehouseRepo: warehouseRepo,
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
ApprovalService: approvalService,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
}
}
@@ -744,7 +743,6 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
repoTx := s.Repository.WithTx(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction)
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction)
@@ -771,6 +769,9 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
}
if action == entity.ApprovalActionApproved {
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
if err != nil {
@@ -792,58 +793,70 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
totalSourceRequested += source.RequestedQty
}
sourceBeforeUsage := make(map[uint]float64, len(sources))
affectedPW := make(map[uint]struct{})
for _, source := range sources {
if source.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID))
}
sourceShare := (source.RequestedQty / totalSourceRequested) * totalTargetQty
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyTransferToLayingOut,
UsableID: source.Id,
ProductWarehouseID: *source.ProductWarehouseId,
Quantity: sourceShare,
AllowPending: false,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal consume FIFO stock: %v", err))
sourceShare := 0.0
if totalSourceRequested > 0 {
sourceShare = (source.RequestedQty / totalSourceRequested) * totalTargetQty
}
sourceBeforeUsage[source.Id] = source.UsageQty
if err := sourceRepoTx.PatchOne(c.Context(), source.Id, map[string]interface{}{
"usage_qty": source.UsageQty + consumeResult.UsageQuantity,
"pending_usage_qty": consumeResult.PendingQuantity,
"usage_qty": sourceShare,
"pending_usage_qty": 0,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
}
targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare)
affectedPW[*source.ProductWarehouseId] = struct{}{}
}
for i, target := range targets {
roundedQty := math.Round(targetShares[i])
if roundedQty <= 0 {
continue
}
mappingAllocation := &entity.StockAllocation{
StockableType: fifo.UsableKeyTransferToLayingOut.String(),
StockableId: source.Id,
UsableType: fifo.StockableKeyTransferToLayingIn.String(),
UsableId: target.Id,
ProductWarehouseId: *source.ProductWarehouseId,
Qty: roundedQty,
Status: entity.StockAllocationStatusActive,
}
if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target")
}
for _, target := range targets {
if target.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
}
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{
"total_qty": target.TotalQty,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
}
affectedPW[*target.ProductWarehouseId] = struct{}{}
}
for pwID := range affectedPW {
asOfCopy := transfer.TransferDate
if err := reflowTransferLayingScope(c.Context(), s.FifoStockV2Svc, dbTransaction, pwID, &asOfCopy); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow FIFO stock transfer laying (pw=%d): %v", pwID, err))
}
}
for _, source := range sources {
if source.ProductWarehouseId == nil {
continue
}
refreshedSource, err := sourceRepoTx.GetByID(c.Context(), source.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal refresh source transfer setelah reflow")
}
usageDelta := refreshedSource.UsageQty - sourceBeforeUsage[source.Id]
if usageDelta <= 0 {
continue
}
stockLogDecrease := &entity.StockLog{
ProductWarehouseId: *source.ProductWarehouseId,
CreatedBy: actorID,
Increase: 0,
Decrease: sourceShare,
Decrease: usageDelta,
LoggableType: string(utils.StockLogTypeTransferLaying),
LoggableId: approvableID,
Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber),
@@ -867,26 +880,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
for _, target := range targets {
if target.ProductWarehouseId == nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID))
}
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
_, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyTransferToLayingIn,
StockableID: target.Id,
ProductWarehouseID: *target.ProductWarehouseId,
Quantity: target.TotalQty,
Note: &note,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err))
}
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{
"total_qty": target.TotalQty,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
continue
}
stockLogIncrease := &entity.StockLog{