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 -22
View File
@@ -2,7 +2,6 @@ package marketing
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -20,7 +19,6 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
)
type MarketingModule struct{}
@@ -35,24 +33,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockLogRepo := rShared.NewStockLogRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyMarketingDelivery,
Table: "marketing_delivery_products",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register marketing delivery usable workflow: %v", err))
}
}
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
@@ -64,8 +45,8 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate)
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoStockV2Service, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoStockV2Service, validate)
userService := sUser.NewUserService(userRepo, validate)
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
@@ -15,7 +15,6 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -36,7 +35,7 @@ type deliveryOrdersService struct {
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
StockLogRepo rShared.StockLogRepository
ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
}
func NewDeliveryOrdersService(
@@ -45,7 +44,7 @@ func NewDeliveryOrdersService(
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
stockLogRepo rShared.StockLogRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
fifoStockV2Svc commonSvc.FifoStockV2Service,
validate *validator.Validate,
) DeliveryOrdersService {
return &deliveryOrdersService{
@@ -55,7 +54,7 @@ func NewDeliveryOrdersService(
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
StockLogRepo: stockLogRepo,
ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
}
}
@@ -549,33 +548,42 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
ProductWarehouseID: marketingProduct.ProductWarehouseId,
Quantity: requestedQty,
AllowPending: false,
Tx: tx,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
previousUsage := deliveryProduct.UsageQty
deliveryProduct.UsageQty = requestedQty
deliveryProduct.PendingQty = 0
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil {
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
}
if actorID > 0 && result.UsageQuantity > 0 {
if err := reflowMarketingScope(
ctx,
s.FifoStockV2Svc,
tx,
marketingProduct.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
}
deliveryProduct.UsageQty = refreshed.UsageQty
deliveryProduct.PendingQty = refreshed.PendingQty
deliveryProduct.CreatedAt = refreshed.CreatedAt
allocatedDelta := deliveryProduct.UsageQty - previousUsage
if actorID > 0 && allocatedDelta > 0 {
decreaseLog := &entity.StockLog{
Decrease: result.UsageQuantity,
Decrease: allocatedDelta,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity),
Notes: fmt.Sprintf("FIFO v2 reflow consume (%.2f)", allocatedDelta),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
@@ -604,35 +612,45 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
}
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
if err != nil {
currentUsage = 0
}
if currentUsage == 0 {
currentUsage := deliveryProduct.UsageQty
currentPending := deliveryProduct.PendingQty
if currentUsage <= 0 && currentPending <= 0 {
return nil
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: tx,
}); err != nil {
return err
deliveryProduct.UsageQty = 0
deliveryProduct.PendingQty = 0
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset delivery product")
}
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
return err
if err := reflowMarketingScope(
ctx,
s.FifoStockV2Svc,
tx,
marketingProduct.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
}
if actorID > 0 && currentUsage > 0 {
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
}
deliveryProduct.UsageQty = refreshed.UsageQty
deliveryProduct.PendingQty = refreshed.PendingQty
deliveryProduct.CreatedAt = refreshed.CreatedAt
releasedUsage := currentUsage - deliveryProduct.UsageQty
if actorID > 0 && releasedUsage > 0 {
increaseLog := &entity.StockLog{
Increase: currentUsage,
Increase: releasedUsage,
LoggableType: string(utils.StockLogTypeMarketing),
LoggableId: deliveryProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage),
Notes: fmt.Sprintf("FIFO v2 reflow release (%.2f)", releasedUsage),
}
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
if err != nil {
@@ -0,0 +1,97 @@
package service
import (
"context"
"fmt"
"strings"
"time"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
const (
marketingOutFunctionCode = "MARKETING_OUT"
marketingUsableLane = "USABLE"
marketingSourceTable = "marketing_delivery_products"
)
func reflowMarketingScope(
ctx context.Context,
fifoStockV2Svc commonSvc.FifoStockV2Service,
tx *gorm.DB,
productWarehouseID uint,
asOf *time.Time,
) error {
if fifoStockV2Svc == nil {
return fmt.Errorf("FIFO v2 service is not available")
}
if productWarehouseID == 0 {
return fmt.Errorf("product warehouse id is required")
}
flagGroupCode, err := resolveMarketingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
if err != nil {
return err
}
if strings.TrimSpace(flagGroupCode) == "" {
return fmt.Errorf("flag group code is not found for product warehouse %d", productWarehouseID)
}
_, err = fifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode,
ProductWarehouseID: productWarehouseID,
AsOf: asOf,
Tx: tx,
})
return err
}
func resolveMarketingFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
type row struct {
FlagGroupCode string `gorm:"column:flag_group_code"`
}
var selected row
err := tx.WithContext(ctx).
Table("fifo_stock_v2_route_rules rr").
Select("rr.flag_group_code").
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("rr.is_active = TRUE").
Where("rr.lane = ?", marketingUsableLane).
Where("rr.function_code = ?", marketingOutFunctionCode).
Where("rr.source_table = ?", marketingSourceTable).
Where(`
EXISTS (
SELECT 1
FROM product_warehouses pw
JOIN flags f ON f.flagable_id = pw.product_id
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE pw.id = ?
AND f.flagable_type = ?
AND fm.flag_group_code = rr.flag_group_code
)
`, productWarehouseID, entity.FlagableTypeProduct).
Order("rr.id ASC").
Limit(1).
Take(&selected).Error
if err != nil {
return "", err
}
return strings.TrimSpace(selected.FlagGroupCode), nil
}
func resolveMarketingAsOf(deliveryDate, createdAt *time.Time) *time.Time {
if deliveryDate != nil {
asOf := *deliveryDate
return &asOf
}
if createdAt != nil {
asOf := *createdAt
return &asOf
}
asOf := time.Now()
return &asOf
}
@@ -20,7 +20,6 @@ import (
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -43,12 +42,12 @@ type salesOrdersService struct {
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
UserRepo userRepo.UserRepository
ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
FifoStockV2Svc commonSvc.FifoStockV2Service
WarehouseRepo warehouseRepo.WarehouseRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
}
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository,
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, warehouseRepo warehouseRepo.WarehouseRepository,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
return &salesOrdersService{
Log: utils.Log,
@@ -58,7 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome
ProductWarehouseRepo: productWarehouseRepo,
UserRepo: userRepo,
ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
FifoStockV2Svc: fifoStockV2Svc,
WarehouseRepo: warehouseRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
}
@@ -376,15 +375,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if qtyDiff < 0 {
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
} else if qtyDiff > 0 {
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
ProductWarehouseID: rp.ProductWarehouseId,
Quantity: qtyDiff,
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err))
nextRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty + qtyDiff
if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, nextRequestedQty, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing delivery fifo fields")
}
if err := reflowMarketingScope(
c.Context(),
s.FifoStockV2Svc,
dbTransaction,
rp.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
}
}
}
@@ -439,12 +441,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id))
}
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err))
if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, 0, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset marketing delivery fifo fields")
}
if err := reflowMarketingScope(
c.Context(),
s.FifoStockV2Svc,
dbTransaction,
deliveryProduct.ProductWarehouseId,
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
}
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
@@ -523,12 +530,17 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
if err == nil && len(deliveryProducts) > 0 {
for _, dp := range deliveryProducts {
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: dp.Id,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err))
if err := marketingDeliveryProductRepoTx.UpdateFifoFields(c.Context(), dp.Id, 0, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset fifo fields for delivery product %d", dp.Id))
}
if err := reflowMarketingScope(
c.Context(),
s.FifoStockV2Svc,
dbTransaction,
dp.ProductWarehouseId,
resolveMarketingAsOf(dp.DeliveryDate, dp.CreatedAt),
); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2 for delivery product %d: %v", dp.Id, err))
}
}
}