fix: first push need support testing, and implemented fifo v2 to all modules

This commit is contained in:
Hafizh A. Y
2026-02-27 19:09:01 +07:00
parent a2de21e351
commit 944604adad
21 changed files with 1105 additions and 810 deletions
@@ -21,7 +21,6 @@ import (
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
stockLogsRepo "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"
"gorm.io/gorm"
)
@@ -167,15 +166,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
transactionType := utils.ResolveAdjustmentTransactionType(routeMeta.FunctionCode)
allowPending := false
if routeMeta.Lane == adjustmentLaneUsable {
allowPending, err = s.resolveOverconsumePolicy(ctx, routeMeta)
if err != nil {
s.Log.Errorf("Failed to resolve overconsume rule: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO policy")
}
}
var createdAdjustmentStockId uint
var projectFlockKandangID *uint
@@ -228,6 +218,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
Price: req.Price,
GrandTotal: grandTotal,
}
switch routeMeta.Lane {
case adjustmentLaneStockable:
adjustmentStock.TotalQty = qty
case adjustmentLaneUsable:
adjustmentStock.UsageQty = qty
}
code, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
if err != nil {
return err
@@ -240,60 +236,32 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
var increaseQty float64
var decreaseQty float64
if routeMeta.Lane != adjustmentLaneStockable && routeMeta.Lane != adjustmentLaneUsable {
return fiber.NewError(fiber.StatusBadRequest, "Unsupported transaction subtype lane")
}
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
asOf := adjustmentStock.CreatedAt
if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
FlagGroupCode: routeMeta.FlagGroupCode,
ProductWarehouseID: productWarehouse.Id,
AsOf: &asOf,
Tx: tx,
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
}
refreshedAdjustment, err := adjustmentStockRepoTX.GetByID(ctx, adjustmentStock.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh adjustment stock")
}
switch routeMeta.Lane {
case adjustmentLaneStockable:
fifoNote := fmt.Sprintf("Stock Adjustment %s #%s", routeMeta.FunctionCode, adjustmentStock.AdjNumber)
result, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
StockableKey: fifo.StockableKeyAdjustmentIn,
StockableID: adjustmentStock.Id,
ProductWarehouseID: productWarehouse.Id,
Quantity: qty,
Note: &fifoNote,
Tx: tx,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
}
increaseQty = result.AddedQuantity
increaseQty = refreshedAdjustment.TotalQty
case adjustmentLaneUsable:
if s.FifoStockV2Svc != nil {
usableLegacyTypeKey := fifo.UsableKeyAdjustmentOut.String()
if routeMeta.SourceTable == "adjustment_stocks" && strings.TrimSpace(routeMeta.LegacyTypeKey) != "" {
usableLegacyTypeKey = strings.TrimSpace(routeMeta.LegacyTypeKey)
}
reflowResult, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
FlagGroupCode: routeMeta.FlagGroupCode,
ProductWarehouseID: productWarehouse.Id,
Usable: common.FifoStockV2Ref{
ID: adjustmentStock.Id,
LegacyTypeKey: usableLegacyTypeKey,
FunctionCode: routeMeta.FunctionCode,
},
DesiredQty: qty,
AllowOverConsume: &allowPending,
Tx: tx,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO v2: %v", err))
}
decreaseQty = reflowResult.Allocate.AllocatedQty
} else {
result, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
UsableKey: fifo.UsableKeyAdjustmentOut,
UsableID: adjustmentStock.Id,
ProductWarehouseID: productWarehouse.Id,
Quantity: qty,
AllowPending: allowPending,
Tx: tx,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
}
decreaseQty = result.UsageQuantity
}
default:
return fiber.NewError(fiber.StatusBadRequest, "Unsupported transaction subtype lane")
decreaseQty = refreshedAdjustment.UsageQty
}
stockLogs, err := stockLogRepoTX.GetByProductWarehouse(ctx, productWarehouse.Id, 1)
@@ -21,7 +21,6 @@ import (
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
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"
"gorm.io/gorm"
)
@@ -444,83 +443,79 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}
}
pakanProducts := map[uint]bool{}
if s.FifoStockV2Svc != nil && len(req.Products) > 0 {
pakanProducts, err = s.resolvePakanProducts(c.Context(), tx, req.Products)
if err != nil {
return err
}
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
flagGroupByProduct := make(map[uint]string, len(req.Products))
for _, product := range req.Products {
detail := detailMap[uint64(product.ProductID)]
if detail == nil || detail.SourceProductWarehouseID == nil || detail.DestProductWarehouseID == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Data transfer detail tidak valid")
}
outUsageQty := 0.0
outPendingQty := 0.0
useFifoV2 := s.FifoStockV2Svc != nil && pakanProducts[uint(product.ProductID)]
if useFifoV2 {
s.Log.Infof(
"[fifo-v2][transfer] use reflow movement=%s detail_id=%d product_id=%d source_pw=%d qty=%.3f",
entityTransfer.MovementNumber,
detail.Id,
product.ProductID,
*detail.SourceProductWarehouseID,
product.ProductQty,
)
reflowResult, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: "PAKAN",
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Usable: commonSvc.FifoStockV2Ref{
ID: uint(detail.Id),
LegacyTypeKey: fifo.UsableKeyStockTransferOut.String(),
FunctionCode: "STOCK_TRANSFER_OUT",
},
DesiredQty: product.ProductQty,
Tx: tx,
})
flagGroupCode, ok := flagGroupByProduct[uint(product.ProductID)]
if !ok {
flagGroupCode, err = s.resolveTransferFlagGroup(c.Context(), tx, uint(product.ProductID))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("FIFO v2 route tidak ditemukan untuk produk %d: %v", product.ProductID, err))
}
outUsageQty = reflowResult.Allocate.AllocatedQty
outPendingQty = reflowResult.Allocate.PendingQty
s.Log.Infof(
"[fifo-v2][transfer] reflow result movement=%s detail_id=%d usage=%.3f pending=%.3f",
entityTransfer.MovementNumber,
detail.Id,
outUsageQty,
outPendingQty,
)
} else {
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyStockTransferOut,
UsableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Quantity: product.ProductQty,
AllowPending: false,
Tx: tx,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
}
outUsageQty = consumeResult.UsageQuantity
outPendingQty = consumeResult.PendingQuantity
flagGroupByProduct[uint(product.ProductID)] = flagGroupCode
}
if err := tx.Model(&entity.StockTransferDetail{}).
Where("id = ?", detail.Id).
Updates(map[string]interface{}{
"usage_qty": outUsageQty,
"pending_qty": outPendingQty,
"usage_qty": product.ProductQty,
"pending_qty": 0,
"total_qty": product.ProductQty,
}).Error; err != nil {
s.Log.Errorf("Failed to update tracking usage for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
s.Log.Errorf("Failed to update transfer detail seed fields for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
}
asOf := transferDate
if _, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode,
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
AsOf: &asOf,
Tx: tx,
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
}
if _, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode,
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
AsOf: &asOf,
Tx: tx,
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow stok tujuan untuk produk %d. Error: %v", product.ProductID, err))
}
type usageSnapshot struct {
UsageQty float64 `gorm:"column:usage_qty"`
PendingQty float64 `gorm:"column:pending_qty"`
}
var usage usageSnapshot
if err := tx.WithContext(c.Context()).
Table("stock_transfer_details").
Select("usage_qty, pending_qty").
Where("id = ?", detail.Id).
Take(&usage).Error; err != nil {
s.Log.Errorf("Failed to read transfer usage snapshot detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data tracking")
}
outUsageQty := usage.UsageQty
outPendingQty := usage.PendingQty
if outPendingQty > 1e-6 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal", product.ProductID))
}
stockLogDecrease := &entity.StockLog{
ProductWarehouseId: uint(*detail.SourceProductWarehouseID),
CreatedBy: uint(actorID),
Increase: 0,
Decrease: product.ProductQty,
Decrease: outUsageQty,
LoggableType: string(utils.StockLogTypeTransfer),
LoggableId: uint(detail.Id),
Notes: "",
@@ -541,45 +536,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
}
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
inAddedQty := 0.0
if useFifoV2 {
s.Log.Infof(
"[fifo-v2][transfer] stock-in uses replenish path movement=%s detail_id=%d product_id=%d dest_pw=%d qty=%.3f",
entityTransfer.MovementNumber,
detail.Id,
product.ProductID,
*detail.DestProductWarehouseID,
product.ProductQty,
)
}
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyStockTransferIn,
StockableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
Quantity: product.ProductQty,
Note: &note,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to replenish stock for product_id=%d, pw_id=%d, qty=%.2f: %+v", product.ProductID, *detail.DestProductWarehouseID, product.ProductQty, err)
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menambah stok gudang tujuan")
}
inAddedQty = replenishResult.AddedQuantity
if err := tx.Model(&entity.StockTransferDetail{}).
Where("id = ?", detail.Id).
Updates(map[string]interface{}{
"total_qty": inAddedQty,
}).Error; err != nil {
s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
}
inAddedQty := outUsageQty
stockLogIncrease := &entity.StockLog{
ProductWarehouseId: uint(*detail.DestProductWarehouseID),
CreatedBy: uint(actorID),
Increase: product.ProductQty,
Increase: inAddedQty,
Decrease: 0,
LoggableType: string(utils.StockLogTypeTransfer),
LoggableId: uint(detail.Id),
@@ -657,51 +619,45 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return result, nil
}
func (s *transferService) resolvePakanProducts(
func (s *transferService) resolveTransferFlagGroup(
ctx context.Context,
tx *gorm.DB,
products []validation.TransferProduct,
) (map[uint]bool, error) {
out := make(map[uint]bool, len(products))
if len(products) == 0 {
return out, nil
}
productIDs := make([]uint, 0, len(products))
seen := make(map[uint]struct{}, len(products))
for _, product := range products {
if product.ProductID == 0 {
continue
}
if _, ok := seen[product.ProductID]; ok {
continue
}
seen[product.ProductID] = struct{}{}
productIDs = append(productIDs, product.ProductID)
}
if len(productIDs) == 0 {
return out, nil
productID uint,
) (string, error) {
if productID == 0 {
return "", fmt.Errorf("product id is required")
}
type row struct {
ProductID uint `gorm:"column:product_id"`
FlagGroupCode string `gorm:"column:flag_group_code"`
}
var rows []row
var selected row
err := tx.WithContext(ctx).
Table("flags f").
Select("DISTINCT f.flagable_id AS product_id").
Where("f.flagable_type = ?", entity.FlagableTypeProduct).
Where("f.name IN ?", []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}).
Where("f.flagable_id IN ?", productIDs).
Scan(&rows).Error
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 = ?", "USABLE").
Where("rr.function_code = ?", "STOCK_TRANSFER_OUT").
Where("rr.source_table = ?", "stock_transfer_details").
Where(`
EXISTS (
SELECT 1
FROM flags f
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE f.flagable_type = ?
AND f.flagable_id = ?
AND fm.flag_group_code = rr.flag_group_code
)
`, entity.FlagableTypeProduct, productID).
Order("rr.id ASC").
Limit(1).
Take(&selected).Error
if err != nil {
return nil, err
return "", err
}
for _, row := range rows {
out[row.ProductID] = true
}
return out, nil
return strings.TrimSpace(selected.FlagGroupCode), nil
}
func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
+3 -22
View File
@@ -2,7 +2,6 @@ package marketing
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -20,7 +19,6 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
)
type MarketingModule struct{}
@@ -35,24 +33,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockLogRepo := rShared.NewStockLogRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyMarketingDelivery,
Table: "marketing_delivery_products",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register marketing delivery usable workflow: %v", err))
}
}
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
@@ -64,8 +45,8 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate)
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoStockV2Service, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoStockV2Service, validate)
userService := sUser.NewUserService(userRepo, validate)
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
@@ -15,7 +15,6 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
rShared "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"
@@ -36,7 +35,7 @@ type deliveryOrdersService struct {
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
StockLogRepo rShared.StockLogRepository
ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
}
func NewDeliveryOrdersService(
@@ -45,7 +44,7 @@ func NewDeliveryOrdersService(
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
stockLogRepo rShared.StockLogRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
fifoStockV2Svc commonSvc.FifoStockV2Service,
validate *validator.Validate,
) DeliveryOrdersService {
return &deliveryOrdersService{
@@ -55,7 +54,7 @@ func NewDeliveryOrdersService(
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
StockLogRepo: stockLogRepo,
ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
}
}
@@ -549,33 +548,42 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
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,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
previousUsage := deliveryProduct.UsageQty
deliveryProduct.UsageQty = requestedQty
deliveryProduct.PendingQty = 0
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil {
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
}
if actorID > 0 && result.UsageQuantity > 0 {
if err := reflowMarketingScope(
ctx,
s.FifoStockV2Svc,
tx,
marketingProduct.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
}
deliveryProduct.UsageQty = refreshed.UsageQty
deliveryProduct.PendingQty = refreshed.PendingQty
deliveryProduct.CreatedAt = refreshed.CreatedAt
allocatedDelta := deliveryProduct.UsageQty - previousUsage
if actorID > 0 && allocatedDelta > 0 {
decreaseLog := &entity.StockLog{
Decrease: result.UsageQuantity,
Decrease: allocatedDelta,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity),
Notes: fmt.Sprintf("FIFO v2 reflow consume (%.2f)", allocatedDelta),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
@@ -604,35 +612,45 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
}
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
if err != nil {
currentUsage = 0
}
if currentUsage == 0 {
currentUsage := deliveryProduct.UsageQty
currentPending := deliveryProduct.PendingQty
if currentUsage <= 0 && currentPending <= 0 {
return nil
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: tx,
}); err != nil {
return err
deliveryProduct.UsageQty = 0
deliveryProduct.PendingQty = 0
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset delivery product")
}
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
return err
if err := reflowMarketingScope(
ctx,
s.FifoStockV2Svc,
tx,
marketingProduct.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
if actorID > 0 && currentUsage > 0 {
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
}
deliveryProduct.UsageQty = refreshed.UsageQty
deliveryProduct.PendingQty = refreshed.PendingQty
deliveryProduct.CreatedAt = refreshed.CreatedAt
releasedUsage := currentUsage - deliveryProduct.UsageQty
if actorID > 0 && releasedUsage > 0 {
increaseLog := &entity.StockLog{
Increase: currentUsage,
Increase: releasedUsage,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage),
Notes: fmt.Sprintf("FIFO v2 reflow release (%.2f)", releasedUsage),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
if err != nil {
@@ -0,0 +1,97 @@
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 (
marketingOutFunctionCode = "MARKETING_OUT"
marketingUsableLane = "USABLE"
marketingSourceTable = "marketing_delivery_products"
)
func reflowMarketingScope(
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 productWarehouseID == 0 {
return fmt.Errorf("product warehouse id is required")
}
flagGroupCode, err := resolveMarketingFlagGroupByProductWarehouse(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 resolveMarketingFlagGroupByProductWarehouse(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 = ?", marketingUsableLane).
Where("rr.function_code = ?", marketingOutFunctionCode).
Where("rr.source_table = ?", marketingSourceTable).
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
}
func resolveMarketingAsOf(deliveryDate, createdAt *time.Time) *time.Time {
if deliveryDate != nil {
asOf := *deliveryDate
return &asOf
}
if createdAt != nil {
asOf := *createdAt
return &asOf
}
asOf := time.Now()
return &asOf
}
@@ -20,7 +20,6 @@ import (
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -43,12 +42,12 @@ type salesOrdersService struct {
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
UserRepo userRepo.UserRepository
ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
WarehouseRepo warehouseRepo.WarehouseRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
}
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository,
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, warehouseRepo warehouseRepo.WarehouseRepository,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
return &salesOrdersService{
Log: utils.Log,
@@ -58,7 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome
ProductWarehouseRepo: productWarehouseRepo,
UserRepo: userRepo,
ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
WarehouseRepo: warehouseRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
}
@@ -376,15 +375,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if qtyDiff < 0 {
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
} else if qtyDiff > 0 {
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
ProductWarehouseID: rp.ProductWarehouseId,
Quantity: qtyDiff,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err))
nextRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty + qtyDiff
if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, nextRequestedQty, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing delivery fifo fields")
}
if err := reflowMarketingScope(
c.Context(),
s.FifoStockV2Svc,
dbTransaction,
rp.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
}
}
}
@@ -439,12 +441,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id))
}
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err))
if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, 0, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset marketing delivery fifo fields")
}
if err := reflowMarketingScope(
c.Context(),
s.FifoStockV2Svc,
dbTransaction,
deliveryProduct.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
}
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
@@ -523,12 +530,17 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
if err == nil && len(deliveryProducts) > 0 {
for _, dp := range deliveryProducts {
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: dp.Id,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err))
if err := marketingDeliveryProductRepoTx.UpdateFifoFields(c.Context(), dp.Id, 0, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset fifo fields for delivery product %d", dp.Id))
}
if err := reflowMarketingScope(
c.Context(),
s.FifoStockV2Svc,
dbTransaction,
dp.ProductWarehouseId,
resolveMarketingAsOf(dp.DeliveryDate, dp.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2 for delivery product %d: %v", dp.Id, err))
}
}
}
+2 -40
View File
@@ -2,7 +2,6 @@ package chickins
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -10,7 +9,6 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -40,45 +38,9 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
productRepo := rProduct.NewProductRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
userRepo := rUser.NewUserRepository(db)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyProjectChickin,
Table: "project_chickins",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_usage_qty",
CreatedAt: "created_at",
},
ExcludedStockables: []fifo.StockableKey{fifo.StockableKeyProjectFlockPopulation},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register chickin usable workflow: %v", err))
}
}
if err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyProjectFlockPopulation,
Table: "project_flock_populations",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used_qty",
CreatedAt: "created_at",
},
OrderBy: []string{"created_at ASC", "id ASC"},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register project flock population stockable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
@@ -96,7 +58,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
projectflockpopulationrepo,
chickinDetailRepo,
validate,
fifoService)
fifoStockV2Service)
userService := sUser.NewUserService(userRepo, validate)
ChickinRoutes(router, userService, chickinService)
@@ -19,7 +19,6 @@ import (
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
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"
@@ -27,8 +26,6 @@ import (
"gorm.io/gorm"
)
var chickinUsableKey = fifo.UsableKeyProjectChickin
type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
@@ -51,11 +48,11 @@ type chickinService struct {
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
StockLogRepo rStockLogs.StockLogRepository
}
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoStockV2Svc commonSvc.FifoStockV2Service) ChickinService {
return &chickinService{
Log: utils.Log,
Validate: validate,
@@ -68,7 +65,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
ProjectflockKandangRepo: projectflockkandangRepo,
ProjectflockPopulationRepo: projectflockpopulationRepo,
ProjectChickinDetailRepo: projectChickinDetailRepo,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
}
}
@@ -372,18 +369,9 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
}
if chickin.UsageQty > 0 {
currentUsageQty := chickin.UsageQty
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
return err
}
warehouseDeltas := make(map[uint]float64)
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
return err
}
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
@@ -549,12 +537,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err))
}
warehouseDeltas := make(map[uint]float64)
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
return err
}
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id))
@@ -617,36 +599,48 @@ func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB,
}
func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, desiredQty float64, actorID uint) error {
if chickin == nil || s.FifoSvc == nil {
if chickin == nil {
return nil
}
if tx == nil {
return errors.New("transaction is required")
}
if s.FifoStockV2Svc == nil {
return errors.New("fifo v2 service is not available")
}
if desiredQty < 0 {
return errors.New("desired quantity must be zero or greater")
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: chickinUsableKey,
UsableID: chickin.Id,
ProductWarehouseID: chickin.ProductWarehouseId,
Quantity: desiredQty,
AllowPending: true,
Tx: tx,
})
if err != nil {
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0); err != nil {
return err
}
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
asOf := chickin.ChickInDate
if asOf.IsZero() {
asOf = chickin.CreatedAt
}
if err := reflowChickinScope(ctx, s.FifoStockV2Svc, tx, chickin.ProductWarehouseId, &asOf); err != nil {
return err
}
if result.UsageQuantity > 0 {
var refreshed entity.ProjectChickin
if err := tx.WithContext(ctx).
Where("id = ?", chickin.Id).
Take(&refreshed).Error; err != nil {
return err
}
if refreshed.UsageQty > 0 {
decreaseLog := &entity.StockLog{
Decrease: result.UsageQuantity,
Decrease: refreshed.UsageQty,
LoggableType: string(utils.StockLogTypeChikin),
LoggableId: chickin.Id,
ProductWarehouseId: chickin.ProductWarehouseId,
LoggableId: refreshed.Id,
ProductWarehouseId: refreshed.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
Notes: fmt.Sprintf("Chickin #%d", refreshed.Id),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, chickin.ProductWarehouseId, 1)
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
@@ -658,46 +652,52 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
decreaseLog.Stock -= decreaseLog.Decrease
}
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil); err != nil {
return err
}
}
return nil
}
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error {
if chickin == nil || targetPW == nil || population == nil || s.FifoSvc == nil {
if chickin == nil || targetPW == nil || population == nil {
return nil
}
if tx == nil {
return errors.New("transaction is required")
}
if s.FifoStockV2Svc == nil {
return errors.New("fifo v2 service is not available")
}
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyProjectFlockPopulation,
StockableID: population.Id,
ProductWarehouseID: targetPW.Id,
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
if err := tx.WithContext(ctx).
Model(&entity.ProjectFlockPopulation{}).
Where("id = ?", population.Id).
Update("total_qty", chickin.UsageQty).Error; err != nil {
return err
}
return nil
asOf := chickin.ChickInDate
if asOf.IsZero() {
asOf = chickin.CreatedAt
}
return reflowChickinScope(ctx, s.FifoStockV2Svc, tx, targetPW.Id, &asOf)
}
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
if chickin == nil || s.FifoSvc == nil {
if chickin == nil {
return nil
}
if tx == nil {
return errors.New("transaction is required")
}
if s.FifoStockV2Svc == nil {
return errors.New("fifo v2 service is not available")
}
var currentUsage float64
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(&currentUsage).Error; err != nil {
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: chickinUsableKey,
UsableID: chickin.Id,
Tx: tx,
}); err != nil {
return err
}
@@ -705,6 +705,14 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
return err
}
asOf := chickin.ChickInDate
if asOf.IsZero() {
asOf = chickin.CreatedAt
}
if err := reflowChickinScope(ctx, s.FifoStockV2Svc, tx, chickin.ProductWarehouseId, &asOf); err != nil {
return err
}
if currentUsage > 0 {
increaseLog := &entity.StockLog{
Increase: currentUsage,
@@ -726,7 +734,9 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
increaseLog.Stock += increaseLog.Increase
}
s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil); err != nil {
return err
}
}
return nil
@@ -755,10 +765,3 @@ func (s chickinService) EnsureChickInExists(ctx context.Context, projectFlockKan
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
}
func (s *chickinService) adjustProductWarehouseQuantities(ctx context.Context, tx *gorm.DB, deltas map[uint]float64) error {
if len(deltas) == 0 {
return nil
}
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
}
@@ -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 (
chickinOutFunctionCode = "CHICKIN_OUT"
chickinUsableLane = "USABLE"
chickinSourceTable = "project_chickins"
)
func reflowChickinScope(
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 := resolveChickinFlagGroupByProductWarehouse(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 resolveChickinFlagGroupByProductWarehouse(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 = ?", chickinUsableLane).
Where("rr.function_code = ?", chickinOutFunctionCode).
Where("rr.source_table = ?", chickinSourceTable).
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
}
@@ -2,7 +2,6 @@ package recordings
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -26,7 +25,6 @@ import (
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
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"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -48,7 +46,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
productRepo := rProduct.NewProductRepository(db)
chickinRepo := rChickin.NewChickinRepository(db)
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
stockLogRepo := rStockLogs.NewStockLogRepository(db)
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
@@ -61,76 +58,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
validate,
)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
if err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyRecordingEgg,
Table: "recording_eggs",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used",
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_eggs.recording_id)",
},
OrderBy: []string{"(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_eggs.recording_id) ASC", "id ASC"},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording egg stockable workflow: %v", err))
}
}
if err := fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyRecordingDepletion,
Table: "recording_depletions",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
TotalQuantity: "qty",
TotalUsedQuantity: "total_used_qty",
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id)",
},
OrderBy: []string{"(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id) ASC", "id ASC"},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording depletion stockable workflow: %v", err))
}
}
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyRecordingStock,
Table: "recording_stocks",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_stocks.recording_id)",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
}
}
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyRecordingDepletion,
Table: "recording_depletions",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "source_product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id)",
},
ExcludedStockables: []fifo.StockableKey{
fifo.StockableKeyTransferToLayingIn,
fifo.StockableKeyStockTransferIn,
fifo.StockableKeyAdjustmentIn,
fifo.StockableKeyPurchaseItems,
fifo.StockableKeyRecordingEgg,
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording depletion usable workflow: %v", err))
}
}
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
@@ -169,7 +97,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo,
chickinDetailRepo,
validate,
fifoService,
fifoStockV2Service,
)
recordingService := sRecording.NewRecordingService(
@@ -179,7 +107,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo,
approvalRepo,
approvalService,
fifoService,
fifoStockV2Service,
stockLogRepo,
productionStandardService,
projectFlockService,
@@ -0,0 +1,137 @@
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 (
recordingLaneUsable = "USABLE"
recordingLaneStockable = "STOCKABLE"
recordingFunctionStockOut = "RECORDING_STOCK_OUT"
recordingFunctionDepletionOut = "RECORDING_DEPLETION_OUT"
recordingFunctionDepletionIn = "RECORDING_DEPLETION_IN"
recordingFunctionEggIn = "RECORDING_EGG_IN"
recordingSourceStocks = "recording_stocks"
recordingSourceDepletions = "recording_depletions"
recordingSourceEggs = "recording_eggs"
)
func (s *recordingService) reflowRecordingScope(
ctx context.Context,
tx *gorm.DB,
productWarehouseID uint,
recordingID uint,
lane string,
functionCode string,
sourceTable string,
) error {
if s == nil || s.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 := resolveRecordingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID, lane, functionCode, sourceTable)
if err != nil {
return err
}
if strings.TrimSpace(flagGroupCode) == "" {
return fmt.Errorf("flag group code is not found for product warehouse %d", productWarehouseID)
}
asOf, err := resolveRecordingAsOf(ctx, tx, recordingID)
if err != nil {
return err
}
_, err = s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode,
ProductWarehouseID: productWarehouseID,
AsOf: asOf,
Tx: tx,
})
return err
}
func resolveRecordingFlagGroupByProductWarehouse(
ctx context.Context,
tx *gorm.DB,
productWarehouseID uint,
lane string,
functionCode string,
sourceTable string,
) (string, error) {
type row struct {
FlagGroupCode string `gorm:"column:flag_group_code"`
}
var selected row
q := 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 = ?", lane).
Where("rr.source_table = ?", sourceTable)
if strings.TrimSpace(functionCode) != "" {
q = q.Where("rr.function_code = ?", functionCode)
}
err := q.
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
}
func resolveRecordingAsOf(ctx context.Context, tx *gorm.DB, recordingID uint) (*time.Time, error) {
if recordingID == 0 {
asOf := time.Now().UTC()
return &asOf, nil
}
type row struct {
RecordDatetime time.Time `gorm:"column:record_datetime"`
}
var selected row
if err := tx.WithContext(ctx).
Table("recordings").
Select("record_datetime").
Where("id = ?", recordingID).
Limit(1).
Take(&selected).Error; err != nil {
return nil, err
}
asOf := selected.RecordDatetime.UTC()
return &asOf, nil
}
@@ -52,7 +52,7 @@ type recordingService struct {
ProductionStandardSvc sProductionStandard.ProductionStandardService
ProjectFlockSvc sProjectFlock.ProjectflockService
ChickinSvc sChickin.ChickinService
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
StockLogRepo rStockLogs.StockLogRepository
}
@@ -63,7 +63,7 @@ func NewRecordingService(
projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
fifoStockV2Svc commonSvc.FifoStockV2Service,
stockLogRepo rStockLogs.StockLogRepository,
productionStandardSvc sProductionStandard.ProductionStandardService,
projectFlockSvc sProjectFlock.ProjectflockService,
@@ -82,7 +82,7 @@ func NewRecordingService(
ProductionStandardSvc: productionStandardSvc,
ProjectFlockSvc: projectFlockSvc,
ChickinSvc: chickinSvc,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
StockLogRepo: stockLogRepo,
}
}
@@ -8,7 +8,6 @@ import (
"strings"
"time"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -18,9 +17,6 @@ import (
"gorm.io/gorm"
)
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
const depletionUsageTolerance = 0.000001
func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) {
@@ -101,9 +97,9 @@ func (s *recordingService) consumeRecordingStocks(
if len(stocks) == 0 {
return nil
}
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for consuming recording stocks")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for consuming recording stocks")
return errors.New("fifo v2 service is not available")
}
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available")
@@ -125,38 +121,52 @@ func (s *recordingService) consumeRecordingStocks(
}
desiredTotal := desired + pending
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
ProductWarehouseID: stock.ProductWarehouseId,
Quantity: desiredTotal,
AllowPending: true,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for recording stock %d: %+v", stock.Id, err)
if err := s.Repository.UpdateStockUsage(tx, stock.Id, desiredTotal, 0); err != nil {
return err
}
if err := s.reflowRecordingScope(
ctx,
tx,
stock.ProductWarehouseId,
stock.RecordingId,
recordingLaneUsable,
recordingFunctionStockOut,
recordingSourceStocks,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording stock %d: %+v", stock.Id, err)
return err
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
var refreshed entity.RecordingStock
if err := tx.WithContext(ctx).
Where("id = ?", stock.Id).
Take(&refreshed).Error; err != nil {
return err
}
s.logStockTrace("consume:done", stock, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desiredTotal, result.UsageQuantity, result.PendingQuantity))
actualUsage := 0.0
actualPending := 0.0
if refreshed.UsageQty != nil {
actualUsage = *refreshed.UsageQty
}
if refreshed.PendingQty != nil {
actualPending = *refreshed.PendingQty
}
s.logStockTrace("consume:done", refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desiredTotal, actualUsage, actualPending))
logDecrease := result.UsageQuantity
if result.PendingQuantity > 0 {
logDecrease += result.PendingQuantity
logDecrease := actualUsage
if actualPending > 0 {
logDecrease += actualPending
}
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
log := &entity.StockLog{
ProductWarehouseId: stock.ProductWarehouseId,
ProductWarehouseId: refreshed.ProductWarehouseId,
CreatedBy: actorID,
Decrease: logDecrease,
LoggableType: string(utils.StockLogTypeRecording),
LoggableId: stock.RecordingId,
LoggableId: refreshed.RecordingId,
Notes: note,
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1)
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
@@ -187,9 +197,9 @@ func (s *recordingService) consumeRecordingDepletions(
if len(depletions) == 0 {
return nil
}
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for consuming recording depletions")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for consuming recording depletions")
return errors.New("fifo v2 service is not available")
}
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available")
@@ -210,27 +220,40 @@ func (s *recordingService) consumeRecordingDepletions(
}
desired := depletion.Qty + depletion.PendingQty
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id,
ProductWarehouseID: sourceWarehouseID,
Quantity: desired,
AllowPending: false,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err)
if err := tx.WithContext(ctx).
Model(&entity.RecordingDepletion{}).
Where("id = ?", depletion.Id).
Updates(map[string]any{
"qty": desired,
"usage_qty": desired,
"pending_qty": 0,
}).Error; err != nil {
return err
}
if err := s.reflowRecordingScope(
ctx,
tx,
sourceWarehouseID,
depletion.RecordingId,
recordingLaneUsable,
recordingFunctionDepletionOut,
recordingSourceDepletions,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording depletion %d: %+v", depletion.Id, err)
return err
}
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil {
var refreshed entity.RecordingDepletion
if err := tx.WithContext(ctx).
Where("id = ?", depletion.Id).
Take(&refreshed).Error; err != nil {
return err
}
s.logDepletionTrace("consume:done", depletion, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desired, result.UsageQuantity, result.PendingQuantity))
s.logDepletionTrace("consume:done", refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desired, refreshed.UsageQty, refreshed.PendingQty))
logDecrease := result.UsageQuantity
if result.PendingQuantity > 0 {
logDecrease += result.PendingQuantity
logDecrease := refreshed.UsageQty
if refreshed.PendingQty > 0 {
logDecrease += refreshed.PendingQty
}
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
log := &entity.StockLog{
@@ -238,7 +261,7 @@ func (s *recordingService) consumeRecordingDepletions(
CreatedBy: actorID,
Decrease: logDecrease,
LoggableType: string(utils.StockLogTypeRecording),
LoggableId: depletion.RecordingId,
LoggableId: refreshed.RecordingId,
Notes: note,
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1)
@@ -258,20 +281,20 @@ func (s *recordingService) consumeRecordingDepletions(
}
}
destDelta := depletion.Qty + depletion.PendingQty
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
if depletion.ProductWarehouseId == sourceWarehouseID {
destDelta := refreshed.Qty + refreshed.PendingQty
if refreshed.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
if refreshed.ProductWarehouseId == sourceWarehouseID {
continue
}
log := &entity.StockLog{
ProductWarehouseId: depletion.ProductWarehouseId,
ProductWarehouseId: refreshed.ProductWarehouseId,
CreatedBy: actorID,
Increase: destDelta,
LoggableType: string(utils.StockLogTypeRecording),
LoggableId: depletion.RecordingId,
LoggableId: refreshed.RecordingId,
Notes: note,
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1)
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
}
@@ -302,9 +325,9 @@ func (s *recordingService) releaseRecordingStocks(
if len(stocks) == 0 {
return nil
}
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for releasing recording stocks")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for releasing recording stocks")
return errors.New("fifo v2 service is not available")
}
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available")
@@ -314,45 +337,35 @@ func (s *recordingService) releaseRecordingStocks(
if stock.Id == 0 {
continue
}
if stock.UsageQty != nil && *stock.UsageQty > 0 {
activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id)
if err != nil {
return err
}
if activeCount == 0 {
s.Log.Warnf("recording-stock release: no active allocations, forcing usage/pending to 0 (stock_id=%d)", stock.Id)
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
return err
}
continue
}
if err := s.resyncStockableUsageFromAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id); err != nil {
return err
}
if err := s.ensureActiveAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id); err != nil {
return err
}
currentUsage := 0.0
if stock.UsageQty != nil {
currentUsage = *stock.UsageQty
}
s.logStockTrace("release:start", stock, "")
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for recording stock %d: %+v", stock.Id, err)
return err
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
return err
}
if err := s.reflowRecordingScope(
ctx,
tx,
stock.ProductWarehouseId,
stock.RecordingId,
recordingLaneUsable,
recordingFunctionStockOut,
recordingSourceStocks,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 release for recording stock %d: %+v", stock.Id, err)
return err
}
s.logStockTrace("release:done", stock, "")
if stock.UsageQty != nil && *stock.UsageQty > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
if currentUsage > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
log := &entity.StockLog{
ProductWarehouseId: stock.ProductWarehouseId,
CreatedBy: actorID,
Increase: *stock.UsageQty,
Increase: currentUsage,
LoggableType: string(utils.StockLogTypeRecording),
LoggableId: stock.RecordingId,
Notes: note,
@@ -388,9 +401,9 @@ func (s *recordingService) releaseRecordingDepletions(
if len(depletions) == 0 {
return nil
}
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for releasing recording depletions")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for releasing recording depletions")
return errors.New("fifo v2 service is not available")
}
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available")
@@ -400,36 +413,7 @@ func (s *recordingService) releaseRecordingDepletions(
if depletion.Id == 0 {
continue
}
if depletion.UsageQty > 0 {
activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id)
if err != nil {
return err
}
if activeCount == 0 {
s.Log.Warnf("recording-depletion release: no active allocations, forcing usage/pending to 0 (depletion_id=%d)", depletion.Id)
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil {
return err
}
if err := tx.WithContext(ctx).
Table("recording_depletions").
Where("id = ?", depletion.Id).
Update("usage_qty", 0).Error; err != nil {
return err
}
continue
}
if err := s.resyncStockableUsageFromAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id); err != nil {
return err
}
if err := s.ensureActiveAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id); err != nil {
return err
}
}
s.logDepletionTrace("release:start", depletion, "")
if err := validateDepletionUsage(depletion); err != nil {
s.Log.Errorf("FIFO depletion mismatch for recording %d (depletion %d): qty=%.3f usage=%.3f pending=%.3f", depletion.RecordingId, depletion.Id, depletion.Qty, depletion.UsageQty, depletion.PendingQty)
return err
}
sourceWarehouseID := uint(0)
if depletion.SourceProductWarehouseId != nil {
@@ -438,24 +422,49 @@ func (s *recordingService) releaseRecordingDepletions(
if sourceWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingDepletionUsableKey,
UsableID: depletion.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err)
logIncrease := depletion.Qty + depletion.PendingQty
destDelta := depletion.Qty + depletion.PendingQty
if err := tx.WithContext(ctx).
Model(&entity.RecordingDepletion{}).
Where("id = ?", depletion.Id).
Updates(map[string]any{
"qty": 0,
"usage_qty": 0,
"pending_qty": 0,
}).Error; err != nil {
return err
}
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil {
if err := s.reflowRecordingScope(
ctx,
tx,
sourceWarehouseID,
depletion.RecordingId,
recordingLaneUsable,
recordingFunctionDepletionOut,
recordingSourceDepletions,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 source release for recording depletion %d: %+v", depletion.Id, err)
return err
}
if depletion.ProductWarehouseId != 0 {
if err := s.reflowRecordingScope(
ctx,
tx,
depletion.ProductWarehouseId,
depletion.RecordingId,
recordingLaneStockable,
recordingFunctionDepletionIn,
recordingSourceDepletions,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 destination release for recording depletion %d: %+v", depletion.Id, err)
return err
}
}
s.logDepletionTrace("release:done", depletion, "")
logIncrease := depletion.Qty
if depletion.PendingQty > 0 {
logIncrease += depletion.PendingQty
}
if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
log := &entity.StockLog{
ProductWarehouseId: sourceWarehouseID,
@@ -482,7 +491,6 @@ func (s *recordingService) releaseRecordingDepletions(
}
}
destDelta := depletion.Qty + depletion.PendingQty
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
if depletion.ProductWarehouseId == sourceWarehouseID {
continue
@@ -618,9 +626,9 @@ func (s *recordingService) replenishRecordingEggs(
if len(eggs) == 0 {
return nil
}
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for replenishing recording eggs")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for replenishing recording eggs")
return errors.New("fifo v2 service is not available")
}
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available")
@@ -631,14 +639,23 @@ func (s *recordingService) replenishRecordingEggs(
continue
}
s.logEggTrace("replenish:start", egg, "")
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyRecordingEgg,
StockableID: egg.Id,
ProductWarehouseID: egg.ProductWarehouseId,
Quantity: float64(egg.Qty),
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to replenish FIFO stock for recording egg %d: %+v", egg.Id, err)
if err := tx.WithContext(ctx).
Model(&entity.RecordingEgg{}).
Where("id = ?", egg.Id).
Update("total_qty", float64(egg.Qty)).Error; err != nil {
return err
}
if err := s.reflowRecordingScope(
ctx,
tx,
egg.ProductWarehouseId,
egg.RecordingId,
recordingLaneStockable,
recordingFunctionEggIn,
recordingSourceEggs,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording egg %d: %+v", egg.Id, err)
return err
}
s.logEggTrace("replenish:done", egg, "")
@@ -681,9 +698,9 @@ func (s *recordingService) replenishRecordingDepletions(
if len(depletions) == 0 {
return nil
}
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for replenishing recording depletions")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for replenishing recording depletions")
return errors.New("fifo v2 service is not available")
}
for _, depletion := range depletions {
@@ -691,14 +708,16 @@ func (s *recordingService) replenishRecordingDepletions(
continue
}
s.logDepletionTrace("replenish:start", depletion, "")
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyRecordingDepletion,
StockableID: depletion.Id,
ProductWarehouseID: depletion.ProductWarehouseId,
Quantity: depletion.Qty,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to replenish FIFO stock for recording depletion %d: %+v", depletion.Id, err)
if err := s.reflowRecordingScope(
ctx,
tx,
depletion.ProductWarehouseId,
depletion.RecordingId,
recordingLaneStockable,
recordingFunctionDepletionIn,
recordingSourceDepletions,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording depletion %d: %+v", depletion.Id, err)
return err
}
s.logDepletionTrace("replenish:done", depletion, "")
@@ -715,9 +734,9 @@ func (s *recordingService) reduceRecordingDepletions(
if len(depletions) == 0 {
return nil
}
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for reducing recording depletions")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for reducing recording depletions")
return errors.New("fifo v2 service is not available")
}
for _, depletion := range depletions {
@@ -725,16 +744,44 @@ func (s *recordingService) reduceRecordingDepletions(
continue
}
s.logDepletionTrace("reduce:start", depletion, "")
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: fifo.StockableKeyRecordingDepletion,
StockableID: depletion.Id,
ProductWarehouseID: depletion.ProductWarehouseId,
Quantity: -depletion.Qty,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to reduce FIFO stock for recording depletion %d: %+v", depletion.Id, err)
if err := tx.WithContext(ctx).
Model(&entity.RecordingDepletion{}).
Where("id = ?", depletion.Id).
Updates(map[string]any{
"qty": 0,
"usage_qty": 0,
"pending_qty": 0,
}).Error; err != nil {
return err
}
if depletion.SourceProductWarehouseId != nil && *depletion.SourceProductWarehouseId != 0 {
if err := s.reflowRecordingScope(
ctx,
tx,
*depletion.SourceProductWarehouseId,
depletion.RecordingId,
recordingLaneUsable,
recordingFunctionDepletionOut,
recordingSourceDepletions,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 source stock for recording depletion %d: %+v", depletion.Id, err)
return err
}
}
if err := s.reflowRecordingScope(
ctx,
tx,
depletion.ProductWarehouseId,
depletion.RecordingId,
recordingLaneStockable,
recordingFunctionDepletionIn,
recordingSourceDepletions,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 destination stock for recording depletion %d: %+v", depletion.Id, err)
return err
}
s.logDepletionTrace("reduce:done", depletion, "")
}
@@ -749,9 +796,9 @@ func (s *recordingService) reduceRecordingEggs(
if len(eggs) == 0 {
return nil
}
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for reducing recording eggs")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for reducing recording eggs")
return errors.New("fifo v2 service is not available")
}
for _, egg := range eggs {
@@ -759,14 +806,22 @@ func (s *recordingService) reduceRecordingEggs(
continue
}
s.logEggTrace("reduce:start", egg, "")
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: fifo.StockableKeyRecordingEgg,
StockableID: egg.Id,
ProductWarehouseID: egg.ProductWarehouseId,
Quantity: -float64(egg.Qty),
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to reduce FIFO stock for recording egg %d: %+v", egg.Id, err)
if err := tx.WithContext(ctx).
Model(&entity.RecordingEgg{}).
Where("id = ?", egg.Id).
Update("total_qty", 0).Error; err != nil {
return err
}
if err := s.reflowRecordingScope(
ctx,
tx,
egg.ProductWarehouseId,
egg.RecordingId,
recordingLaneStockable,
recordingFunctionEggIn,
recordingSourceEggs,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording egg %d: %+v", egg.Id, err)
return err
}
s.logEggTrace("reduce:done", egg, "")
@@ -934,9 +989,9 @@ func (s *recordingService) syncRecordingStocks(
note string,
actorID uint,
) error {
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for syncing recording stocks")
return errors.New("fifo service is not available")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for syncing recording stocks")
return errors.New("fifo v2 service is not available")
}
existingByWarehouse := make(map[uint][]entity.RecordingStock)
@@ -1125,9 +1180,9 @@ func (s *recordingService) rollbackRecordingInventory(ctx context.Context, tx *g
}
func (s *recordingService) requireFIFO() error {
if s.FifoSvc == nil {
s.Log.Errorf("FIFO service is not available for recording operations")
return fiber.NewError(fiber.StatusInternalServerError, "FIFO service is required for recording operations")
if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO v2 service is not available for recording operations")
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is required for recording operations")
}
return nil
}
+2 -16
View File
@@ -23,7 +23,6 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
@@ -40,7 +39,6 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
expenseRepository := expenseRepo.NewExpenseRepository(db)
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
projectFlockKandangRepository := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
@@ -73,19 +71,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
expenseServiceInstance,
)
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
_ = fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyPurchaseItems,
Table: "purchase_items",
Columns: fifo.StockableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
TotalQuantity: "total_qty",
TotalUsedQuantity: "total_used",
CreatedAt: "id",
},
OrderBy: []string{"id ASC"},
})
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
purchaseService := service.NewPurchaseService(
validate,
@@ -97,7 +83,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockKandangRepository,
approvalService,
expenseBridge,
fifoService,
fifoStockV2Service,
documentSvc,
)
@@ -0,0 +1,93 @@
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 (
purchaseInFunctionCode = "PURCHASE_IN"
purchaseStockableLane = "STOCKABLE"
purchaseSourceTable = "purchase_items"
)
func reflowPurchaseScope(
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 productWarehouseID == 0 {
return fmt.Errorf("product warehouse id is required")
}
flagGroupCode, err := resolvePurchaseFlagGroupByProductWarehouse(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 resolvePurchaseFlagGroupByProductWarehouse(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 = ?", purchaseStockableLane).
Where("rr.function_code = ?", purchaseInFunctionCode).
Where("rr.source_table = ?", purchaseSourceTable).
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
}
func assignEarliestAsOf(m map[uint]time.Time, productWarehouseID uint, asOf time.Time) {
if productWarehouseID == 0 {
return
}
if current, ok := m[productWarehouseID]; !ok || asOf.Before(current) {
m[productWarehouseID] = asOf
}
}
@@ -57,7 +57,7 @@ type purchaseService struct {
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
ExpenseBridge PurchaseExpenseBridge
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
DocumentSvc commonSvc.DocumentService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
@@ -77,7 +77,7 @@ func NewPurchaseService(
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
approvalSvc commonSvc.ApprovalService,
expenseBridge PurchaseExpenseBridge,
fifoSvc commonSvc.FifoService,
fifoStockV2Svc commonSvc.FifoStockV2Service,
documentSvc commonSvc.DocumentService,
) PurchaseService {
return &purchaseService{
@@ -91,7 +91,7 @@ func NewPurchaseService(
ProjectFlockKandangRepo: projectFlockKandangRepo,
ApprovalSvc: approvalSvc,
ExpenseBridge: expenseBridge,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
DocumentSvc: documentSvc,
approvalWorkflow: utils.ApprovalWorkflowPurchase,
}
@@ -1026,22 +1026,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
deltas := make(map[uint]float64)
affected := make(map[uint]struct{})
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared))
totalQtyDeltas := make(map[uint]float64)
fifoAdds := make([]struct {
itemID uint
pwID uint
qty float64
}, 0, len(prepared))
fifoSubs := make([]struct {
itemID uint
pwID uint
qty float64
}, 0, len(prepared))
resolvePendingIDs := make(map[uint]struct{})
reflowAsOfByPW := make(map[uint]time.Time)
logEntries := make([]struct {
itemID uint
pwID uint
@@ -1083,35 +1072,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
delta float64
}{itemID: item.Id, pwID: *newPWID, delta: deltaQty})
}
switch {
case deltaQty > 0 && newPWID != nil:
if s.FifoSvc != nil {
fifoAdds = append(fifoAdds, struct {
itemID uint
pwID uint
qty float64
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
resolvePendingIDs[*newPWID] = struct{}{}
} else {
deltas[*newPWID] += deltaQty
totalQtyDeltas[item.Id] += deltaQty
}
case deltaQty < 0 && newPWID != nil:
if s.FifoSvc != nil {
fifoSubs = append(fifoSubs, struct {
itemID uint
pwID uint
qty float64
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
affected[*newPWID] = struct{}{}
resolvePendingIDs[*newPWID] = struct{}{}
} else {
deltas[*newPWID] += deltaQty // negative
affected[*newPWID] = struct{}{}
totalQtyDeltas[item.Id] += deltaQty
}
case newPWID != nil:
resolvePendingIDs[*newPWID] = struct{}{}
if newPWID != nil {
assignEarliestAsOf(reflowAsOfByPW, *newPWID, prep.receivedDate.UTC())
}
if deltaQty != 0 {
totalQtyDeltas[item.Id] += deltaQty
}
if deltaQty < 0 && newPWID != nil {
affected[*newPWID] = struct{}{}
}
dateCopy := prep.receivedDate
@@ -1147,10 +1115,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return err
}
if err := pwRepoTx.AdjustQuantities(c.Context(), deltas, nil); err != nil {
return err
}
if len(priceUpdates) > 0 {
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
return err
@@ -1180,48 +1144,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
}
}
if s.FifoSvc != nil {
for _, adj := range fifoAdds {
if adj.pwID == 0 || adj.qty <= 0 {
continue
}
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyPurchaseItems,
StockableID: adj.itemID,
ProductWarehouseID: adj.pwID,
Quantity: adj.qty,
Tx: tx,
}); err != nil {
return err
}
if len(reflowAsOfByPW) > 0 {
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
for _, adj := range fifoSubs {
if adj.pwID == 0 || adj.qty >= 0 {
continue
}
if err := s.FifoSvc.AdjustStockableQuantity(c.Context(), commonSvc.StockAdjustRequest{
StockableKey: fifo.StockableKeyPurchaseItems,
StockableID: adj.itemID,
ProductWarehouseID: adj.pwID,
Quantity: adj.qty,
Tx: tx,
}); err != nil {
for pwID, asOf := range reflowAsOfByPW {
asOfCopy := asOf
if err := reflowPurchaseScope(c.Context(), s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
return err
}
}
for pwID := range resolvePendingIDs {
if pwID == 0 {
continue
}
resolved, err := s.FifoSvc.ResolvePending(c.Context(), commonSvc.PendingResolveRequest{
ProductWarehouseID: pwID,
Tx: tx,
})
if err != nil {
return err
}
s.Log.Infof("ResolvePending purchase=%d pw=%d resolved=%d", purchase.Id, pwID, len(resolved))
}
}
if len(logEntries) > 0 {
@@ -1577,10 +1509,9 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
return nil
}
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
deltas := make(map[uint]float64)
affected := make(map[uint]struct{})
reflowAsOfByPW := make(map[uint]time.Time)
logEntries := make([]struct {
pwID uint
qty float64
@@ -1596,42 +1527,43 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
pwID := *item.ProductWarehouseId
qty := item.TotalQty
if s.FifoSvc != nil {
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: fifo.StockableKeyPurchaseItems,
StockableID: item.Id,
ProductWarehouseID: pwID,
Quantity: -qty,
Tx: tx,
}); err != nil {
return err
}
logEntries = append(logEntries, struct {
pwID uint
qty float64
}{pwID: pwID, qty: qty})
continue
if err := tx.WithContext(ctx).
Model(&entity.PurchaseItem{}).
Where("id = ?", item.Id).
Update("total_qty", 0).Error; err != nil {
return err
}
deltas[pwID] -= qty
affected[pwID] = struct{}{}
if item.ReceivedDate != nil {
assignEarliestAsOf(reflowAsOfByPW, pwID, item.ReceivedDate.UTC())
} else {
assignEarliestAsOf(reflowAsOfByPW, pwID, time.Now().UTC())
}
logEntries = append(logEntries, struct {
pwID uint
qty float64
}{pwID: pwID, qty: qty})
}
if s.FifoSvc == nil && len(deltas) > 0 {
if err := pwRepoTx.AdjustQuantities(ctx, deltas, nil); err != nil {
return err
if len(reflowAsOfByPW) > 0 {
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
if len(affected) > 0 {
if err := pwRepoTx.CleanupEmpty(ctx, affected); err != nil {
for pwID, asOf := range reflowAsOfByPW {
asOfCopy := asOf
if err := reflowPurchaseScope(ctx, s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
return err
}
}
}
if len(affected) > 0 {
if err := rProductWarehouse.NewProductWarehouseRepository(tx).CleanupEmpty(ctx, affected); err != nil {
return err
}
}
if strings.TrimSpace(note) != "" && actorID != 0 && len(logEntries) > 0 {
logs := make([]*entity.StockLog, 0, len(logEntries))
for _, entry := range logEntries {