package main import ( "context" "errors" "flag" "fmt" "log" "math" "os" "strings" "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/database" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gorm.io/gorm" ) const qtyEpsilon = 1e-6 const ( levelAll = 1 levelByProductName = 2 levelByProductWarehouse = 3 ) type reflowRow struct { ProductWarehouseID uint `gorm:"column:product_warehouse_id"` ProductID uint `gorm:"column:product_id"` ProductName string `gorm:"column:product_name"` CurrentQty float64 `gorm:"column:current_qty"` SumTotalQty float64 `gorm:"column:sum_total_qty"` SumAllocatedQty float64 `gorm:"column:sum_allocated_qty"` ComputedQty float64 `gorm:"column:computed_qty"` } func main() { var ( apply bool level int productName string productWarehouseID uint ) flag.BoolVar(&apply, "apply", false, "Apply changes. If false, run as dry-run") flag.IntVar(&level, "level", levelAll, "CLI level: 1=all product_warehouse scope, 2=product name scope, 3=product_warehouse_id scope") flag.StringVar(&productName, "product-name", "", "Product name (required for level 2)") flag.UintVar(&productWarehouseID, "product-warehouse-id", 0, "Product warehouse id (required for level 3)") flag.Parse() productName = strings.TrimSpace(productName) if err := validateFlags(level, productName, productWarehouseID); err != nil { log.Fatalf("invalid flags: %v", err) } ctx := context.Background() db := database.Connect(config.DBHost, config.DBName) rows, err := loadReflowRows(ctx, db, level, productName, productWarehouseID) if err != nil { log.Fatalf("failed to calculate reflow qty: %v", err) } fmt.Printf("Mode: %s\n", modeLabel(apply)) fmt.Printf("Level: %d (%s)\n", level, levelLabel(level)) if productName != "" { fmt.Printf("Filter product_name: %s\n", productName) } if productWarehouseID > 0 { fmt.Printf("Filter product_warehouse_id: %d\n", productWarehouseID) } fmt.Printf("Targets found: %d\n\n", len(rows)) if len(rows) == 0 { fmt.Println("No product warehouse found from purchase_items scope") return } negativePlan := 0 for _, row := range rows { if row.ComputedQty < 0 { negativePlan++ } fmt.Printf( "PLAN pw=%d product_id=%d product=%q current_qty=%.3f total_qty=%.3f allocated_qty=%.3f computed_qty=%.3f delta=%.3f\n", row.ProductWarehouseID, row.ProductID, row.ProductName, row.CurrentQty, row.SumTotalQty, row.SumAllocatedQty, row.ComputedQty, row.ComputedQty-row.CurrentQty, ) } if !apply { fmt.Println() fmt.Printf("Summary: planned=%d updated=0 skipped=0 failed=0 negative_plan=%d\n", len(rows), negativePlan) return } updated := 0 skipped := 0 negativeUpdated := 0 err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { for _, row := range rows { if nearlyEqual(row.CurrentQty, row.ComputedQty) { fmt.Printf( "SKIP pw=%d reason=no_change current_qty=%.3f computed_qty=%.3f\n", row.ProductWarehouseID, row.CurrentQty, row.ComputedQty, ) skipped++ continue } if err := tx.Table("product_warehouses"). Where("id = ?", row.ProductWarehouseID). Update("qty", row.ComputedQty).Error; err != nil { return fmt.Errorf("update qty for product_warehouse_id=%d: %w", row.ProductWarehouseID, err) } if row.ComputedQty < 0 { negativeUpdated++ } fmt.Printf( "DONE pw=%d product_id=%d product=%q old_qty=%.3f new_qty=%.3f\n", row.ProductWarehouseID, row.ProductID, row.ProductName, row.CurrentQty, row.ComputedQty, ) updated++ } return nil }) if err != nil { fmt.Println() fmt.Printf( "Summary: planned=%d updated=%d skipped=%d failed=1 negative_plan=%d negative_updated=%d\n", len(rows), updated, skipped, negativePlan, negativeUpdated, ) log.Printf("error: %v", err) os.Exit(1) } fmt.Println() fmt.Printf( "Summary: planned=%d updated=%d skipped=%d failed=0 negative_plan=%d negative_updated=%d\n", len(rows), updated, skipped, negativePlan, negativeUpdated, ) } func validateFlags(level int, productName string, productWarehouseID uint) error { switch level { case levelAll: if productName != "" { return errors.New("--product-name cannot be used on level 1") } if productWarehouseID > 0 { return errors.New("--product-warehouse-id cannot be used on level 1") } case levelByProductName: if productName == "" { return errors.New("--product-name is required on level 2") } if productWarehouseID > 0 { return errors.New("--product-warehouse-id cannot be used on level 2") } case levelByProductWarehouse: if productWarehouseID == 0 { return errors.New("--product-warehouse-id is required on level 3") } if productName != "" { return errors.New("--product-name cannot be used on level 3") } default: return fmt.Errorf("unsupported --level=%d (allowed: 1, 2, 3)", level) } return nil } func loadReflowRows( ctx context.Context, db *gorm.DB, level int, productName string, productWarehouseID uint, ) ([]reflowRow, error) { allocSub := db.WithContext(ctx). Table("stock_allocations sa"). Select(` sa.stockable_id, COALESCE(SUM(sa.qty), 0) AS used_qty `). Where("sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("sa.deleted_at IS NULL"). Group("sa.stockable_id") calcSub := db.WithContext(ctx). Table("purchase_items pi"). Select(` pi.product_warehouse_id, COALESCE(SUM(pi.total_qty), 0) AS sum_total_qty, COALESCE(SUM(COALESCE(alloc.used_qty, 0)), 0) AS sum_allocated_qty, COALESCE(SUM(COALESCE(pi.total_qty, 0) - COALESCE(alloc.used_qty, 0)), 0) AS computed_qty `). Joins("LEFT JOIN (?) alloc ON alloc.stockable_id = pi.id", allocSub). Where("pi.product_warehouse_id IS NOT NULL"). Group("pi.product_warehouse_id") query := db.WithContext(ctx). Table("product_warehouses pw"). Select(` pw.id AS product_warehouse_id, pw.product_id AS product_id, p.name AS product_name, COALESCE(pw.qty, 0) AS current_qty, calc.sum_total_qty, calc.sum_allocated_qty, calc.computed_qty `). Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN (?) calc ON calc.product_warehouse_id = pw.id", calcSub). Order("pw.id ASC") switch level { case levelByProductName: query = query.Where("LOWER(p.name) = LOWER(?)", productName) case levelByProductWarehouse: query = query.Where("pw.id = ?", productWarehouseID) } rows := make([]reflowRow, 0) if err := query.Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func modeLabel(apply bool) string { if apply { return "APPLY" } return "DRY-RUN" } func levelLabel(level int) string { switch level { case levelAll: return "all product_warehouse from purchase_items" case levelByProductName: return "specific product name" case levelByProductWarehouse: return "specific product_warehouse_id" default: return "unknown" } } func nearlyEqual(a, b float64) bool { return math.Abs(a-b) <= qtyEpsilon }