feat(BE): fixing fifo system recording

This commit is contained in:
ragilap
2025-12-31 19:30:04 +07:00
parent e0dd2799fc
commit fe51f33ab4
4 changed files with 451 additions and 60 deletions
@@ -192,6 +192,16 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
if req.Quantity < 0 { if req.Quantity < 0 {
return nil, errors.New("quantity must be zero or greater") return nil, errors.New("quantity must be zero or greater")
} }
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
s.logger.WithFields(logrus.Fields{
"usable_key": req.UsableKey.String(),
"usable_id": req.UsableID,
"requested_quantity": req.Quantity,
"allow_pending": req.AllowPending,
"product_warehouse_id": req.ProductWarehouseID,
}).Debug("fifo consume request")
}
cfg, ok := fifo.Usable(req.UsableKey) cfg, ok := fifo.Usable(req.UsableKey)
if !ok { if !ok {
@@ -220,6 +230,19 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
currentPending := ctxRow.PendingQty currentPending := ctxRow.PendingQty
currentTotal := currentUsage + currentPending currentTotal := currentUsage + currentPending
delta := req.Quantity - currentTotal delta := req.Quantity - currentTotal
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
s.logger.WithFields(logrus.Fields{
"usable_key": req.UsableKey.String(),
"usable_id": req.UsableID,
"product_warehouse_id": productWarehouseID,
"current_usage_qty": currentUsage,
"current_pending_qty": currentPending,
"current_total_qty": currentTotal,
"requested_quantity": req.Quantity,
"calculated_delta": delta,
"input_warehouse_match": req.ProductWarehouseID == 0 || req.ProductWarehouseID == productWarehouseID,
}).Debug("fifo consume context")
}
var ( var (
usageDelta float64 usageDelta float64
@@ -285,6 +308,20 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
result.ReleasedQuantity = releasedAmount result.ReleasedQuantity = releasedAmount
result.UsageQuantity = currentUsage + usageDelta result.UsageQuantity = currentUsage + usageDelta
result.PendingQuantity = currentPending + pendingDelta result.PendingQuantity = currentPending + pendingDelta
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
s.logger.WithFields(logrus.Fields{
"usable_key": req.UsableKey.String(),
"usable_id": req.UsableID,
"product_warehouse_id": productWarehouseID,
"usage_delta": usageDelta,
"pending_delta": pendingDelta,
"released_quantity": releasedAmount,
"added_allocations": len(addedAlloc),
"final_usage_qty": result.UsageQuantity,
"final_pending_qty": result.PendingQuantity,
"final_requested_qty": result.RequestedQuantity,
}).Debug("fifo consume result")
}
return nil return nil
}) })
@@ -299,6 +336,13 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" { if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
return errors.New("usable key and id are required") return errors.New("usable key and id are required")
} }
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
s.logger.WithFields(logrus.Fields{
"usable_key": req.UsableKey.String(),
"usable_id": req.UsableID,
"reason": req.Reason,
}).Debug("fifo release request")
}
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error { return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
cfg, ok := fifo.Usable(req.UsableKey) cfg, ok := fifo.Usable(req.UsableKey)
@@ -310,6 +354,16 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
if err != nil { if err != nil {
return err return err
} }
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
s.logger.WithFields(logrus.Fields{
"usable_key": req.UsableKey.String(),
"usable_id": req.UsableID,
"product_warehouse_id": ctxRow.ProductWarehouseID,
"current_usage_qty": ctxRow.UsageQty,
"current_pending_qty": ctxRow.PendingQty,
"current_total_qty": ctxRow.UsageQty + ctxRow.PendingQty,
}).Debug("fifo release context")
}
var usageDelta, pendingDelta float64 var usageDelta, pendingDelta float64
if ctxRow.UsageQty > 0 { if ctxRow.UsageQty > 0 {
@@ -326,6 +380,15 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
return err return err
} }
if s.logger.IsLevelEnabled(logrus.DebugLevel) {
s.logger.WithFields(logrus.Fields{
"usable_key": req.UsableKey.String(),
"usable_id": req.UsableID,
"usage_delta": usageDelta,
"pending_delta": pendingDelta,
}).Debug("fifo release applied")
}
return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB { return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db) return s.txOrDB(tx, db)
}) })
@@ -194,13 +194,17 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
StockLogId: newLog.Id, StockLogId: newLog.Id,
ProductWarehouseId: productWarehouse.Id, ProductWarehouseId: productWarehouse.Id,
} }
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
}
if transactionType == string(utils.StockLogTransactionTypeIncrease) { if transactionType == string(utils.StockLogTransactionTypeIncrease) {
// Adjustment INCREASE → Replenish stock (Stockable) // Adjustment INCREASE → Replenish stock (Stockable)
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id) note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
replenishResult, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{ _, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
StockableKey: "ADJUSTMENT_IN", StockableKey: "ADJUSTMENT_IN",
StockableID: newLog.Id, StockableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id), ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity, Quantity: req.Quantity,
Note: &note, Note: &note,
@@ -210,15 +214,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
} }
// Update stockable tracking fields
adjustmentStock.TotalQty = replenishResult.AddedQuantity
adjustmentStock.TotalUsed = 0
} else { } else {
// Adjustment DECREASE → Consume stock (Usable) // Adjustment DECREASE → Consume stock (Usable)
consumeResult, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{ _, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
UsableKey: "ADJUSTMENT_OUT", UsableKey: "ADJUSTMENT_OUT",
UsableID: newLog.Id, UsableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id), ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity, Quantity: req.Quantity,
AllowPending: false, // Don't allow pending for adjustment AllowPending: false, // Don't allow pending for adjustment
@@ -227,16 +227,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
} }
// Update usable tracking fields
adjustmentStock.UsageQty = consumeResult.UsageQuantity
adjustmentStock.PendingQty = consumeResult.PendingQuantity
}
// Save AdjustmentStock record
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
} }
// Update ProductWarehouse quantity (for backward compatibility/reporting) // Update ProductWarehouse quantity (for backward compatibility/reporting)
@@ -295,16 +295,17 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
var rows []struct { var rows []struct {
UsageQty float64 TotalQty float64
UomName string UomName string
} }
if err := tx. if err := tx.
Table("recording_stocks"). Table("recording_stocks").
Select("COALESCE(recording_stocks.usage_qty, 0) AS usage_qty, LOWER(uoms.name) AS uom_name"). Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id"). Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN uoms ON uoms.id = products.uom_id"). Joins("JOIN uoms ON uoms.id = products.uom_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
Where("recording_stocks.recording_id = ?", recordingID). Where("recording_stocks.recording_id = ?", recordingID).
Scan(&rows).Error; err != nil { Scan(&rows).Error; err != nil {
return 0, err return 0, err
@@ -312,16 +313,16 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
var total float64 var total float64
for _, row := range rows { for _, row := range rows {
if row.UsageQty <= 0 { if row.TotalQty <= 0 {
continue continue
} }
switch strings.TrimSpace(row.UomName) { switch strings.TrimSpace(row.UomName) {
case "kilogram", "kg", "kilograms", "kilo": case "kilogram", "kg", "kilograms", "kilo":
total += row.UsageQty * 1000 total += row.TotalQty * 1000
case "gram", "g", "grams": case "gram", "g", "grams":
total += row.UsageQty total += row.TotalQty
default: default:
total += row.UsageQty total += row.TotalQty
} }
} }
return total, nil return total, nil
@@ -247,11 +247,13 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
} }
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks) mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
stockDesired := resetStockQuantitiesForFIFO(mappedStocks, s.FifoSvc != nil)
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
s.Log.Errorf("Failed to persist stocks: %+v", err) s.Log.Errorf("Failed to persist stocks: %+v", err)
return err return err
} }
applyStockDesiredQuantities(mappedStocks, stockDesired, s.FifoSvc != nil)
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil { if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
return err return err
} }
@@ -324,6 +326,49 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
hasDepletionChanges := req.Depletions != nil hasDepletionChanges := req.Depletions != nil
hasEggChanges := req.Eggs != nil hasEggChanges := req.Eggs != nil
var existingStocks []entity.RecordingStock
if hasStockChanges {
existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing stocks: %+v", err)
return err
}
if s.Log != nil && s.Log.IsLevelEnabled(logrus.DebugLevel) {
s.Log.WithFields(logrus.Fields{
"recording_id": recordingEntity.Id,
"existing": summarizeExistingStocks(existingStocks),
"incoming": summarizeIncomingStocks(req.Stocks),
}).Debug("recording update stock comparison")
}
if stocksMatch(existingStocks, req.Stocks) {
hasStockChanges = false
}
}
var existingDepletions []entity.RecordingDepletion
if hasDepletionChanges {
existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing depletions: %+v", err)
return err
}
if depletionsMatch(existingDepletions, req.Depletions) {
hasDepletionChanges = false
}
}
var existingEggs []entity.RecordingEgg
if hasEggChanges {
existingEggs, err = s.Repository.ListEggs(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing eggs: %+v", err)
return err
}
if eggsMatch(existingEggs, req.Eggs) {
hasEggChanges = false
}
}
if !hasStockChanges && !hasDepletionChanges && !hasEggChanges { if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
return nil return nil
} }
@@ -355,39 +400,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
if hasStockChanges { if hasStockChanges {
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id) if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks); err != nil {
if err != nil {
s.Log.Errorf("Failed to list existing stocks: %+v", err)
return err
}
if err := s.releaseRecordingStocks(ctx, tx, existingStocks); err != nil {
return err
}
if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear stocks: %+v", err)
return err
}
mappedStocks := recordingutil.MapStocks(recordingEntity.Id, req.Stocks)
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
s.Log.Errorf("Failed to update stocks: %+v", err)
return err
}
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
return err return err
} }
} }
if hasDepletionChanges { if hasDepletionChanges {
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing depletions: %+v", err)
return err
}
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil { if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear depletions: %+v", err) s.Log.Errorf("Failed to clear depletions: %+v", err)
return err return err
@@ -406,12 +424,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
if hasEggChanges { if hasEggChanges {
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing eggs: %+v", err)
return err
}
if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil { if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear eggs: %+v", err) s.Log.Errorf("Failed to clear eggs: %+v", err)
return err return err
@@ -429,7 +441,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
} }
if hasStockChanges || hasDepletionChanges { if hasStockChanges || hasDepletionChanges || hasEggChanges {
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
s.Log.Errorf("Failed to recompute recording metrics: %+v", err) s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
return err return err
@@ -680,12 +692,27 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
if stock.UsageQty != nil { if stock.UsageQty != nil {
desired = *stock.UsageQty desired = *stock.UsageQty
} }
var pending float64
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
desiredTotal := desired + pending
if s.Log != nil && s.Log.IsLevelEnabled(logrus.DebugLevel) {
s.Log.WithFields(logrus.Fields{
"recording_stock_id": stock.Id,
"product_warehouse_id": stock.ProductWarehouseId,
"desired_usage_qty": desired,
"desired_pending_qty": pending,
"desired_total_qty": desiredTotal,
}).Debug("recording fifo consume start")
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingStockUsableKey, UsableKey: recordingStockUsableKey,
UsableID: stock.Id, UsableID: stock.Id,
ProductWarehouseID: stock.ProductWarehouseId, ProductWarehouseID: stock.ProductWarehouseId,
Quantity: desired, Quantity: desiredTotal,
AllowPending: true, AllowPending: true,
Tx: tx, Tx: tx,
}) })
@@ -694,6 +721,17 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
return err return err
} }
if s.Log != nil && s.Log.IsLevelEnabled(logrus.DebugLevel) {
s.Log.WithFields(logrus.Fields{
"recording_stock_id": stock.Id,
"product_warehouse_id": stock.ProductWarehouseId,
"result_usage_qty": result.UsageQuantity,
"result_pending_qty": result.PendingQuantity,
"released_qty": result.ReleasedQuantity,
"added_allocations": len(result.AddedAllocations),
}).Debug("recording fifo consume result")
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil { if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
return err return err
} }
@@ -716,6 +754,23 @@ func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.
continue continue
} }
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
if s.Log != nil && s.Log.IsLevelEnabled(logrus.DebugLevel) {
s.Log.WithFields(logrus.Fields{
"recording_stock_id": stock.Id,
"product_warehouse_id": stock.ProductWarehouseId,
"current_usage_qty": usage,
"current_pending_qty": pending,
}).Debug("recording fifo release start")
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingStockUsableKey, UsableKey: recordingStockUsableKey,
UsableID: stock.Id, UsableID: stock.Id,
@@ -771,6 +826,288 @@ func (s *recordingService) adjustProductWarehouseQuantities(ctx context.Context,
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx }) return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
} }
type desiredStock struct {
Usage float64
Pending float64
}
func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock, enabled bool) []desiredStock {
desired := make([]desiredStock, len(stocks))
for i := range stocks {
if stocks[i].UsageQty != nil {
desired[i].Usage = *stocks[i].UsageQty
}
if stocks[i].PendingQty != nil {
desired[i].Pending = *stocks[i].PendingQty
}
if !enabled {
continue
}
zero := 0.0
stocks[i].UsageQty = &zero
stocks[i].PendingQty = &zero
}
return desired
}
func applyStockDesiredQuantities(stocks []entity.RecordingStock, desired []desiredStock, enabled bool) {
if !enabled {
return
}
for i := range stocks {
if i >= len(desired) {
break
}
usage := desired[i].Usage
pending := desired[i].Pending
stocks[i].UsageQty = &usage
stocks[i].PendingQty = &pending
}
}
func (s *recordingService) syncRecordingStocks(
ctx context.Context,
tx *gorm.DB,
recordingID uint,
existing []entity.RecordingStock,
incoming []validation.Stock,
) error {
if s.FifoSvc == nil {
if err := s.Repository.DeleteStocks(tx, recordingID); err != nil {
return err
}
mapped := recordingutil.MapStocks(recordingID, incoming)
return s.Repository.CreateStocks(tx, mapped)
}
existingByWarehouse := make(map[uint][]entity.RecordingStock)
for _, stock := range existing {
existingByWarehouse[stock.ProductWarehouseId] = append(existingByWarehouse[stock.ProductWarehouseId], stock)
}
stocksToConsume := make([]entity.RecordingStock, 0, len(incoming))
for _, item := range incoming {
list := existingByWarehouse[item.ProductWarehouseId]
var stock entity.RecordingStock
if len(list) > 0 {
stock = list[0]
existingByWarehouse[item.ProductWarehouseId] = list[1:]
} else {
zero := 0.0
stock = entity.RecordingStock{
RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId,
UsageQty: &zero,
PendingQty: &zero,
}
if err := tx.Create(&stock).Error; err != nil {
return err
}
}
desired := item.Qty
stock.UsageQty = &desired
if item.PendingQty != nil {
pending := *item.PendingQty
stock.PendingQty = &pending
}
stocksToConsume = append(stocksToConsume, stock)
}
var leftovers []entity.RecordingStock
for _, list := range existingByWarehouse {
leftovers = append(leftovers, list...)
}
if len(leftovers) > 0 {
if err := s.releaseRecordingStocks(ctx, tx, leftovers); err != nil {
return err
}
ids := make([]uint, 0, len(leftovers))
for _, stock := range leftovers {
if stock.Id != 0 {
ids = append(ids, stock.Id)
}
}
if len(ids) > 0 {
if err := tx.Where("id IN ?", ids).Delete(&entity.RecordingStock{}).Error; err != nil {
return err
}
}
}
if len(stocksToConsume) == 0 {
return nil
}
return s.consumeRecordingStocks(ctx, tx, stocksToConsume)
}
type eggTotals struct {
Qty int
Weight float64
}
type stockTotals struct {
Usage float64
Pending float64
Total float64
}
func summarizeExistingStocks(stocks []entity.RecordingStock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += usage
current.Pending += pending
current.Total += usage + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func summarizeIncomingStocks(stocks []validation.Stock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var pending float64
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += stock.Qty
current.Pending += pending
current.Total += stock.Qty + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
hasPending := false
for _, item := range incoming {
if item.PendingQty != nil {
hasPending = true
break
}
}
existingUsage := make(map[uint]float64)
existingTotal := make(map[uint]float64)
for _, stock := range existing {
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
existingUsage[stock.ProductWarehouseId] += usage
existingTotal[stock.ProductWarehouseId] += usage + pending
}
incomingUsage := make(map[uint]float64)
incomingTotal := make(map[uint]float64)
for _, item := range incoming {
var pending float64
if item.PendingQty != nil {
pending = *item.PendingQty
}
incomingUsage[item.ProductWarehouseId] += item.Qty
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
}
if hasPending {
return floatMapsMatch(existingTotal, incomingTotal)
}
return floatMapsMatch(existingUsage, incomingUsage)
}
func depletionsMatch(existing []entity.RecordingDepletion, incoming []validation.Depletion) bool {
existingTotals := make(map[uint]float64)
for _, dep := range existing {
existingTotals[dep.ProductWarehouseId] += dep.Qty
}
incomingTotals := make(map[uint]float64)
for _, dep := range incoming {
incomingTotals[dep.ProductWarehouseId] += dep.Qty
}
return floatMapsMatch(existingTotals, incomingTotals)
}
func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
existingTotals := make(map[uint]eggTotals)
for _, egg := range existing {
weight := 0.0
if egg.Weight != nil {
weight = *egg.Weight
}
current := existingTotals[egg.ProductWarehouseId]
current.Qty += egg.Qty
current.Weight += float64(egg.Qty) * weight
existingTotals[egg.ProductWarehouseId] = current
}
incomingTotals := make(map[uint]eggTotals)
for _, egg := range incoming {
weight := 0.0
if egg.Weight != nil {
weight = *egg.Weight
}
current := incomingTotals[egg.ProductWarehouseId]
current.Qty += egg.Qty
current.Weight += float64(egg.Qty) * weight
incomingTotals[egg.ProductWarehouseId] = current
}
if len(existingTotals) != len(incomingTotals) {
return false
}
for key, existingTotal := range existingTotals {
incomingTotal, ok := incomingTotals[key]
if !ok {
return false
}
if existingTotal.Qty != incomingTotal.Qty {
return false
}
if !floatNearlyEqual(existingTotal.Weight, incomingTotal.Weight) {
return false
}
}
return true
}
func floatMapsMatch(a, b map[uint]float64) bool {
if len(a) != len(b) {
return false
}
for key, value := range a {
other, ok := b[key]
if !ok {
return false
}
if !floatNearlyEqual(value, other) {
return false
}
}
return true
}
func floatNearlyEqual(a, b float64) bool {
return math.Abs(a-b) <= 0.000001
}
func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error { func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error {
day := 0 day := 0
if recording.Day != nil { if recording.Day != nil {