diff --git a/internal/common/service/fifo_stock_v2/allocate.go b/internal/common/service/fifo_stock_v2/allocate.go index 33c3887b..4d38dab3 100644 --- a/internal/common/service/fifo_stock_v2/allocate.go +++ b/internal/common/service/fifo_stock_v2/allocate.go @@ -496,10 +496,6 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re 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 diff --git a/internal/config/config.go b/internal/config/config.go index 95307f00..f891e0ba 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -259,6 +259,10 @@ func defaultString(v, def string) string { return v } +func LayingWeekStart() int { + return TransferToLayingGrowingMaxWeek +} + func joinPath(parts ...string) string { out := make([]string, 0, len(parts)) for _, part := range parts { diff --git a/internal/modules/master/production-standards/services/production-standard.service.go b/internal/modules/master/production-standards/services/production-standard.service.go index c2841708..406dccdc 100644 --- a/internal/modules/master/production-standards/services/production-standard.service.go +++ b/internal/modules/master/production-standards/services/production-standard.service.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" @@ -343,17 +344,22 @@ func (s productionStandardService) EnsureWeekStart(ctx context.Context, standard return nil } + layingWeekStart := config.LayingWeekStart() + switch strings.ToUpper(category) { case string(utils.ProjectFlockCategoryLaying): details, err := s.ProductionStandardDetailRepo.GetByProductionStandardID(ctx, standardID) if err != nil { return err } - startWeek := 0 - if len(details) > 0 { - startWeek = details[0].Week + if len(details) == 0 { + return fiber.NewError( + fiber.StatusBadRequest, + "Standart production tidak tersedia untuk kategori laying", + ) } - if startWeek != 18 { + startWeek := details[0].Week + if startWeek > layingWeekStart { return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") } case string(utils.ProjectFlockCategoryGrowing): @@ -361,10 +367,13 @@ func (s productionStandardService) EnsureWeekStart(ctx context.Context, standard if err != nil { return err } - startWeek := 0 - if len(details) > 0 { - startWeek = details[0].Week + if len(details) == 0 { + return fiber.NewError( + fiber.StatusBadRequest, + "Standart production tidak tersedia untuk kategori growing", + ) } + startWeek := details[0].Week if startWeek != 1 { return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") } @@ -381,7 +390,7 @@ func (s productionStandardService) EnsureWeekAvailable(ctx context.Context, stan upperCategory := strings.ToUpper(category) weekBase := 1 if upperCategory == string(utils.ProjectFlockCategoryLaying) { - weekBase = 18 + weekBase = config.LayingWeekStart() } week := ((day - 1) / 7) + weekBase if week <= 0 { diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 8a4b0d09..16d51a2d 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -3,6 +3,7 @@ package dto import ( "time" + "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" areaRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" flockRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" @@ -35,13 +36,13 @@ type ChickinRelationDTO struct { } type ProjectFlockDTO struct { - Id uint `json:"id"` - Period int `json:"period"` - Category string `json:"category"` - Flock *flockRelationDTO.FlockRelationDTO `json:"flock"` - Area *areaRelationDTO.AreaRelationDTO `json:"area"` - StandardFcr *float64 `json:"standard_fcr"` - Location *locationRelationDTO.LocationRelationDTO `json:"location"` + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` + Flock *flockRelationDTO.FlockRelationDTO `json:"flock"` + Area *areaRelationDTO.AreaRelationDTO `json:"area"` + StandardFcr *float64 `json:"standard_fcr"` + Location *locationRelationDTO.LocationRelationDTO `json:"location"` } type ProjectFlockKandangDTO struct { @@ -123,13 +124,13 @@ func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO { location = &mapped } return ProjectFlockDTO{ - Id: e.Id, - Period: pfk.Period, - Category: e.Category, - Flock: flock, - Area: area, + Id: e.Id, + Period: pfk.Period, + Category: e.Category, + Flock: flock, + Area: area, StandardFcr: resolveProjectFlockStandardFcr(e), - Location: location, + Location: location, } } @@ -219,7 +220,7 @@ func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 { } week := 1 if e.Category == string(utils.ProjectFlockCategoryLaying) { - week = 18 + week = config.LayingWeekStart() } for _, detail := range e.ProductionStandard.ProductionStandardDetails { if detail.Week == week && detail.StandardFCR != nil { diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index a1335598..1a30a058 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -6,6 +6,7 @@ import ( "math" "strconv" "strings" + "time" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" @@ -272,10 +273,20 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { projectFlockId := c.QueryInt("project_flock_id", 0) kandangId := c.QueryInt("kandang_id", 0) withPopulation := c.QueryBool("withpopulation", false) + recordDateRaw := strings.TrimSpace(c.Query("record_date", "")) + var recordDate *time.Time if projectFlockId == 0 || kandangId == 0 { return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id") } + if recordDateRaw != "" { + parsed, err := time.Parse("2006-01-02", recordDateRaw) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format") + } + utc := parsed.UTC() + recordDate = &utc + } result, availableStock, err := u.ProjectflockService.GetProjectFlockKandangByProjectAndKandang(c, uint(projectFlockId), uint(kandangId)) if err != nil { @@ -300,7 +311,7 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse) dtoResult.Warehouse = &mapped } - if isTransition, isLaying, serr := u.ProjectflockService.GetProjectFlockKandangTransferState(c, result.Id); serr != nil { + if isTransition, isLaying, serr := u.ProjectflockService.GetProjectFlockKandangTransferStateAtDate(c, result.Id, recordDate); serr != nil { return serr } else { dtoResult.IsTransition = isTransition diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index e7240b49..7354158b 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -3,6 +3,7 @@ package dto import ( "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" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" @@ -25,17 +26,17 @@ type ProjectFlockRelationDTO struct { type ProjectFlockListDTO struct { ProjectFlockRelationDTO - Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` - Category string `json:"category"` - StandardFcr *float64 `json:"standard_fcr,omitempty"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Category string `json:"category"` + StandardFcr *float64 `json:"standard_fcr,omitempty"` ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` - Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` - Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` - ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval approvalDTO.ApprovalRelationDTO `json:"approval"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` + ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval approvalDTO.ApprovalRelationDTO `json:"approval"` } type KandangWithProjectFlockIdDTO struct { @@ -203,7 +204,7 @@ func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 { } week := 1 if e.Category == string(utils.ProjectFlockCategoryLaying) { - week = 18 + week = config.LayingWeekStart() } for _, detail := range e.ProductionStandard.ProductionStandardDetails { if detail.Week == week && detail.StandardFCR != nil { diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 9007fd1b..f7812d0c 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -46,6 +46,7 @@ type ProjectflockService interface { GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, projectFlockKandangID uint) (*time.Time, error) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error) + GetProjectFlockKandangTransferStateAtDate(ctx *fiber.Ctx, projectFlockKandangID uint, referenceDate *time.Time) (bool, bool, error) GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) @@ -544,6 +545,10 @@ func (s projectflockService) GetProjectFlockKandangChickinDate(ctx *fiber.Ctx, p } func (s projectflockService) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, projectFlockKandangID uint) (bool, bool, error) { + return s.GetProjectFlockKandangTransferStateAtDate(ctx, projectFlockKandangID, nil) +} + +func (s projectflockService) GetProjectFlockKandangTransferStateAtDate(ctx *fiber.Ctx, projectFlockKandangID uint, referenceDate *time.Time) (bool, bool, error) { if projectFlockKandangID == 0 || s.TransferLayingRepo == nil || s.PivotRepo == nil { return false, false, nil } @@ -593,9 +598,12 @@ func (s projectflockService) GetProjectFlockKandangTransferState(ctx *fiber.Ctx, economicCutoffDate = physicalMoveDate } - referenceDate := normalizeDateOnlyUTC(time.Now().UTC()) - isTransition := !referenceDate.Before(physicalMoveDate) && referenceDate.Before(economicCutoffDate) - isLaying := !referenceDate.Before(economicCutoffDate) + reference := normalizeDateOnlyUTC(time.Now().UTC()) + if referenceDate != nil && !referenceDate.IsZero() { + reference = normalizeDateOnlyUTC(referenceDate.UTC()) + } + isTransition := !reference.Before(physicalMoveDate) && reference.Before(economicCutoffDate) + isLaying := !reference.Before(economicCutoffDate) return isTransition, isLaying, nil } diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index 278f8aec..b92d5a3c 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -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 } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 3010eca1..fae5740d 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -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 diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 2f99dce0..2a4d8726 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -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 } } diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 5a747fca..7de39ef8 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -13,6 +13,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" @@ -380,12 +381,13 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file } } weekBase := 1 - if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) { - weekBase = 18 + isLayingCategory := strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) + if isLayingCategory { + weekBase = config.LayingWeekStart() } if req.Week < weekBase { - if weekBase == 18 { - return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects") + if isLayingCategory { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) } return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") } @@ -399,8 +401,8 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence") } if latestWeek == 0 && req.Week != weekBase { - if weekBase == 18 { - return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects") + if isLayingCategory { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) } return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") } @@ -474,7 +476,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file }); err != nil { s.Log.Errorf("Failed to create uniformity: %+v", err) return nil, err - } + } if s.DocumentSvc != nil { actorIDCopy := actorID @@ -575,12 +577,13 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui } } weekBase := 1 - if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) { - weekBase = 18 + isLayingCategory := strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) + if isLayingCategory { + weekBase = config.LayingWeekStart() } if targetWeek < weekBase { - if weekBase == 18 { - return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 18 for laying projects") + if isLayingCategory { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) } return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 38fdd74b..3e002e2c 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "gitlab.com/mbugroup/lti-api.git/internal/config" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" @@ -256,11 +257,11 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation. const ( recordsPerWeek = 7 - defaultStartWoa = 18 defaultStdBw = 1951 defaultBw = 0 defaultUniformText = "90% up" ) + defaultStartWoa := config.LayingWeekStart() if params.Limit <= 0 { params.Limit = 10 diff --git a/internal/utils/recording/recording_helpers.go b/internal/utils/recording/recording_helpers.go index 644f6e8d..ff95a2f7 100644 --- a/internal/utils/recording/recording_helpers.go +++ b/internal/utils/recording/recording_helpers.go @@ -6,6 +6,7 @@ import ( "strings" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -13,31 +14,31 @@ import ( ) type warnLogger interface { - Warnf(format string, args ...any) + Warnf(format string, args ...any) } type productWarehouseExistsRepo interface { - ExistsByID(ctx context.Context, id uint) (bool, error) + ExistsByID(ctx context.Context, id uint) (bool, error) } type recordingValidationRepo interface { - ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error) + ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error) } func EnsureProductWarehousesExist(ctx context.Context, repo productWarehouseExistsRepo, ids []uint) error { - if repo == nil || len(ids) == 0 { - return nil - } - for _, id := range ids { - ok, err := repo.ExistsByID(ctx, id) - if err != nil { - return err - } - if !ok { - return fmt.Errorf("product warehouse %d not found", id) - } - } - return nil + if repo == nil || len(ids) == 0 { + return nil + } + for _, id := range ids { + ok, err := repo.ExistsByID(ctx, id) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("product warehouse %d not found", id) + } + } + return nil } func EnsureProductWarehousesByFlags(ctx context.Context, repo recordingValidationRepo, ids []uint, flags []string, label string) error { @@ -82,212 +83,212 @@ func EnsureProductWarehousesByFlagsForItems[T any]( } func ComputeDepletionRate(prevRecording *entity.Recording, currentDepletion float64, totalChick int64) float64 { - base := 0.0 - if prevRecording != nil && prevRecording.TotalChickQty != nil && *prevRecording.TotalChickQty > 0 { - base = *prevRecording.TotalChickQty - } else if totalChick > 0 { - base = float64(totalChick) + currentDepletion - } - if base <= 0 { - return 0 - } - return (currentDepletion / base) * 100 + base := 0.0 + if prevRecording != nil && prevRecording.TotalChickQty != nil && *prevRecording.TotalChickQty > 0 { + base = *prevRecording.TotalChickQty + } else if totalChick > 0 { + base = float64(totalChick) + currentDepletion + } + if base <= 0 { + return 0 + } + return (currentDepletion / base) * 100 } func AttachLatestApprovals(ctx context.Context, items []entity.Recording, approvalSvc commonSvc.ApprovalService, logger warnLogger) error { - if len(items) == 0 || approvalSvc == nil { - return nil - } + if len(items) == 0 || approvalSvc == nil { + return nil + } - ids := make([]uint, 0, len(items)) - visited := make(map[uint]struct{}, len(items)) - for _, item := range items { - if item.Id == 0 { - continue - } - if _, ok := visited[item.Id]; ok { - continue - } - visited[item.Id] = struct{}{} - ids = append(ids, item.Id) - } + ids := make([]uint, 0, len(items)) + visited := make(map[uint]struct{}, len(items)) + for _, item := range items { + if item.Id == 0 { + continue + } + if _, ok := visited[item.Id]; ok { + continue + } + visited[item.Id] = struct{}{} + ids = append(ids, item.Id) + } - if len(ids) == 0 { - return nil - } + if len(ids) == 0 { + return nil + } - latestMap, err := approvalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowRecording, ids, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - if logger != nil { - logger.Warnf("Unable to load latest approvals for recordings: %+v", err) - } - return nil - } + latestMap, err := approvalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowRecording, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + if logger != nil { + logger.Warnf("Unable to load latest approvals for recordings: %+v", err) + } + return nil + } - if len(latestMap) == 0 { - return nil - } + if len(latestMap) == 0 { + return nil + } - for i := range items { - if items[i].Id == 0 { - continue - } - if approval, ok := latestMap[items[i].Id]; ok { - items[i].LatestApproval = approval - } - } + for i := range items { + if items[i].Id == 0 { + continue + } + if approval, ok := latestMap[items[i].Id]; ok { + items[i].LatestApproval = approval + } + } - return nil + return nil } func AttachLatestApproval(ctx context.Context, item *entity.Recording, approvalSvc commonSvc.ApprovalService, logger warnLogger) error { - if item == nil || item.Id == 0 || approvalSvc == nil { - return nil - } + if item == nil || item.Id == 0 || approvalSvc == nil { + return nil + } - latest, err := approvalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, item.Id, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - if logger != nil { - logger.Warnf("Unable to load approvals for recording %d: %+v", item.Id, err) - } - return nil - } - item.LatestApproval = latest - return nil + latest, err := approvalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, item.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + if logger != nil { + logger.Warnf("Unable to load approvals for recording %d: %+v", item.Id, err) + } + return nil + } + item.LatestApproval = latest + return nil } type productionStandardValues struct { - HenDay *float64 - HenHouse *float64 - FeedIntake *float64 - MaxDepletion *float64 - EggMass *float64 - EggWeight *float64 + HenDay *float64 + HenHouse *float64 + FeedIntake *float64 + MaxDepletion *float64 + EggMass *float64 + EggWeight *float64 } func AttachProductionStandards(ctx context.Context, db *gorm.DB, warnOnly bool, logger warnLogger, items ...*entity.Recording) error { - if len(items) == 0 { - return nil - } + if len(items) == 0 { + return nil + } - type standardKey struct { - standardID uint - week int - } - type standardCacheEntry struct { - values productionStandardValues - fcr *float64 - } + type standardKey struct { + standardID uint + week int + } + type standardCacheEntry struct { + values productionStandardValues + fcr *float64 + } - if db == nil { - return nil - } + if db == nil { + return nil + } - standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) - growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) - cache := make(map[standardKey]standardCacheEntry, len(items)) + standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) + growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) + cache := make(map[standardKey]standardCacheEntry, len(items)) - standardIDs := make(map[uint]struct{}, len(items)) - for _, item := range items { - if item == nil || item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 { - continue - } - if item.ProjectFlockKandang.ProjectFlock.ProductionStandardId > 0 { - standardIDs[item.ProjectFlockKandang.ProjectFlock.ProductionStandardId] = struct{}{} - } - } + standardIDs := make(map[uint]struct{}, len(items)) + for _, item := range items { + if item == nil || item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 { + continue + } + if item.ProjectFlockKandang.ProjectFlock.ProductionStandardId > 0 { + standardIDs[item.ProjectFlockKandang.ProjectFlock.ProductionStandardId] = struct{}{} + } + } - standardDetailByStd := make(map[uint]map[int]*entity.ProductionStandardDetail, len(standardIDs)) - growthDetailByStd := make(map[uint]map[int]*entity.StandardGrowthDetail, len(standardIDs)) + standardDetailByStd := make(map[uint]map[int]*entity.ProductionStandardDetail, len(standardIDs)) + growthDetailByStd := make(map[uint]map[int]*entity.StandardGrowthDetail, len(standardIDs)) - for standardID := range standardIDs { - details, err := standardDetailRepo.GetByProductionStandardID(ctx, standardID) - if err != nil { - if warnOnly { - if logger != nil { - logger.Warnf("Unable to preload production standard detail for standard %d: %+v", standardID, err) - } - } else { - return err - } - continue - } - detailMap := make(map[int]*entity.ProductionStandardDetail, len(details)) - for i := range details { - detail := details[i] - detailMap[detail.Week] = &detail - } - standardDetailByStd[standardID] = detailMap + for standardID := range standardIDs { + details, err := standardDetailRepo.GetByProductionStandardID(ctx, standardID) + if err != nil { + if warnOnly { + if logger != nil { + logger.Warnf("Unable to preload production standard detail for standard %d: %+v", standardID, err) + } + } else { + return err + } + continue + } + detailMap := make(map[int]*entity.ProductionStandardDetail, len(details)) + for i := range details { + detail := details[i] + detailMap[detail.Week] = &detail + } + standardDetailByStd[standardID] = detailMap - growths, err := growthDetailRepo.GetByProductionStandardID(ctx, standardID) - if err != nil { - if warnOnly { - if logger != nil { - logger.Warnf("Unable to preload standard growth detail for standard %d: %+v", standardID, err) - } - } else { - return err - } - continue - } - growthMap := make(map[int]*entity.StandardGrowthDetail, len(growths)) - for i := range growths { - growth := growths[i] - growthMap[growth.Week] = &growth - } - growthDetailByStd[standardID] = growthMap - } + growths, err := growthDetailRepo.GetByProductionStandardID(ctx, standardID) + if err != nil { + if warnOnly { + if logger != nil { + logger.Warnf("Unable to preload standard growth detail for standard %d: %+v", standardID, err) + } + } else { + return err + } + continue + } + growthMap := make(map[int]*entity.StandardGrowthDetail, len(growths)) + for i := range growths { + growth := growths[i] + growthMap[growth.Week] = &growth + } + growthDetailByStd[standardID] = growthMap + } - for _, item := range items { - if item == nil || item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 { - continue - } - standardID := item.ProjectFlockKandang.ProjectFlock.ProductionStandardId - if standardID == 0 { - continue - } - week := RecordingWeekValue(*item) - cacheKey := standardKey{standardID: standardID, week: week} - if cached, ok := cache[cacheKey]; ok { - applyProductionStandardValues(item, cached.values, cached.fcr) - continue - } - values := productionStandardValues{} - var fcr *float64 - if detailMap, ok := standardDetailByStd[standardID]; ok { - if detail, ok := detailMap[week]; ok { - values.HenDay = detail.TargetHenDayProduction - values.HenHouse = detail.TargetHenHouseProduction - values.EggMass = detail.TargetEggMass - values.EggWeight = detail.TargetEggWeight - fcr = detail.StandardFCR - } - } - if growthMap, ok := growthDetailByStd[standardID]; ok { - if growth, ok := growthMap[week]; ok { - values.FeedIntake = growth.FeedIntake - values.MaxDepletion = growth.MaxDepletion - } - } - cache[cacheKey] = standardCacheEntry{values: values, fcr: fcr} - applyProductionStandardValues(item, values, fcr) - } + for _, item := range items { + if item == nil || item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 { + continue + } + standardID := item.ProjectFlockKandang.ProjectFlock.ProductionStandardId + if standardID == 0 { + continue + } + week := RecordingWeekValue(*item) + cacheKey := standardKey{standardID: standardID, week: week} + if cached, ok := cache[cacheKey]; ok { + applyProductionStandardValues(item, cached.values, cached.fcr) + continue + } + values := productionStandardValues{} + var fcr *float64 + if detailMap, ok := standardDetailByStd[standardID]; ok { + if detail, ok := detailMap[week]; ok { + values.HenDay = detail.TargetHenDayProduction + values.HenHouse = detail.TargetHenHouseProduction + values.EggMass = detail.TargetEggMass + values.EggWeight = detail.TargetEggWeight + fcr = detail.StandardFCR + } + } + if growthMap, ok := growthDetailByStd[standardID]; ok { + if growth, ok := growthMap[week]; ok { + values.FeedIntake = growth.FeedIntake + values.MaxDepletion = growth.MaxDepletion + } + } + cache[cacheKey] = standardCacheEntry{values: values, fcr: fcr} + applyProductionStandardValues(item, values, fcr) + } - return nil + return nil } func applyProductionStandardValues(item *entity.Recording, values productionStandardValues, fcr *float64) { - item.StandardHenDay = values.HenDay - item.StandardHenHouse = values.HenHouse - item.StandardFeedIntake = values.FeedIntake - item.StandardMaxDepletion = values.MaxDepletion - item.StandardEggMass = values.EggMass - item.StandardEggWeight = values.EggWeight - item.StandardFcr = fcr + item.StandardHenDay = values.HenDay + item.StandardHenHouse = values.HenHouse + item.StandardFeedIntake = values.FeedIntake + item.StandardMaxDepletion = values.MaxDepletion + item.StandardEggMass = values.EggMass + item.StandardEggWeight = values.EggWeight + item.StandardFcr = fcr } func RecordingWeekValue(e entity.Recording) int { @@ -297,7 +298,7 @@ func RecordingWeekValue(e entity.Recording) int { } weekBase := 1 if IsLayingRecording(e) { - weekBase = 18 + weekBase = config.LayingWeekStart() } return ((day - 1) / 7) + weekBase }