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
+3 -8
View File
@@ -134,14 +134,9 @@ func main() {
reflowReq := commonSvc.FifoStockV2ReflowRequest{ reflowReq := commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: route.FlagGroupCode, FlagGroupCode: route.FlagGroupCode,
ProductWarehouseID: adj.ProductWarehouseID, ProductWarehouseID: adj.ProductWarehouseID,
Usable: commonSvc.FifoStockV2Ref{ AsOf: &adj.CreatedAt,
ID: adj.ID, IdempotencyKey: fmt.Sprintf("delete-adjustment-usable-%d-%d", adj.ID, time.Now().UnixNano()),
LegacyTypeKey: fifo.UsableKeyAdjustmentOut.String(), Tx: tx,
FunctionCode: route.FunctionCode,
},
DesiredQty: 0,
IdempotencyKey: fmt.Sprintf("delete-adjustment-usable-%d-%d", adj.ID, time.Now().UnixNano()),
Tx: tx,
} }
if _, err := fifoStockV2Svc.Reflow(ctx, reflowReq); err != nil { if _, err := fifoStockV2Svc.Reflow(ctx, reflowReq); err != nil {
return fmt.Errorf("reflow usable to zero: %w", err) return fmt.Errorf("reflow usable to zero: %w", err)
+2 -13
View File
@@ -121,12 +121,7 @@ func main() {
continue continue
} }
usableType := fifo.UsableKeyAdjustmentOut.String() activeAllocationCount, err := countActiveAllocations(ctx, db, fifo.UsableKeyAdjustmentOut.String(), adj.ID)
if route.SourceTable == "adjustment_stocks" && strings.TrimSpace(route.LegacyTypeKey) != "" {
usableType = strings.TrimSpace(route.LegacyTypeKey)
}
activeAllocationCount, err := countActiveAllocations(ctx, db, usableType, adj.ID)
if err != nil { if err != nil {
fmt.Printf("FAIL adj=%d error=count allocations: %v\n", adj.ID, err) fmt.Printf("FAIL adj=%d error=count allocations: %v\n", adj.ID, err)
failed++ failed++
@@ -142,13 +137,7 @@ func main() {
reflowReq := commonSvc.FifoStockV2ReflowRequest{ reflowReq := commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: route.FlagGroupCode, FlagGroupCode: route.FlagGroupCode,
ProductWarehouseID: adj.ProductWarehouseID, ProductWarehouseID: adj.ProductWarehouseID,
Usable: commonSvc.FifoStockV2Ref{ IdempotencyKey: fmt.Sprintf("manual-adjustment-reflow-%d-%d", adj.ID, time.Now().UnixNano()),
ID: adj.ID,
LegacyTypeKey: usableType,
FunctionCode: route.FunctionCode,
},
DesiredQty: desiredQty,
IdempotencyKey: fmt.Sprintf("manual-adjustment-reflow-%d-%d", adj.ID, time.Now().UnixNano()),
} }
if asOfCreatedAt { if asOfCreatedAt {
asOf := adj.CreatedAt asOf := adj.CreatedAt
+116 -25
View File
@@ -401,12 +401,9 @@ func (s *fifoStockV2Service) rollbackInternal(
} }
func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*ReflowResult, error) { func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*ReflowResult, error) {
if strings.TrimSpace(req.FlagGroupCode) == "" || req.ProductWarehouseID == 0 || req.Usable.ID == 0 || strings.TrimSpace(req.Usable.LegacyTypeKey) == "" { if strings.TrimSpace(req.FlagGroupCode) == "" || req.ProductWarehouseID == 0 {
return nil, fmt.Errorf("%w: invalid reflow request", ErrInvalidRequest) return nil, fmt.Errorf("%w: invalid reflow request", ErrInvalidRequest)
} }
if req.DesiredQty < 0 {
return nil, fmt.Errorf("%w: desired qty must be >= 0", ErrInvalidRequest)
}
result := &ReflowResult{} result := &ReflowResult{}
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error { err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
@@ -420,11 +417,7 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
hash := requestHash(map[string]any{ hash := requestHash(map[string]any{
"flag_group_code": req.FlagGroupCode, "flag_group_code": req.FlagGroupCode,
"product_warehouse_id": req.ProductWarehouseID, "product_warehouse_id": req.ProductWarehouseID,
"usable_type": req.Usable.LegacyTypeKey,
"usable_id": req.Usable.ID,
"desired_qty": req.DesiredQty,
"as_of": req.AsOf, "as_of": req.AsOf,
"allow_over_consume": req.AllowOverConsume,
}) })
logRow, reused, err := s.beginOperation( logRow, reused, err := s.beginOperation(
tx, tx,
@@ -433,8 +426,8 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
hash, hash,
req.ProductWarehouseID, req.ProductWarehouseID,
req.FlagGroupCode, req.FlagGroupCode,
req.Usable.LegacyTypeKey, "",
req.Usable.ID, 0,
) )
if err != nil { if err != nil {
return err return err
@@ -456,32 +449,82 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
}() }()
} }
rollbackRes, rollbackErr := s.rollbackInternal(ctx, tx, RollbackRequest{ usableRows, gatherErr := s.gatherAllRows(ctx, tx, GatherRequest{
FlagGroupCode: req.FlagGroupCode,
Lane: LaneUsable,
ProductWarehouseID: req.ProductWarehouseID, ProductWarehouseID: req.ProductWarehouseID,
Usable: req.Usable, Limit: s.defaultGatherLimit,
ReleaseQty: nil, })
Reason: "reflow reset", if gatherErr != nil {
}, req.FlagGroupCode) err = gatherErr
if rollbackErr != nil { return gatherErr
err = rollbackErr
return rollbackErr
} }
result.Rollback = *rollbackRes result.ProcessedUsables = len(usableRows)
if req.DesiredQty > 0 { for _, usableRow := range usableRows {
desiredQty := usableRow.Quantity + usableRow.PendingQuantity
rollbackRes, rollbackErr := s.rollbackInternal(ctx, tx, RollbackRequest{
ProductWarehouseID: req.ProductWarehouseID,
Usable: usableRow.Ref,
ReleaseQty: nil,
Reason: "reflow reset",
}, req.FlagGroupCode)
if rollbackErr != nil {
err = rollbackErr
return rollbackErr
}
result.Rollback.ReleasedQty += rollbackRes.ReleasedQty
if len(rollbackRes.Details) > 0 {
result.Rollback.Details = append(result.Rollback.Details, rollbackRes.Details...)
}
minDesired := rollbackRes.ReleasedQty + usableRow.PendingQuantity
if desiredQty < minDesired {
desiredQty = minDesired
}
if desiredQty <= 0 {
continue
}
asOf := usableRow.SortAt
if req.AsOf != nil && asOf.Before(*req.AsOf) {
asOf = *req.AsOf
}
allocateRes, allocateErr := s.allocateInternal(ctx, tx, AllocateRequest{ allocateRes, allocateErr := s.allocateInternal(ctx, tx, AllocateRequest{
FlagGroupCode: req.FlagGroupCode, FlagGroupCode: req.FlagGroupCode,
ProductWarehouseID: req.ProductWarehouseID, ProductWarehouseID: req.ProductWarehouseID,
Usable: req.Usable, Usable: usableRow.Ref,
NeedQty: req.DesiredQty, NeedQty: desiredQty,
AllowOverConsume: req.AllowOverConsume, AsOf: &asOf,
AsOf: req.AsOf,
}) })
if allocateErr != nil { if allocateErr != nil {
err = allocateErr err = allocateErr
return allocateErr return allocateErr
} }
result.Allocate = *allocateRes result.Allocate.AllocatedQty += allocateRes.AllocatedQty
result.Allocate.PendingQty += allocateRes.PendingQty
if len(allocateRes.Details) > 0 {
result.Allocate.Details = append(result.Allocate.Details, allocateRes.Details...)
}
}
expectedQty, calcErr := s.calculateWarehouseAvailableForGroup(ctx, tx, req.ProductWarehouseID, req.FlagGroupCode, nil)
if calcErr != nil {
err = calcErr
return calcErr
}
actualQty, loadErr := s.loadWarehouseQty(ctx, tx, req.ProductWarehouseID)
if loadErr != nil {
err = loadErr
return loadErr
}
drift := expectedQty - actualQty
if math.Abs(drift) >= 1e-6 {
if adjustErr := s.adjustProductWarehouseQty(tx, req.ProductWarehouseID, drift); adjustErr != nil {
err = adjustErr
return adjustErr
}
} }
if finishErr := s.finishOperation(tx, logRow, result); finishErr != nil { if finishErr := s.finishOperation(tx, logRow, result); finishErr != nil {
@@ -496,6 +539,54 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
return result, nil return result, nil
} }
func (s *fifoStockV2Service) gatherAllRows(
ctx context.Context,
tx *gorm.DB,
req GatherRequest,
) ([]GatherRow, error) {
limit := req.Limit
if limit <= 0 {
limit = s.defaultGatherLimit
}
if limit <= 0 {
limit = 1000
}
req.Limit = limit
out := make([]GatherRow, 0, limit)
var cursorSortAt *time.Time
cursorSourceTable := ""
var cursorSourceID uint
for {
req.AfterSortAt = cursorSortAt
req.AfterSourceTable = cursorSourceTable
req.AfterSourceID = cursorSourceID
rows, err := s.gatherRows(ctx, tx, req)
if err != nil {
return nil, err
}
if len(rows) == 0 {
break
}
out = append(out, rows...)
if len(rows) < limit {
break
}
last := rows[len(rows)-1]
lastSortAt := last.SortAt
cursorSortAt = &lastSortAt
cursorSourceTable = last.SourceTable
cursorSourceID = last.SourceID
}
return out, nil
}
func (s *fifoStockV2Service) loadActiveAllocations( func (s *fifoStockV2Service) loadActiveAllocations(
tx *gorm.DB, tx *gorm.DB,
usableType string, usableType string,
@@ -197,6 +197,9 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
if req.AsOf != nil { if req.AsOf != nil {
whereParts = append(whereParts, fmt.Sprintf("%s <= ?", sortExpr)) whereParts = append(whereParts, fmt.Sprintf("%s <= ?", sortExpr))
} }
if req.From != nil {
whereParts = append(whereParts, fmt.Sprintf("%s >= ?", sortExpr))
}
if rule.ScopeSQL != nil && strings.TrimSpace(*rule.ScopeSQL) != "" { if rule.ScopeSQL != nil && strings.TrimSpace(*rule.ScopeSQL) != "" {
whereParts = append(whereParts, fmt.Sprintf("(%s)", normalizeScopeSQL(*rule.ScopeSQL))) whereParts = append(whereParts, fmt.Sprintf("(%s)", normalizeScopeSQL(*rule.ScopeSQL)))
@@ -236,6 +239,9 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
if req.AsOf != nil { if req.AsOf != nil {
args = append(args, *req.AsOf) args = append(args, *req.AsOf)
} }
if req.From != nil {
args = append(args, *req.From)
}
return subquery, args, nil return subquery, args, nil
} }
@@ -34,6 +34,7 @@ type GatherRequest struct {
FlagGroupCode string FlagGroupCode string
Lane Lane Lane Lane
ProductWarehouseID uint ProductWarehouseID uint
From *time.Time
AsOf *time.Time AsOf *time.Time
Limit int Limit int
AfterSortAt *time.Time AfterSortAt *time.Time
@@ -98,17 +99,15 @@ type RollbackResult struct {
type ReflowRequest struct { type ReflowRequest struct {
FlagGroupCode string FlagGroupCode string
ProductWarehouseID uint ProductWarehouseID uint
Usable Ref
DesiredQty float64
AllowOverConsume *bool
IdempotencyKey string
AsOf *time.Time AsOf *time.Time
IdempotencyKey string
Tx *gorm.DB Tx *gorm.DB
} }
type ReflowResult struct { type ReflowResult struct {
Rollback RollbackResult ProcessedUsables int
Allocate AllocateResult Rollback RollbackResult
Allocate AllocateResult
} }
type RecalculateRequest struct { type RecalculateRequest struct {
@@ -21,7 +21,6 @@ import (
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -167,15 +166,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
transactionType := utils.ResolveAdjustmentTransactionType(routeMeta.FunctionCode) 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 createdAdjustmentStockId uint
var projectFlockKandangID *uint var projectFlockKandangID *uint
@@ -228,6 +218,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
Price: req.Price, Price: req.Price,
GrandTotal: grandTotal, GrandTotal: grandTotal,
} }
switch routeMeta.Lane {
case adjustmentLaneStockable:
adjustmentStock.TotalQty = qty
case adjustmentLaneUsable:
adjustmentStock.UsageQty = qty
}
code, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix) code, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
if err != nil { if err != nil {
return err return err
@@ -240,60 +236,32 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
var increaseQty float64 var increaseQty float64
var decreaseQty 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 { switch routeMeta.Lane {
case adjustmentLaneStockable: case adjustmentLaneStockable:
fifoNote := fmt.Sprintf("Stock Adjustment %s #%s", routeMeta.FunctionCode, adjustmentStock.AdjNumber) increaseQty = refreshedAdjustment.TotalQty
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
case adjustmentLaneUsable: case adjustmentLaneUsable:
if s.FifoStockV2Svc != nil { decreaseQty = refreshedAdjustment.UsageQty
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")
} }
stockLogs, err := stockLogRepoTX.GetByProductWarehouse(ctx, productWarehouse.Id, 1) 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" projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "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 {
if s.FifoStockV2Svc != nil && len(req.Products) > 0 { return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
pakanProducts, err = s.resolvePakanProducts(c.Context(), tx, req.Products)
if err != nil {
return err
}
} }
flagGroupByProduct := make(map[uint]string, len(req.Products))
for _, product := range req.Products { for _, product := range req.Products {
detail := detailMap[uint64(product.ProductID)] 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 flagGroupCode, ok := flagGroupByProduct[uint(product.ProductID)]
outPendingQty := 0.0 if !ok {
useFifoV2 := s.FifoStockV2Svc != nil && pakanProducts[uint(product.ProductID)] flagGroupCode, err = s.resolveTransferFlagGroup(c.Context(), tx, 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,
})
if err != nil { 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 flagGroupByProduct[uint(product.ProductID)] = flagGroupCode
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
} }
if err := tx.Model(&entity.StockTransferDetail{}). if err := tx.Model(&entity.StockTransferDetail{}).
Where("id = ?", detail.Id). Where("id = ?", detail.Id).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"usage_qty": outUsageQty, "usage_qty": product.ProductQty,
"pending_qty": outPendingQty, "pending_qty": 0,
"total_qty": product.ProductQty,
}).Error; err != nil { }).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") 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{ stockLogDecrease := &entity.StockLog{
ProductWarehouseId: uint(*detail.SourceProductWarehouseID), ProductWarehouseId: uint(*detail.SourceProductWarehouseID),
CreatedBy: uint(actorID), CreatedBy: uint(actorID),
Increase: 0, Increase: 0,
Decrease: product.ProductQty, Decrease: outUsageQty,
LoggableType: string(utils.StockLogTypeTransfer), LoggableType: string(utils.StockLogTypeTransfer),
LoggableId: uint(detail.Id), LoggableId: uint(detail.Id),
Notes: "", 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") return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
} }
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) inAddedQty := outUsageQty
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")
}
stockLogIncrease := &entity.StockLog{ stockLogIncrease := &entity.StockLog{
ProductWarehouseId: uint(*detail.DestProductWarehouseID), ProductWarehouseId: uint(*detail.DestProductWarehouseID),
CreatedBy: uint(actorID), CreatedBy: uint(actorID),
Increase: product.ProductQty, Increase: inAddedQty,
Decrease: 0, Decrease: 0,
LoggableType: string(utils.StockLogTypeTransfer), LoggableType: string(utils.StockLogTypeTransfer),
LoggableId: uint(detail.Id), LoggableId: uint(detail.Id),
@@ -657,51 +619,45 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return result, nil return result, nil
} }
func (s *transferService) resolvePakanProducts( func (s *transferService) resolveTransferFlagGroup(
ctx context.Context, ctx context.Context,
tx *gorm.DB, tx *gorm.DB,
products []validation.TransferProduct, productID uint,
) (map[uint]bool, error) { ) (string, error) {
out := make(map[uint]bool, len(products)) if productID == 0 {
if len(products) == 0 { return "", fmt.Errorf("product id is required")
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
} }
type row struct { 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). err := tx.WithContext(ctx).
Table("flags f"). Table("fifo_stock_v2_route_rules rr").
Select("DISTINCT f.flagable_id AS product_id"). Select("rr.flag_group_code").
Where("f.flagable_type = ?", entity.FlagableTypeProduct). Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("f.name IN ?", []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}). Where("rr.is_active = TRUE").
Where("f.flagable_id IN ?", productIDs). Where("rr.lane = ?", "USABLE").
Scan(&rows).Error 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 { if err != nil {
return nil, err return "", err
} }
for _, row := range rows { return strings.TrimSpace(selected.FlagGroupCode), nil
out[row.ProductID] = true
}
return out, nil
} }
func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error { func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
+3 -22
View File
@@ -2,7 +2,6 @@ package marketing
import ( import (
"fmt" "fmt"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -20,7 +19,6 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" 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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
) )
type MarketingModule struct{} type MarketingModule struct{}
@@ -35,24 +33,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockLogRepo := rShared.NewStockLogRepository(db) stockLogRepo := rShared.NewStockLogRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
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))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo) approvalSvc := commonSvc.NewApprovalService(approvalRepo)
@@ -64,8 +45,8 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate) salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoStockV2Service, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate) deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoStockV2Service, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService) RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
@@ -15,7 +15,6 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" 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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -36,7 +35,7 @@ type deliveryOrdersService struct {
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
StockLogRepo rShared.StockLogRepository StockLogRepo rShared.StockLogRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService FifoStockV2Svc commonSvc.FifoStockV2Service
} }
func NewDeliveryOrdersService( func NewDeliveryOrdersService(
@@ -45,7 +44,7 @@ func NewDeliveryOrdersService(
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository, marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
stockLogRepo rShared.StockLogRepository, stockLogRepo rShared.StockLogRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService, fifoStockV2Svc commonSvc.FifoStockV2Service,
validate *validator.Validate, validate *validator.Validate,
) DeliveryOrdersService { ) DeliveryOrdersService {
return &deliveryOrdersService{ return &deliveryOrdersService{
@@ -55,7 +54,7 @@ func NewDeliveryOrdersService(
MarketingDeliveryProductRepo: marketingDeliveryProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
StockLogRepo: stockLogRepo, StockLogRepo: stockLogRepo,
ApprovalSvc: approvalSvc, 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") 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) 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") 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{ decreaseLog := &entity.StockLog{
Decrease: result.UsageQuantity, Decrease: allocatedDelta,
LoggableType: string(utils.StockLogTypeMarketing), LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id, LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId, ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID, 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) 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) deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id) currentUsage := deliveryProduct.UsageQty
if err != nil { currentPending := deliveryProduct.PendingQty
currentUsage = 0 if currentUsage <= 0 && currentPending <= 0 {
}
if currentUsage == 0 {
return nil return nil
} }
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ deliveryProduct.UsageQty = 0
UsableKey: fifo.UsableKeyMarketingDelivery, deliveryProduct.PendingQty = 0
UsableID: deliveryProduct.Id, if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
Tx: tx, return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset delivery product")
}); err != nil {
return err
} }
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil { if err := reflowMarketingScope(
return err 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{ increaseLog := &entity.StockLog{
Increase: currentUsage, Increase: releasedUsage,
LoggableType: string(utils.StockLogTypeMarketing), LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id, LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId, ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID, 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) stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
if err != nil { 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" userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" 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/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -43,12 +42,12 @@ type salesOrdersService struct {
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
UserRepo userRepo.UserRepository UserRepo userRepo.UserRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService FifoStockV2Svc commonSvc.FifoStockV2Service
WarehouseRepo warehouseRepo.WarehouseRepository WarehouseRepo warehouseRepo.WarehouseRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository 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 { projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
return &salesOrdersService{ return &salesOrdersService{
Log: utils.Log, Log: utils.Log,
@@ -58,7 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
UserRepo: userRepo, UserRepo: userRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc, FifoStockV2Svc: fifoStockV2Svc,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
} }
@@ -376,15 +375,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if qtyDiff < 0 { if qtyDiff < 0 {
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.") return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
} else if qtyDiff > 0 { } else if qtyDiff > 0 {
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ nextRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty + qtyDiff
UsableKey: fifo.UsableKeyMarketingDelivery, if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, nextRequestedQty, 0); err != nil {
UsableID: deliveryProduct.Id, return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing delivery fifo fields")
ProductWarehouseID: rp.ProductWarehouseId, }
Quantity: qtyDiff, if err := reflowMarketingScope(
Tx: dbTransaction, c.Context(),
}) s.FifoStockV2Svc,
if err != nil { dbTransaction,
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err)) 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)) 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{ if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, 0, 0); err != nil {
UsableKey: fifo.UsableKeyMarketingDelivery, return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset marketing delivery fifo fields")
UsableID: deliveryProduct.Id, }
Tx: dbTransaction, if err := reflowMarketingScope(
}); err != nil { c.Context(),
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err)) 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 { 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) deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
if err == nil && len(deliveryProducts) > 0 { if err == nil && len(deliveryProducts) > 0 {
for _, dp := range deliveryProducts { for _, dp := range deliveryProducts {
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{ if err := marketingDeliveryProductRepoTx.UpdateFifoFields(c.Context(), dp.Id, 0, 0); err != nil {
UsableKey: fifo.UsableKeyMarketingDelivery, return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset fifo fields for delivery product %d", dp.Id))
UsableID: dp.Id, }
Tx: dbTransaction, if err := reflowMarketingScope(
}); err != nil { c.Context(),
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err)) 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 ( import (
"fmt" "fmt"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -10,7 +9,6 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" 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" 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" 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) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
productRepo := rProduct.NewProductRepository(db) productRepo := rProduct.NewProductRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
userRepo := rUser.NewUserRepository(db) 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) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil { 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, projectflockpopulationrepo,
chickinDetailRepo, chickinDetailRepo,
validate, validate,
fifoService) fifoStockV2Service)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ChickinRoutes(router, userService, chickinService) ChickinRoutes(router, userService, chickinService)
@@ -19,7 +19,6 @@ import (
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -27,8 +26,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var chickinUsableKey = fifo.UsableKeyProjectChickin
type ChickinService interface { type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
@@ -51,11 +48,11 @@ type chickinService struct {
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
FifoSvc commonSvc.FifoService FifoStockV2Svc commonSvc.FifoStockV2Service
StockLogRepo rStockLogs.StockLogRepository 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{ return &chickinService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
@@ -68,7 +65,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
ProjectflockKandangRepo: projectflockkandangRepo, ProjectflockKandangRepo: projectflockkandangRepo,
ProjectflockPopulationRepo: projectflockpopulationRepo, ProjectflockPopulationRepo: projectflockpopulationRepo,
ProjectChickinDetailRepo: projectChickinDetailRepo, ProjectChickinDetailRepo: projectChickinDetailRepo,
FifoSvc: fifoSvc, FifoStockV2Svc: fifoStockV2Svc,
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()), StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
} }
} }
@@ -372,18 +369,9 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
if chickin.UsageQty > 0 { if chickin.UsageQty > 0 {
currentUsageQty := chickin.UsageQty
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil { if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
return err 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 { 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)) 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 err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) { if !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id)) 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 { 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 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{ if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0); err != nil {
UsableKey: chickinUsableKey,
UsableID: chickin.Id,
ProductWarehouseID: chickin.ProductWarehouseId,
Quantity: desiredQty,
AllowPending: true,
Tx: tx,
})
if err != nil {
return err 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 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{ decreaseLog := &entity.StockLog{
Decrease: result.UsageQuantity, Decrease: refreshed.UsageQty,
LoggableType: string(utils.StockLogTypeChikin), LoggableType: string(utils.StockLogTypeChikin),
LoggableId: chickin.Id, LoggableId: refreshed.Id,
ProductWarehouseId: chickin.ProductWarehouseId, ProductWarehouseId: refreshed.ProductWarehouseId,
CreatedBy: actorID, 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 { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") 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 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 return nil
} }
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error { 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 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{ if err := tx.WithContext(ctx).
StockableKey: fifo.StockableKeyProjectFlockPopulation, Model(&entity.ProjectFlockPopulation{}).
StockableID: population.Id, Where("id = ?", population.Id).
ProductWarehouseID: targetPW.Id, Update("total_qty", chickin.UsageQty).Error; err != nil {
Quantity: chickin.UsageQty,
Tx: tx,
})
if err != nil {
return err 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 { 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 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 var currentUsage float64
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(&currentUsage).Error; err != nil { 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 return err
} }
@@ -705,6 +705,14 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
return err 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 { if currentUsage > 0 {
increaseLog := &entity.StockLog{ increaseLog := &entity.StockLog{
Increase: currentUsage, Increase: currentUsage,
@@ -726,7 +734,9 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
increaseLog.Stock += increaseLog.Increase 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 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") 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 ( import (
"fmt" "fmt"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -26,7 +25,6 @@ import (
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" 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) productRepo := rProduct.NewProductRepository(db)
chickinRepo := rChickin.NewChickinRepository(db) chickinRepo := rChickin.NewChickinRepository(db)
chickinDetailRepo := rChickin.NewChickinDetailRepository(db) chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
stockLogRepo := rStockLogs.NewStockLogRepository(db) stockLogRepo := rStockLogs.NewStockLogRepository(db)
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db) productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
@@ -61,76 +58,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
validate, validate,
) )
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, 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))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
@@ -169,7 +97,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo, projectFlockPopulationRepo,
chickinDetailRepo, chickinDetailRepo,
validate, validate,
fifoService, fifoStockV2Service,
) )
recordingService := sRecording.NewRecordingService( recordingService := sRecording.NewRecordingService(
@@ -179,7 +107,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo, projectFlockPopulationRepo,
approvalRepo, approvalRepo,
approvalService, approvalService,
fifoService, fifoStockV2Service,
stockLogRepo, stockLogRepo,
productionStandardService, productionStandardService,
projectFlockService, 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 ProductionStandardSvc sProductionStandard.ProductionStandardService
ProjectFlockSvc sProjectFlock.ProjectflockService ProjectFlockSvc sProjectFlock.ProjectflockService
ChickinSvc sChickin.ChickinService ChickinSvc sChickin.ChickinService
FifoSvc commonSvc.FifoService FifoStockV2Svc commonSvc.FifoStockV2Service
StockLogRepo rStockLogs.StockLogRepository StockLogRepo rStockLogs.StockLogRepository
} }
@@ -63,7 +63,7 @@ func NewRecordingService(
projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
approvalRepo commonRepo.ApprovalRepository, approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService, fifoStockV2Svc commonSvc.FifoStockV2Service,
stockLogRepo rStockLogs.StockLogRepository, stockLogRepo rStockLogs.StockLogRepository,
productionStandardSvc sProductionStandard.ProductionStandardService, productionStandardSvc sProductionStandard.ProductionStandardService,
projectFlockSvc sProjectFlock.ProjectflockService, projectFlockSvc sProjectFlock.ProjectflockService,
@@ -82,7 +82,7 @@ func NewRecordingService(
ProductionStandardSvc: productionStandardSvc, ProductionStandardSvc: productionStandardSvc,
ProjectFlockSvc: projectFlockSvc, ProjectFlockSvc: projectFlockSvc,
ChickinSvc: chickinSvc, ChickinSvc: chickinSvc,
FifoSvc: fifoSvc, FifoStockV2Svc: fifoStockV2Svc,
StockLogRepo: stockLogRepo, StockLogRepo: stockLogRepo,
} }
} }
@@ -8,7 +8,6 @@ import (
"strings" "strings"
"time" "time"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -18,9 +17,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
const depletionUsageTolerance = 0.000001 const depletionUsageTolerance = 0.000001
func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) { func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) {
@@ -101,9 +97,9 @@ func (s *recordingService) consumeRecordingStocks(
if len(stocks) == 0 { if len(stocks) == 0 {
return nil return nil
} }
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for consuming recording stocks") s.Log.Errorf("FIFO v2 service is not available for consuming recording stocks")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available") return errors.New("stock log repository is not available")
@@ -125,38 +121,52 @@ func (s *recordingService) consumeRecordingStocks(
} }
desiredTotal := desired + pending desiredTotal := desired + pending
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ if err := s.Repository.UpdateStockUsage(tx, stock.Id, desiredTotal, 0); err != nil {
UsableKey: recordingStockUsableKey, return err
UsableID: stock.Id, }
ProductWarehouseID: stock.ProductWarehouseId, if err := s.reflowRecordingScope(
Quantity: desiredTotal, ctx,
AllowPending: true, tx,
Tx: tx, stock.ProductWarehouseId,
}) stock.RecordingId,
if err != nil { recordingLaneUsable,
s.Log.Errorf("Failed to consume FIFO stock for recording stock %d: %+v", stock.Id, err) recordingFunctionStockOut,
recordingSourceStocks,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording stock %d: %+v", stock.Id, err)
return 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 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 logDecrease := actualUsage
if result.PendingQuantity > 0 { if actualPending > 0 {
logDecrease += result.PendingQuantity logDecrease += actualPending
} }
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
log := &entity.StockLog{ log := &entity.StockLog{
ProductWarehouseId: stock.ProductWarehouseId, ProductWarehouseId: refreshed.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
Decrease: logDecrease, Decrease: logDecrease,
LoggableType: string(utils.StockLogTypeRecording), LoggableType: string(utils.StockLogTypeRecording),
LoggableId: stock.RecordingId, LoggableId: refreshed.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1) stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
} }
@@ -187,9 +197,9 @@ func (s *recordingService) consumeRecordingDepletions(
if len(depletions) == 0 { if len(depletions) == 0 {
return nil return nil
} }
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for consuming recording depletions") s.Log.Errorf("FIFO v2 service is not available for consuming recording depletions")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available") return errors.New("stock log repository is not available")
@@ -210,27 +220,40 @@ func (s *recordingService) consumeRecordingDepletions(
} }
desired := depletion.Qty + depletion.PendingQty desired := depletion.Qty + depletion.PendingQty
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ if err := tx.WithContext(ctx).
UsableKey: recordingDepletionUsableKey, Model(&entity.RecordingDepletion{}).
UsableID: depletion.Id, Where("id = ?", depletion.Id).
ProductWarehouseID: sourceWarehouseID, Updates(map[string]any{
Quantity: desired, "qty": desired,
AllowPending: false, "usage_qty": desired,
Tx: tx, "pending_qty": 0,
}) }).Error; err != nil {
if err != nil { return err
s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, 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 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 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 logDecrease := refreshed.UsageQty
if result.PendingQuantity > 0 { if refreshed.PendingQty > 0 {
logDecrease += result.PendingQuantity logDecrease += refreshed.PendingQty
} }
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
log := &entity.StockLog{ log := &entity.StockLog{
@@ -238,7 +261,7 @@ func (s *recordingService) consumeRecordingDepletions(
CreatedBy: actorID, CreatedBy: actorID,
Decrease: logDecrease, Decrease: logDecrease,
LoggableType: string(utils.StockLogTypeRecording), LoggableType: string(utils.StockLogTypeRecording),
LoggableId: depletion.RecordingId, LoggableId: refreshed.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1) stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1)
@@ -258,20 +281,20 @@ func (s *recordingService) consumeRecordingDepletions(
} }
} }
destDelta := depletion.Qty + depletion.PendingQty destDelta := refreshed.Qty + refreshed.PendingQty
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 { if refreshed.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
if depletion.ProductWarehouseId == sourceWarehouseID { if refreshed.ProductWarehouseId == sourceWarehouseID {
continue continue
} }
log := &entity.StockLog{ log := &entity.StockLog{
ProductWarehouseId: depletion.ProductWarehouseId, ProductWarehouseId: refreshed.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
Increase: destDelta, Increase: destDelta,
LoggableType: string(utils.StockLogTypeRecording), LoggableType: string(utils.StockLogTypeRecording),
LoggableId: depletion.RecordingId, LoggableId: refreshed.RecordingId,
Notes: note, Notes: note,
} }
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1) stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
} }
@@ -302,9 +325,9 @@ func (s *recordingService) releaseRecordingStocks(
if len(stocks) == 0 { if len(stocks) == 0 {
return nil return nil
} }
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for releasing recording stocks") s.Log.Errorf("FIFO v2 service is not available for releasing recording stocks")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available") return errors.New("stock log repository is not available")
@@ -314,45 +337,35 @@ func (s *recordingService) releaseRecordingStocks(
if stock.Id == 0 { if stock.Id == 0 {
continue continue
} }
if stock.UsageQty != nil && *stock.UsageQty > 0 {
activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id) currentUsage := 0.0
if err != nil { if stock.UsageQty != nil {
return err currentUsage = *stock.UsageQty
}
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
}
} }
s.logStockTrace("release:start", stock, "") 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 { if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
return err 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, "") 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{ log := &entity.StockLog{
ProductWarehouseId: stock.ProductWarehouseId, ProductWarehouseId: stock.ProductWarehouseId,
CreatedBy: actorID, CreatedBy: actorID,
Increase: *stock.UsageQty, Increase: currentUsage,
LoggableType: string(utils.StockLogTypeRecording), LoggableType: string(utils.StockLogTypeRecording),
LoggableId: stock.RecordingId, LoggableId: stock.RecordingId,
Notes: note, Notes: note,
@@ -388,9 +401,9 @@ func (s *recordingService) releaseRecordingDepletions(
if len(depletions) == 0 { if len(depletions) == 0 {
return nil return nil
} }
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for releasing recording depletions") s.Log.Errorf("FIFO v2 service is not available for releasing recording depletions")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available") return errors.New("stock log repository is not available")
@@ -400,36 +413,7 @@ func (s *recordingService) releaseRecordingDepletions(
if depletion.Id == 0 { if depletion.Id == 0 {
continue 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, "") 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) sourceWarehouseID := uint(0)
if depletion.SourceProductWarehouseId != nil { if depletion.SourceProductWarehouseId != nil {
@@ -438,24 +422,49 @@ func (s *recordingService) releaseRecordingDepletions(
if sourceWarehouseID == 0 { if sourceWarehouseID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion") return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
} }
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingDepletionUsableKey, logIncrease := depletion.Qty + depletion.PendingQty
UsableID: depletion.Id, destDelta := depletion.Qty + depletion.PendingQty
Tx: tx,
}); err != nil { if err := tx.WithContext(ctx).
s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err) Model(&entity.RecordingDepletion{}).
Where("id = ?", depletion.Id).
Updates(map[string]any{
"qty": 0,
"usage_qty": 0,
"pending_qty": 0,
}).Error; err != nil {
return err 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 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, "") s.logDepletionTrace("release:done", depletion, "")
logIncrease := depletion.Qty
if depletion.PendingQty > 0 {
logIncrease += depletion.PendingQty
}
if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 { if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
log := &entity.StockLog{ log := &entity.StockLog{
ProductWarehouseId: sourceWarehouseID, 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 != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
if depletion.ProductWarehouseId == sourceWarehouseID { if depletion.ProductWarehouseId == sourceWarehouseID {
continue continue
@@ -618,9 +626,9 @@ func (s *recordingService) replenishRecordingEggs(
if len(eggs) == 0 { if len(eggs) == 0 {
return nil return nil
} }
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for replenishing recording eggs") s.Log.Errorf("FIFO v2 service is not available for replenishing recording eggs")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil { if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
return errors.New("stock log repository is not available") return errors.New("stock log repository is not available")
@@ -631,14 +639,23 @@ func (s *recordingService) replenishRecordingEggs(
continue continue
} }
s.logEggTrace("replenish:start", egg, "") s.logEggTrace("replenish:start", egg, "")
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyRecordingEgg, if err := tx.WithContext(ctx).
StockableID: egg.Id, Model(&entity.RecordingEgg{}).
ProductWarehouseID: egg.ProductWarehouseId, Where("id = ?", egg.Id).
Quantity: float64(egg.Qty), Update("total_qty", float64(egg.Qty)).Error; err != nil {
Tx: tx, return err
}); err != nil { }
s.Log.Errorf("Failed to replenish FIFO stock for recording egg %d: %+v", egg.Id, 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 return err
} }
s.logEggTrace("replenish:done", egg, "") s.logEggTrace("replenish:done", egg, "")
@@ -681,9 +698,9 @@ func (s *recordingService) replenishRecordingDepletions(
if len(depletions) == 0 { if len(depletions) == 0 {
return nil return nil
} }
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for replenishing recording depletions") s.Log.Errorf("FIFO v2 service is not available for replenishing recording depletions")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
for _, depletion := range depletions { for _, depletion := range depletions {
@@ -691,14 +708,16 @@ func (s *recordingService) replenishRecordingDepletions(
continue continue
} }
s.logDepletionTrace("replenish:start", depletion, "") s.logDepletionTrace("replenish:start", depletion, "")
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ if err := s.reflowRecordingScope(
StockableKey: fifo.StockableKeyRecordingDepletion, ctx,
StockableID: depletion.Id, tx,
ProductWarehouseID: depletion.ProductWarehouseId, depletion.ProductWarehouseId,
Quantity: depletion.Qty, depletion.RecordingId,
Tx: tx, recordingLaneStockable,
}); err != nil { recordingFunctionDepletionIn,
s.Log.Errorf("Failed to replenish FIFO stock for recording depletion %d: %+v", depletion.Id, err) recordingSourceDepletions,
); err != nil {
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording depletion %d: %+v", depletion.Id, err)
return err return err
} }
s.logDepletionTrace("replenish:done", depletion, "") s.logDepletionTrace("replenish:done", depletion, "")
@@ -715,9 +734,9 @@ func (s *recordingService) reduceRecordingDepletions(
if len(depletions) == 0 { if len(depletions) == 0 {
return nil return nil
} }
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for reducing recording depletions") s.Log.Errorf("FIFO v2 service is not available for reducing recording depletions")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
for _, depletion := range depletions { for _, depletion := range depletions {
@@ -725,16 +744,44 @@ func (s *recordingService) reduceRecordingDepletions(
continue continue
} }
s.logDepletionTrace("reduce:start", depletion, "") s.logDepletionTrace("reduce:start", depletion, "")
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: fifo.StockableKeyRecordingDepletion, if err := tx.WithContext(ctx).
StockableID: depletion.Id, Model(&entity.RecordingDepletion{}).
ProductWarehouseID: depletion.ProductWarehouseId, Where("id = ?", depletion.Id).
Quantity: -depletion.Qty, Updates(map[string]any{
Tx: tx, "qty": 0,
}); err != nil { "usage_qty": 0,
s.Log.Errorf("Failed to reduce FIFO stock for recording depletion %d: %+v", depletion.Id, err) "pending_qty": 0,
}).Error; err != nil {
return err 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, "") s.logDepletionTrace("reduce:done", depletion, "")
} }
@@ -749,9 +796,9 @@ func (s *recordingService) reduceRecordingEggs(
if len(eggs) == 0 { if len(eggs) == 0 {
return nil return nil
} }
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for reducing recording eggs") s.Log.Errorf("FIFO v2 service is not available for reducing recording eggs")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
for _, egg := range eggs { for _, egg := range eggs {
@@ -759,14 +806,22 @@ func (s *recordingService) reduceRecordingEggs(
continue continue
} }
s.logEggTrace("reduce:start", egg, "") s.logEggTrace("reduce:start", egg, "")
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{ if err := tx.WithContext(ctx).
StockableKey: fifo.StockableKeyRecordingEgg, Model(&entity.RecordingEgg{}).
StockableID: egg.Id, Where("id = ?", egg.Id).
ProductWarehouseID: egg.ProductWarehouseId, Update("total_qty", 0).Error; err != nil {
Quantity: -float64(egg.Qty), return err
Tx: tx, }
}); err != nil { if err := s.reflowRecordingScope(
s.Log.Errorf("Failed to reduce FIFO stock for recording egg %d: %+v", egg.Id, err) 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 return err
} }
s.logEggTrace("reduce:done", egg, "") s.logEggTrace("reduce:done", egg, "")
@@ -934,9 +989,9 @@ func (s *recordingService) syncRecordingStocks(
note string, note string,
actorID uint, actorID uint,
) error { ) error {
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for syncing recording stocks") s.Log.Errorf("FIFO v2 service is not available for syncing recording stocks")
return errors.New("fifo service is not available") return errors.New("fifo v2 service is not available")
} }
existingByWarehouse := make(map[uint][]entity.RecordingStock) existingByWarehouse := make(map[uint][]entity.RecordingStock)
@@ -1125,9 +1180,9 @@ func (s *recordingService) rollbackRecordingInventory(ctx context.Context, tx *g
} }
func (s *recordingService) requireFIFO() error { func (s *recordingService) requireFIFO() error {
if s.FifoSvc == nil { if s.FifoStockV2Svc == nil {
s.Log.Errorf("FIFO service is not available for recording operations") s.Log.Errorf("FIFO v2 service is not available for recording operations")
return fiber.NewError(fiber.StatusInternalServerError, "FIFO service is required for recording operations") return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is required for recording operations")
} }
return nil return nil
} }
+2 -16
View File
@@ -23,7 +23,6 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -40,7 +39,6 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
expenseRepository := expenseRepo.NewExpenseRepository(db) expenseRepository := expenseRepo.NewExpenseRepository(db)
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db) expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
projectFlockKandangRepository := projectFlockKandangRepo.NewProjectFlockKandangRepository(db) projectFlockKandangRepository := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
@@ -73,19 +71,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
expenseServiceInstance, expenseServiceInstance,
) )
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, 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"},
})
purchaseService := service.NewPurchaseService( purchaseService := service.NewPurchaseService(
validate, validate,
@@ -97,7 +83,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockKandangRepository, projectFlockKandangRepository,
approvalService, approvalService,
expenseBridge, expenseBridge,
fifoService, fifoStockV2Service,
documentSvc, 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 ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
ExpenseBridge PurchaseExpenseBridge ExpenseBridge PurchaseExpenseBridge
FifoSvc commonSvc.FifoService FifoStockV2Svc commonSvc.FifoStockV2Service
DocumentSvc commonSvc.DocumentService DocumentSvc commonSvc.DocumentService
approvalWorkflow approvalutils.ApprovalWorkflowKey approvalWorkflow approvalutils.ApprovalWorkflowKey
} }
@@ -77,7 +77,7 @@ func NewPurchaseService(
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
expenseBridge PurchaseExpenseBridge, expenseBridge PurchaseExpenseBridge,
fifoSvc commonSvc.FifoService, fifoStockV2Svc commonSvc.FifoStockV2Service,
documentSvc commonSvc.DocumentService, documentSvc commonSvc.DocumentService,
) PurchaseService { ) PurchaseService {
return &purchaseService{ return &purchaseService{
@@ -91,7 +91,7 @@ func NewPurchaseService(
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
ExpenseBridge: expenseBridge, ExpenseBridge: expenseBridge,
FifoSvc: fifoSvc, FifoStockV2Svc: fifoStockV2Svc,
DocumentSvc: documentSvc, DocumentSvc: documentSvc,
approvalWorkflow: utils.ApprovalWorkflowPurchase, approvalWorkflow: utils.ApprovalWorkflowPurchase,
} }
@@ -1026,22 +1026,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx) pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx) stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
deltas := make(map[uint]float64)
affected := make(map[uint]struct{}) affected := make(map[uint]struct{})
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared)) updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared)) priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared))
totalQtyDeltas := make(map[uint]float64) totalQtyDeltas := make(map[uint]float64)
fifoAdds := make([]struct { reflowAsOfByPW := make(map[uint]time.Time)
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{})
logEntries := make([]struct { logEntries := make([]struct {
itemID uint itemID uint
pwID uint pwID uint
@@ -1083,35 +1072,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
delta float64 delta float64
}{itemID: item.Id, pwID: *newPWID, delta: deltaQty}) }{itemID: item.Id, pwID: *newPWID, delta: deltaQty})
} }
switch { if newPWID != nil {
case deltaQty > 0 && newPWID != nil: assignEarliestAsOf(reflowAsOfByPW, *newPWID, prep.receivedDate.UTC())
if s.FifoSvc != nil { }
fifoAdds = append(fifoAdds, struct { if deltaQty != 0 {
itemID uint totalQtyDeltas[item.Id] += deltaQty
pwID uint }
qty float64 if deltaQty < 0 && newPWID != nil {
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty}) affected[*newPWID] = struct{}{}
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{}{}
} }
dateCopy := prep.receivedDate dateCopy := prep.receivedDate
@@ -1147,10 +1115,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return err return err
} }
if err := pwRepoTx.AdjustQuantities(c.Context(), deltas, nil); err != nil {
return err
}
if len(priceUpdates) > 0 { if len(priceUpdates) > 0 {
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil { if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
return err return err
@@ -1180,48 +1144,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
} }
} }
if s.FifoSvc != nil { if len(reflowAsOfByPW) > 0 {
for _, adj := range fifoAdds { if s.FifoStockV2Svc == nil {
if adj.pwID == 0 || adj.qty <= 0 { return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
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
}
} }
for _, adj := range fifoSubs { for pwID, asOf := range reflowAsOfByPW {
if adj.pwID == 0 || adj.qty >= 0 { asOfCopy := asOf
continue if err := reflowPurchaseScope(c.Context(), s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
}
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 {
return err 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 { if len(logEntries) > 0 {
@@ -1577,10 +1509,9 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
return nil return nil
} }
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx) stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
deltas := make(map[uint]float64)
affected := make(map[uint]struct{}) affected := make(map[uint]struct{})
reflowAsOfByPW := make(map[uint]time.Time)
logEntries := make([]struct { logEntries := make([]struct {
pwID uint pwID uint
qty float64 qty float64
@@ -1596,42 +1527,43 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
pwID := *item.ProductWarehouseId pwID := *item.ProductWarehouseId
qty := item.TotalQty qty := item.TotalQty
if s.FifoSvc != nil { if err := tx.WithContext(ctx).
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{ Model(&entity.PurchaseItem{}).
StockableKey: fifo.StockableKeyPurchaseItems, Where("id = ?", item.Id).
StockableID: item.Id, Update("total_qty", 0).Error; err != nil {
ProductWarehouseID: pwID, return err
Quantity: -qty,
Tx: tx,
}); err != nil {
return err
}
logEntries = append(logEntries, struct {
pwID uint
qty float64
}{pwID: pwID, qty: qty})
continue
} }
deltas[pwID] -= qty
affected[pwID] = struct{}{} affected[pwID] = struct{}{}
if item.ReceivedDate != nil {
assignEarliestAsOf(reflowAsOfByPW, pwID, item.ReceivedDate.UTC())
} else {
assignEarliestAsOf(reflowAsOfByPW, pwID, time.Now().UTC())
}
logEntries = append(logEntries, struct { logEntries = append(logEntries, struct {
pwID uint pwID uint
qty float64 qty float64
}{pwID: pwID, qty: qty}) }{pwID: pwID, qty: qty})
} }
if s.FifoSvc == nil && len(deltas) > 0 { if len(reflowAsOfByPW) > 0 {
if err := pwRepoTx.AdjustQuantities(ctx, deltas, nil); err != nil { if s.FifoStockV2Svc == nil {
return err return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
} }
if len(affected) > 0 { for pwID, asOf := range reflowAsOfByPW {
if err := pwRepoTx.CleanupEmpty(ctx, affected); err != nil { asOfCopy := asOf
if err := reflowPurchaseScope(ctx, s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
return err 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 { if strings.TrimSpace(note) != "" && actorID != 0 && len(logEntries) > 0 {
logs := make([]*entity.StockLog, 0, len(logEntries)) logs := make([]*entity.StockLog, 0, len(logEntries))
for _, entry := range logEntries { for _, entry := range logEntries {