mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 15:25:43 +00:00
Fix logic recording transition
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user