mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
405 lines
11 KiB
Go
405 lines
11 KiB
Go
package recording
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
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"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type warnLogger interface {
|
|
Warnf(format string, args ...any)
|
|
}
|
|
|
|
type productWarehouseExistsRepo interface {
|
|
ExistsByID(ctx context.Context, id uint) (bool, error)
|
|
}
|
|
|
|
type recordingValidationRepo interface {
|
|
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
|
|
}
|
|
|
|
func EnsureProductWarehousesByFlags(ctx context.Context, repo recordingValidationRepo, ids []uint, flags []string, label string) error {
|
|
if repo == nil || len(ids) == 0 {
|
|
return nil
|
|
}
|
|
invalidID, err := repo.ValidateProductWarehousesByFlags(ctx, ids, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if invalidID != 0 {
|
|
return fmt.Errorf("product warehouse %d is not a %s warehouse", invalidID, label)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type idGetter[T any] func(T) uint
|
|
|
|
func CollectWarehouseIDs[T any](items []T, getID idGetter[T]) []uint {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
ids := make([]uint, 0, len(items))
|
|
for _, item := range items {
|
|
if id := getID(item); id != 0 {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func EnsureProductWarehousesByFlagsForItems[T any](
|
|
ctx context.Context,
|
|
repo recordingValidationRepo,
|
|
items []T,
|
|
getID idGetter[T],
|
|
flags []string,
|
|
label string,
|
|
) error {
|
|
ids := CollectWarehouseIDs(items, getID)
|
|
return EnsureProductWarehousesByFlags(ctx, repo, ids, flags, label)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func AttachLatestApprovals(ctx context.Context, items []entity.Recording, approvalSvc commonSvc.ApprovalService, logger warnLogger) error {
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func AttachProductionStandards(ctx context.Context, db *gorm.DB, warnOnly bool, logger warnLogger, items ...*entity.Recording) error {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
type standardKey struct {
|
|
standardID uint
|
|
week int
|
|
}
|
|
type standardCacheEntry struct {
|
|
values productionStandardValues
|
|
fcr *float64
|
|
}
|
|
|
|
if db == nil {
|
|
return nil
|
|
}
|
|
|
|
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{}{}
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
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
|
|
}
|
|
|
|
// Batch-load laying transfer targets → source PFK chick_in_dates
|
|
// untuk menentukan actual chicken week (bukan hardcode LayingWeekStart offset)
|
|
type transferChickIn struct {
|
|
TargetPFKID uint
|
|
ChickInDate time.Time
|
|
}
|
|
layingPFKIDs := collectLayingPFKIDs(items)
|
|
sourceChickInByTarget := make(map[uint]time.Time, len(layingPFKIDs))
|
|
if len(layingPFKIDs) > 0 {
|
|
var results []transferChickIn
|
|
db.Raw(`
|
|
SELECT ltt.target_project_flock_kandang_id AS target_pfk_id, pc.chick_in_date
|
|
FROM laying_transfer_targets ltt
|
|
JOIN laying_transfer_sources lts ON lts.laying_transfer_id = ltt.laying_transfer_id
|
|
JOIN project_chickins pc ON pc.project_flock_kandang_id = lts.source_project_flock_kandang_id
|
|
WHERE ltt.target_project_flock_kandang_id IN ?
|
|
AND ltt.deleted_at IS NULL
|
|
AND lts.deleted_at IS NULL
|
|
AND pc.deleted_at IS NULL
|
|
`, layingPFKIDs).Scan(&results)
|
|
for _, r := range results {
|
|
sourceChickInByTarget[r.TargetPFKID] = r.ChickInDate
|
|
}
|
|
}
|
|
|
|
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 := computeTransferAwareWeek(item, sourceChickInByTarget)
|
|
item.StandardWeek = &week
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// collectLayingPFKIDs mengumpulkan semua project_flock_kandang_id dari recording laying
|
|
func collectLayingPFKIDs(items []*entity.Recording) []uint {
|
|
seen := make(map[uint]struct{})
|
|
var ids []uint
|
|
for _, item := range items {
|
|
if item == nil || item.ProjectFlockKandang == nil {
|
|
continue
|
|
}
|
|
if strings.EqualFold(item.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying)) {
|
|
id := item.ProjectFlockKandang.Id
|
|
if _, ok := seen[id]; !ok {
|
|
seen[id] = struct{}{}
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// computeTransferAwareWeek menghitung production standard week untuk recording.
|
|
// Laying dengan transfer: actual chicken age dari source PFK chick_in_date.
|
|
// Laying cut-over (tanpa transfer): langsung dari recording.day (tanpa offset LayingWeekStart).
|
|
// Non-laying: ((day-1)/7) + 1.
|
|
func computeTransferAwareWeek(item *entity.Recording, sourceChickInByTarget map[uint]time.Time) int {
|
|
day := intValue(item.Day)
|
|
if item == nil || item.ProjectFlockKandang == nil {
|
|
if day > 0 {
|
|
return ((day - 1) / 7) + 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
isLaying := strings.EqualFold(item.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying))
|
|
if !isLaying {
|
|
if day > 0 {
|
|
return ((day - 1) / 7) + 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Laying recording — cek apakah PFK ini adalah target dari laying transfer
|
|
if sourceChickIn, ok := sourceChickInByTarget[item.ProjectFlockKandang.Id]; ok && !sourceChickIn.IsZero() {
|
|
// Ada laying transfer: hitung umur aktual dari source PFK chick_in_date
|
|
rDate := time.Date(item.RecordDatetime.Year(), item.RecordDatetime.Month(), item.RecordDatetime.Day(), 0, 0, 0, 0, item.RecordDatetime.Location())
|
|
sDate := time.Date(sourceChickIn.Year(), sourceChickIn.Month(), sourceChickIn.Day(), 0, 0, 0, 0, sourceChickIn.Location())
|
|
actualDay := int(rDate.Sub(sDate).Hours() / 24)
|
|
if actualDay > 0 {
|
|
return ((actualDay - 1) / 7) + 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Cut-over laying (tanpa transfer): chick_in_date di PFK sudah umur asli DOC
|
|
if day > 0 {
|
|
return ((day - 1) / 7) + 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func RecordingWeekValue(e entity.Recording) int {
|
|
day := intValue(e.Day)
|
|
if day <= 0 {
|
|
return 0
|
|
}
|
|
weekBase := 1
|
|
if IsLayingRecording(e) {
|
|
weekBase = config.LayingWeekStart()
|
|
}
|
|
return ((day - 1) / 7) + weekBase
|
|
}
|
|
|
|
func IsLayingRecording(e entity.Recording) bool {
|
|
if e.ProjectFlockKandang == nil {
|
|
return false
|
|
}
|
|
return strings.EqualFold(e.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying))
|
|
}
|
|
|
|
func intValue(value *int) int {
|
|
if value == nil {
|
|
return 0
|
|
}
|
|
return *value
|
|
}
|