Fix logic recording transition

This commit is contained in:
ragilap
2026-03-10 17:02:45 +07:00
parent 3a8cc47fa0
commit 333cb9e136
13 changed files with 411 additions and 335 deletions
@@ -5,6 +5,7 @@ import (
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/config"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto"
@@ -308,7 +309,7 @@ func recordingWeekValue(e entity.Recording) int {
}
weekBase := 1
if isLayingRecording(e) {
weekBase = 18
weekBase = config.LayingWeekStart()
}
return ((day - 1) / 7) + weekBase
}
@@ -71,6 +71,7 @@ type RecordingRepository interface {
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error)
GetProjectFlockKandangIDsByPopulationWarehouseIDs(ctx context.Context, tx *gorm.DB, productWarehouseIDs []uint) ([]uint, error)
ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error
ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error)
}
@@ -874,6 +875,34 @@ func (r *RecordingRepositoryImpl) GetAverageTargetMetricsByProjectFlockKandangID
return result, nil
}
func (r *RecordingRepositoryImpl) GetProjectFlockKandangIDsByPopulationWarehouseIDs(
ctx context.Context,
tx *gorm.DB,
productWarehouseIDs []uint,
) ([]uint, error) {
if len(productWarehouseIDs) == 0 {
return nil, nil
}
db := r.DB().WithContext(ctx)
if tx != nil {
db = tx.WithContext(ctx)
}
var kandangIDs []uint
if err := db.Table("project_flock_populations pfp").
Select("DISTINCT pc.project_flock_kandang_id").
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("pfp.product_warehouse_id IN ?", productWarehouseIDs).
Where("pfp.deleted_at IS NULL").
Where("pc.deleted_at IS NULL").
Pluck("pc.project_flock_kandang_id", &kandangIDs).Error; err != nil {
return nil, err
}
return kandangIDs, nil
}
func (r *RecordingRepositoryImpl) ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
if projectFlockKandangID == 0 {
return nil
@@ -1039,11 +1039,75 @@ func (s *recordingService) evaluatePopulationMutationState(ctx context.Context,
populationCanChange := true
if category == strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)) {
populationCanChange = !(transferExecuted && !recordDate.Before(transferDate))
if transferExecuted && !recordDate.Before(transferDate) {
hasTargetLayingRecording, checkErr := s.hasAnyRecordingOnTransferTargets(ctx, transfer)
if checkErr != nil {
s.Log.Errorf("Failed to resolve target laying recording state for transfer %d: %+v", transfer.Id, checkErr)
return true, false, false, false, nil, time.Time{}, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi status transisi recording")
}
if hasTargetLayingRecording {
isTransition = false
isLaying = true
} else {
today := normalizeDateOnlyUTC(time.Now().UTC())
if !today.Before(economicCutoffDate) {
isTransition = true
isLaying = false
}
}
}
}
return populationCanChange, transferExecuted, isTransition, isLaying, transfer, transferDate, nil
}
func (s *recordingService) hasAnyRecordingOnTransferTargets(ctx context.Context, transfer *entity.LayingTransfer) (bool, error) {
if transfer == nil || transfer.Id == 0 {
return false, nil
}
targetIDs, err := s.transferTargetProjectFlockKandangIDs(ctx, transfer.Id)
if err != nil {
return false, err
}
if len(targetIDs) == 0 {
// Keep existing behavior for legacy or incomplete target mapping.
return true, nil
}
var count int64
err = s.Repository.DB().
WithContext(ctx).
Table("recordings").
Where("deleted_at IS NULL").
Where("project_flock_kandangs_id IN ?", targetIDs).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *recordingService) transferTargetProjectFlockKandangIDs(ctx context.Context, transferID uint) ([]uint, error) {
if transferID == 0 {
return nil, nil
}
var targetIDs []uint
err := s.Repository.DB().
WithContext(ctx).
Table("laying_transfer_targets").
Where("laying_transfer_id = ?", transferID).
Where("deleted_at IS NULL").
Pluck("target_project_flock_kandang_id", &targetIDs).Error
if err != nil {
return nil, err
}
return targetIDs, nil
}
func (s *recordingService) ensurePopulationMutationAllowed(ctx context.Context, recording *entity.Recording, operation string) error {
populationCanChange, _, _, _, transfer, transferDate, err := s.evaluatePopulationMutationState(ctx, recording)
if err != nil {
@@ -1091,19 +1155,16 @@ func (s *recordingService) ensureDepletionMutationAllowed(ctx context.Context, r
category = strings.ToUpper(strings.TrimSpace(pfk.ProjectFlock.Category))
}
if !shouldGuardDepletionMutation(category) {
return nil
}
var (
transfer *entity.LayingTransfer
err error
)
switch category {
case strings.ToUpper(string(utils.ProjectFlockCategoryGrowing)):
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
case strings.ToUpper(string(utils.ProjectFlockCategoryLaying)):
transfer, err = s.TransferLayingRepo.GetLatestApprovedByTargetKandang(ctx, recording.ProjectFlockKandangId)
default:
return nil
}
transfer, err = s.TransferLayingRepo.GetLatestApprovedBySourceKandang(ctx, recording.ProjectFlockKandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
@@ -1132,6 +1193,10 @@ func (s *recordingService) ensureDepletionMutationAllowed(ctx context.Context, r
)
}
func shouldGuardDepletionMutation(category string) bool {
return strings.EqualFold(strings.TrimSpace(category), string(utils.ProjectFlockCategoryGrowing))
}
func (s *recordingService) tryAutoExecuteTransferForRecordingCreate(c *fiber.Ctx, pfk *entity.ProjectFlockKandang, recordTime time.Time) error {
if pfk == nil || pfk.Id == 0 || s.TransferLayingRepo == nil || s.TransferLayingSvc == nil {
return nil
@@ -2026,10 +2091,7 @@ func (s *recordingService) reflowApplyRecordingStocks(
}
s.logStockTrace("reflow_apply:done", *refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desiredTotal, actualUsage, actualPending))
logDecrease := actualUsage
if actualPending > 0 {
logDecrease += actualPending
}
logDecrease := recordingStockRollbackQty(*refreshed)
if logDecrease > 0 && shouldWriteLog {
log := &entity.StockLog{
ProductWarehouseId: refreshed.ProductWarehouseId,
@@ -2073,11 +2135,8 @@ func (s *recordingService) reflowResetRecordingStocks(
continue
}
currentUsage := 0.0
if stock.UsageQty != nil {
currentUsage = *stock.UsageQty
}
s.logStockTrace("reflow_reset:start", stock, "")
rollbackQty := recordingStockRollbackQty(stock)
s.logStockTrace("reflow_reset:start", stock, fmt.Sprintf("rollback_qty=%.3f", rollbackQty))
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
return err
@@ -2094,13 +2153,13 @@ func (s *recordingService) reflowResetRecordingStocks(
s.Log.Errorf("Failed to reflow FIFO v2 rollback for recording stock %d: %+v", stock.Id, err)
return err
}
s.logStockTrace("reflow_reset:done", stock, "")
s.logStockTrace("reflow_reset:done", stock, fmt.Sprintf("rollback_qty=%.3f", rollbackQty))
if currentUsage > 0 && shouldWriteLog {
if rollbackQty > 0 && shouldWriteLog {
log := &entity.StockLog{
ProductWarehouseId: stock.ProductWarehouseId,
CreatedBy: actorID,
Increase: currentUsage,
Increase: rollbackQty,
LoggableType: string(utils.StockLogTypeRecording),
LoggableId: stock.RecordingId,
Notes: note,
@@ -2114,6 +2173,24 @@ func (s *recordingService) reflowResetRecordingStocks(
return nil
}
func recordingStockRollbackQty(stock entity.RecordingStock) float64 {
usage := 0.0
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
pending := 0.0
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
if usage < 0 {
usage = 0
}
if pending < 0 {
pending = 0
}
return usage + pending
}
type desiredStock struct {
Usage float64
Pending float64
@@ -2627,19 +2704,8 @@ func (s *recordingService) resyncPopulationUsageForDepletions(
}
if len(sourceWarehouseIDs) > 0 {
db := s.Repository.DB().WithContext(ctx)
if tx != nil {
db = tx.WithContext(ctx)
}
var sourceKandangIDs []uint
if err := db.Table("project_flock_populations pfp").
Select("DISTINCT pc.project_flock_kandang_id").
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("pfp.product_warehouse_id IN ?", sourceWarehouseIDs).
Where("pfp.deleted_at IS NULL").
Where("pc.deleted_at IS NULL").
Pluck("pc.project_flock_kandang_id", &sourceKandangIDs).Error; err != nil {
sourceKandangIDs, err := s.Repository.GetProjectFlockKandangIDsByPopulationWarehouseIDs(ctx, tx, sourceWarehouseIDs)
if err != nil {
return err
}
@@ -2651,62 +2717,7 @@ func (s *recordingService) resyncPopulationUsageForDepletions(
}
for kandangID := range kandangIDs {
if err := s.resyncPopulationUsageByProjectFlockKandang(ctx, tx, kandangID); err != nil {
return err
}
}
return nil
}
func (s *recordingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
if projectFlockKandangID == 0 {
return nil
}
db := s.Repository.DB().WithContext(ctx)
if tx != nil {
db = tx.WithContext(ctx)
}
var populationIDs []uint
if err := db.Table("project_flock_populations pfp").
Select("pfp.id").
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
Pluck("pfp.id", &populationIDs).Error; err != nil {
return err
}
if len(populationIDs) == 0 {
return nil
}
type usageRow struct {
StockableID uint `gorm:"column:stockable_id"`
Used float64 `gorm:"column:used"`
}
var usageRows []usageRow
if err := db.Table("stock_allocations").
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
Where("stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
Where("status = ?", entity.StockAllocationStatusActive).
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
Where("stockable_id IN ?", populationIDs).
Group("stockable_id").
Scan(&usageRows).Error; err != nil {
return err
}
if err := db.Model(&entity.ProjectFlockPopulation{}).
Where("id IN ?", populationIDs).
Update("total_used_qty", 0).Error; err != nil {
return err
}
for _, row := range usageRows {
if err := db.Model(&entity.ProjectFlockPopulation{}).
Where("id = ?", row.StockableID).
Update("total_used_qty", row.Used).Error; err != nil {
if err := s.Repository.ResyncProjectFlockPopulationUsage(ctx, tx, kandangID); err != nil {
return err
}
}