package fifo_stock_v2 import ( "context" "encoding/json" "fmt" "math" "time" "gorm.io/gorm" ) func (s *fifoStockV2Service) Recalculate(ctx context.Context, req RecalculateRequest) (*RecalculateResult, error) { result := &RecalculateResult{Drifts: make([]WarehouseDrift, 0)} err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error { hash := requestHash(map[string]any{ "product_warehouse_ids": req.ProductWarehouseIDs, "flag_group_codes": req.FlagGroupCodes, "as_of": req.AsOf, "fix_drift": req.FixDrift, }) logRow, reused, err := s.beginOperation( tx, OperationRecalculate, req.IdempotencyKey, hash, 0, "RECALCULATE", "", 0, ) if err != nil { return err } if reused { if len(logRow.ResultPayload) == 0 { return fmt.Errorf("idempotent recalculate has empty payload") } if err := json.Unmarshal(logRow.ResultPayload, result); err != nil { return err } return nil } if logRow != nil { defer func() { if err != nil { s.failOperation(tx, logRow, err) } }() } warehouseIDs, err := s.resolveRecalculateWarehouseIDs(ctx, tx, req.ProductWarehouseIDs) if err != nil { return err } groupCodes, err := s.resolveRecalculateGroupCodes(ctx, tx, req.FlagGroupCodes) if err != nil { return err } for _, warehouseID := range warehouseIDs { expected := 0.0 for _, flagGroup := range groupCodes { available, calcErr := s.calculateWarehouseAvailableForGroup(ctx, tx, warehouseID, flagGroup, req.AsOf) if calcErr != nil { return calcErr } expected += available } actual, actualErr := s.loadWarehouseQty(ctx, tx, warehouseID) if actualErr != nil { return actualErr } delta := expected - actual result.Checked++ if math.Abs(delta) < 1e-6 { continue } drift := WarehouseDrift{ ProductWarehouseID: warehouseID, ExpectedQty: expected, ActualQty: actual, Delta: delta, } result.Drifts = append(result.Drifts, drift) if req.FixDrift { if err := s.adjustProductWarehouseQty(tx, warehouseID, delta); err != nil { return err } result.Fixed++ } } if err := s.finishOperation(tx, logRow, result); err != nil { return err } return nil }) if err != nil { return nil, err } return result, nil } func (s *fifoStockV2Service) resolveRecalculateWarehouseIDs(ctx context.Context, tx *gorm.DB, provided []uint) ([]uint, error) { if len(provided) > 0 { return provided, nil } var ids []uint err := tx.WithContext(ctx).Table("product_warehouses").Select("id").Order("id ASC").Scan(&ids).Error if err != nil { return nil, err } return ids, nil } func (s *fifoStockV2Service) resolveRecalculateGroupCodes(ctx context.Context, tx *gorm.DB, provided []string) ([]string, error) { if len(provided) > 0 { return provided, nil } var groups []string err := tx.WithContext(ctx). Table("fifo_stock_v2_flag_groups"). Select("code"). Where("is_active = TRUE"). Order("priority ASC, code ASC"). Scan(&groups).Error if err != nil { return nil, err } return groups, nil } func (s *fifoStockV2Service) calculateWarehouseAvailableForGroup( ctx context.Context, tx *gorm.DB, warehouseID uint, flagGroupCode string, asOf *time.Time, ) (float64, error) { rows, err := s.gatherRows(ctx, tx, GatherRequest{ FlagGroupCode: flagGroupCode, Lane: LaneStockable, ProductWarehouseID: warehouseID, AsOf: asOf, Limit: 50000, }) if err != nil { return 0, err } total := 0.0 for _, row := range rows { total += row.AvailableQuantity } return total, nil } func (s *fifoStockV2Service) loadWarehouseQty(ctx context.Context, tx *gorm.DB, warehouseID uint) (float64, error) { type row struct { Qty float64 `gorm:"column:qty"` } var out row err := tx.WithContext(ctx). Table("product_warehouses"). Select("COALESCE(qty,0) AS qty"). Where("id = ?", warehouseID). Take(&out).Error if err != nil { return 0, err } return out.Qty, nil }