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
@@ -0,0 +1,93 @@
package service
import (
"context"
"fmt"
"strings"
"time"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
const (
purchaseInFunctionCode = "PURCHASE_IN"
purchaseStockableLane = "STOCKABLE"
purchaseSourceTable = "purchase_items"
)
func reflowPurchaseScope(
ctx context.Context,
fifoStockV2Svc commonSvc.FifoStockV2Service,
tx *gorm.DB,
productWarehouseID uint,
asOf *time.Time,
) error {
if fifoStockV2Svc == nil {
return fmt.Errorf("FIFO v2 service is not available")
}
if productWarehouseID == 0 {
return fmt.Errorf("product warehouse id is required")
}
flagGroupCode, err := resolvePurchaseFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
if err != nil {
return err
}
if strings.TrimSpace(flagGroupCode) == "" {
return fmt.Errorf("flag group code is not found for product warehouse %d", productWarehouseID)
}
_, err = fifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode,
ProductWarehouseID: productWarehouseID,
AsOf: asOf,
Tx: tx,
})
return err
}
func resolvePurchaseFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
type row struct {
FlagGroupCode string `gorm:"column:flag_group_code"`
}
var selected row
err := tx.WithContext(ctx).
Table("fifo_stock_v2_route_rules rr").
Select("rr.flag_group_code").
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("rr.is_active = TRUE").
Where("rr.lane = ?", purchaseStockableLane).
Where("rr.function_code = ?", purchaseInFunctionCode).
Where("rr.source_table = ?", purchaseSourceTable).
Where(`
EXISTS (
SELECT 1
FROM product_warehouses pw
JOIN flags f ON f.flagable_id = pw.product_id
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE pw.id = ?
AND f.flagable_type = ?
AND fm.flag_group_code = rr.flag_group_code
)
`, productWarehouseID, entity.FlagableTypeProduct).
Order("rr.id ASC").
Limit(1).
Take(&selected).Error
if err != nil {
return "", err
}
return strings.TrimSpace(selected.FlagGroupCode), nil
}
func assignEarliestAsOf(m map[uint]time.Time, productWarehouseID uint, asOf time.Time) {
if productWarehouseID == 0 {
return
}
if current, ok := m[productWarehouseID]; !ok || asOf.Before(current) {
m[productWarehouseID] = asOf
}
}
@@ -57,7 +57,7 @@ type purchaseService struct {
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
ExpenseBridge PurchaseExpenseBridge
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
DocumentSvc commonSvc.DocumentService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
@@ -77,7 +77,7 @@ func NewPurchaseService(
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
approvalSvc commonSvc.ApprovalService,
expenseBridge PurchaseExpenseBridge,
fifoSvc commonSvc.FifoService,
fifoStockV2Svc commonSvc.FifoStockV2Service,
documentSvc commonSvc.DocumentService,
) PurchaseService {
return &purchaseService{
@@ -91,7 +91,7 @@ func NewPurchaseService(
ProjectFlockKandangRepo: projectFlockKandangRepo,
ApprovalSvc: approvalSvc,
ExpenseBridge: expenseBridge,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
DocumentSvc: documentSvc,
approvalWorkflow: utils.ApprovalWorkflowPurchase,
}
@@ -1026,22 +1026,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
deltas := make(map[uint]float64)
affected := make(map[uint]struct{})
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared))
totalQtyDeltas := make(map[uint]float64)
fifoAdds := make([]struct {
itemID uint
pwID uint
qty float64
}, 0, len(prepared))
fifoSubs := make([]struct {
itemID uint
pwID uint
qty float64
}, 0, len(prepared))
resolvePendingIDs := make(map[uint]struct{})
reflowAsOfByPW := make(map[uint]time.Time)
logEntries := make([]struct {
itemID uint
pwID uint
@@ -1083,35 +1072,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
delta float64
}{itemID: item.Id, pwID: *newPWID, delta: deltaQty})
}
switch {
case deltaQty > 0 && newPWID != nil:
if s.FifoSvc != nil {
fifoAdds = append(fifoAdds, struct {
itemID uint
pwID uint
qty float64
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
resolvePendingIDs[*newPWID] = struct{}{}
} else {
deltas[*newPWID] += deltaQty
totalQtyDeltas[item.Id] += deltaQty
}
case deltaQty < 0 && newPWID != nil:
if s.FifoSvc != nil {
fifoSubs = append(fifoSubs, struct {
itemID uint
pwID uint
qty float64
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
affected[*newPWID] = struct{}{}
resolvePendingIDs[*newPWID] = struct{}{}
} else {
deltas[*newPWID] += deltaQty // negative
affected[*newPWID] = struct{}{}
totalQtyDeltas[item.Id] += deltaQty
}
case newPWID != nil:
resolvePendingIDs[*newPWID] = struct{}{}
if newPWID != nil {
assignEarliestAsOf(reflowAsOfByPW, *newPWID, prep.receivedDate.UTC())
}
if deltaQty != 0 {
totalQtyDeltas[item.Id] += deltaQty
}
if deltaQty < 0 && newPWID != nil {
affected[*newPWID] = struct{}{}
}
dateCopy := prep.receivedDate
@@ -1147,10 +1115,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return err
}
if err := pwRepoTx.AdjustQuantities(c.Context(), deltas, nil); err != nil {
return err
}
if len(priceUpdates) > 0 {
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
return err
@@ -1180,48 +1144,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
}
}
if s.FifoSvc != nil {
for _, adj := range fifoAdds {
if adj.pwID == 0 || adj.qty <= 0 {
continue
}
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyPurchaseItems,
StockableID: adj.itemID,
ProductWarehouseID: adj.pwID,
Quantity: adj.qty,
Tx: tx,
}); err != nil {
return err
}
if len(reflowAsOfByPW) > 0 {
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
for _, adj := range fifoSubs {
if adj.pwID == 0 || adj.qty >= 0 {
continue
}
if err := s.FifoSvc.AdjustStockableQuantity(c.Context(), commonSvc.StockAdjustRequest{
StockableKey: fifo.StockableKeyPurchaseItems,
StockableID: adj.itemID,
ProductWarehouseID: adj.pwID,
Quantity: adj.qty,
Tx: tx,
}); err != nil {
for pwID, asOf := range reflowAsOfByPW {
asOfCopy := asOf
if err := reflowPurchaseScope(c.Context(), s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
return err
}
}
for pwID := range resolvePendingIDs {
if pwID == 0 {
continue
}
resolved, err := s.FifoSvc.ResolvePending(c.Context(), commonSvc.PendingResolveRequest{
ProductWarehouseID: pwID,
Tx: tx,
})
if err != nil {
return err
}
s.Log.Infof("ResolvePending purchase=%d pw=%d resolved=%d", purchase.Id, pwID, len(resolved))
}
}
if len(logEntries) > 0 {
@@ -1577,10 +1509,9 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
return nil
}
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
deltas := make(map[uint]float64)
affected := make(map[uint]struct{})
reflowAsOfByPW := make(map[uint]time.Time)
logEntries := make([]struct {
pwID uint
qty float64
@@ -1596,42 +1527,43 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
pwID := *item.ProductWarehouseId
qty := item.TotalQty
if s.FifoSvc != nil {
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
StockableKey: fifo.StockableKeyPurchaseItems,
StockableID: item.Id,
ProductWarehouseID: pwID,
Quantity: -qty,
Tx: tx,
}); err != nil {
return err
}
logEntries = append(logEntries, struct {
pwID uint
qty float64
}{pwID: pwID, qty: qty})
continue
if err := tx.WithContext(ctx).
Model(&entity.PurchaseItem{}).
Where("id = ?", item.Id).
Update("total_qty", 0).Error; err != nil {
return err
}
deltas[pwID] -= qty
affected[pwID] = struct{}{}
if item.ReceivedDate != nil {
assignEarliestAsOf(reflowAsOfByPW, pwID, item.ReceivedDate.UTC())
} else {
assignEarliestAsOf(reflowAsOfByPW, pwID, time.Now().UTC())
}
logEntries = append(logEntries, struct {
pwID uint
qty float64
}{pwID: pwID, qty: qty})
}
if s.FifoSvc == nil && len(deltas) > 0 {
if err := pwRepoTx.AdjustQuantities(ctx, deltas, nil); err != nil {
return err
if len(reflowAsOfByPW) > 0 {
if s.FifoStockV2Svc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
}
if len(affected) > 0 {
if err := pwRepoTx.CleanupEmpty(ctx, affected); err != nil {
for pwID, asOf := range reflowAsOfByPW {
asOfCopy := asOf
if err := reflowPurchaseScope(ctx, s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
return err
}
}
}
if len(affected) > 0 {
if err := rProductWarehouse.NewProductWarehouseRepository(tx).CleanupEmpty(ctx, affected); err != nil {
return err
}
}
if strings.TrimSpace(note) != "" && actorID != 0 && len(logEntries) > 0 {
logs := make([]*entity.StockLog, 0, len(logEntries))
for _, entry := range logEntries {