[FEAT/BE] fixing fifo fallback recording,fixing backdate and fixing product category

This commit is contained in:
ragilap
2026-02-18 11:49:25 +07:00
parent 756fc431b3
commit 36ba4f34bb
20 changed files with 2036 additions and 889 deletions
+1
View File
@@ -17,4 +17,5 @@ const (
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
StockableKeyRecordingDepletion StockableKey = "RECORDING_DEPLETION_IN"
)
@@ -0,0 +1,330 @@
package recording
import (
"context"
"fmt"
"strings"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
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 {
ValidateFeedProductWarehouses(ctx context.Context, ids []uint) (uint, error)
ValidateEggProductWarehouses(ctx context.Context, ids []uint) (uint, error)
ValidateDepletionProductWarehouses(ctx context.Context, ids []uint) (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
}
type pwValidatorFunc func(ctx context.Context, ids []uint) (uint, error)
func ensureProductWarehouses(ctx context.Context, ids []uint, label string, validator pwValidatorFunc) error {
if len(ids) == 0 || validator == nil {
return nil
}
invalidID, err := validator(ctx, ids)
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 EnsureFeedProductWarehouses(ctx context.Context, repo recordingValidationRepo, ids []uint) error {
if repo == nil {
return nil
}
return ensureProductWarehouses(ctx, ids, "feed", repo.ValidateFeedProductWarehouses)
}
func EnsureEggProductWarehouses(ctx context.Context, repo recordingValidationRepo, ids []uint) error {
if repo == nil {
return nil
}
return ensureProductWarehouses(ctx, ids, "egg", repo.ValidateEggProductWarehouses)
}
func EnsureDepletionProductWarehouses(ctx context.Context, repo recordingValidationRepo, ids []uint) error {
if repo == nil {
return nil
}
return ensureProductWarehouses(ctx, ids, "depletion", repo.ValidateDepletionProductWarehouses)
}
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
}
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
}
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
}
func RecordingWeekValue(e entity.Recording) int {
day := intValue(e.Day)
if day <= 0 {
return 0
}
weekBase := 1
if IsLayingRecording(e) {
weekBase = 18
}
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
}
@@ -1,6 +1,9 @@
package recording
import (
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
)
@@ -70,3 +73,87 @@ func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity.
}
return result
}
type EggTotals struct {
Qty int
Weight float64
}
func StockUsageByWarehouse(items []entity.RecordingStock) map[uint]float64 {
return TotalsByWarehouse(items, func(stock entity.RecordingStock) (uint, float64) {
var usage float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
return stock.ProductWarehouseId, usage
})
}
func StockUsageByWarehouseReq(items []validation.Stock) map[uint]float64 {
return TotalsByWarehouse(items, func(item validation.Stock) (uint, float64) {
return item.ProductWarehouseId, item.Qty
})
}
func FloatMapsEqual(a, b map[uint]float64) bool {
if len(a) != len(b) {
return false
}
for key, value := range a {
other, ok := b[key]
if !ok || !floatNearlyEqual(value, other) {
return false
}
}
return true
}
func EggTotalsEqual(a, b map[uint]EggTotals) bool {
if len(a) != len(b) {
return false
}
for key, value := range a {
other, ok := b[key]
if !ok || value.Qty != other.Qty || !floatNearlyEqual(value.Weight, other.Weight) {
return false
}
}
return true
}
func floatNearlyEqual(a, b float64) bool {
return a-b <= 0.000001 && b-a <= 0.000001
}
func TotalsByWarehouse[T any](items []T, get func(T) (uint, float64)) map[uint]float64 {
result := make(map[uint]float64)
for _, item := range items {
warehouseID, qty := get(item)
result[warehouseID] += qty
}
return result
}
func EggTotalsByWarehouse[T any](items []T, get func(T) (uint, int, *float64)) map[uint]EggTotals {
result := make(map[uint]EggTotals)
for _, item := range items {
warehouseID, qty, weightPtr := get(item)
weight := 0.0
if weightPtr != nil {
weight = *weightPtr
}
current := result[warehouseID]
current.Qty += qty
current.Weight += weight
result[warehouseID] = current
}
return result
}
func RecordingNote(action string, id uint) string {
action = strings.TrimSpace(action)
if action == "" {
return fmt.Sprintf("Recording#%d", id)
}
return fmt.Sprintf("Recording-%s#%d", action, id)
}