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" "gorm.io/gorm" ) const ( levelAllNoFlagProducts = 1 levelProductName = 2 levelProductWarehouse = 3 qtyEpsilon = 1e-6 ) type targetRow 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"` ComputedQty float64 `gorm:"column:computed_qty"` } func main() { var ( level int productName string productWarehouseID uint apply bool ) flag.IntVar( &level, "level", levelAllNoFlagProducts, "CLI level: 1=all products without flags, 2=specific product name (with flags), 3=specific product warehouse id", ) 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.BoolVar(&apply, "apply", false, "Apply changes. If false, run as dry-run") 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) targets, err := loadTargets(ctx, db, level, productName, productWarehouseID) if err != nil { log.Fatalf("failed to load target product warehouses: %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(targets)) if len(targets) == 0 { fmt.Println("No matching product warehouse rows to process") return } for _, row := range targets { fmt.Printf( "PLAN pw=%d product_id=%d product=%q current_qty=%.3f computed_qty=%.3f delta=%.3f\n", row.ProductWarehouseID, row.ProductID, row.ProductName, row.CurrentQty, row.ComputedQty, row.ComputedQty-row.CurrentQty, ) } if !apply { fmt.Println() fmt.Printf("Summary: planned=%d updated=0 skipped=0 failed=0\n", len(targets)) return } updated := 0 skipped := 0 err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { for _, row := range targets { 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) } 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\n", len(targets), updated, skipped) log.Printf("error: %v", err) os.Exit(1) } fmt.Println() fmt.Printf("Summary: planned=%d updated=%d skipped=%d failed=0\n", len(targets), updated, skipped) } func validateFlags(level int, productName string, productWarehouseID uint) error { switch level { case levelAllNoFlagProducts: 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 levelProductName: 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 levelProductWarehouse: 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 loadTargets( ctx context.Context, db *gorm.DB, level int, productName string, productWarehouseID uint, ) ([]targetRow, error) { switch level { case levelAllNoFlagProducts: return loadTargetsLevel1ByProductWithoutFlags(ctx, db) case levelProductName: return loadTargetsLevel2ByProductWarehouseWithFlags(ctx, db, productName) case levelProductWarehouse: return loadTargetByProductWarehouseID(ctx, db, productWarehouseID) default: return nil, fmt.Errorf("unsupported level %d", level) } } func loadTargetsLevel1ByProductWithoutFlags(ctx context.Context, db *gorm.DB) ([]targetRow, error) { rows := make([]targetRow, 0) if err := 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, COALESCE(SUM(pi.total_qty), 0) AS computed_qty `). Joins("JOIN products p ON p.id = pw.product_id"). Joins("LEFT JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). Where("p.deleted_at IS NULL"). Where("f.id IS NULL"). Group("pw.id, pw.product_id, p.name, pw.qty"). Order("pw.id ASC"). Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func loadTargetsLevel2ByProductWarehouseWithFlags( ctx context.Context, db *gorm.DB, productName string, ) ([]targetRow, error) { rows := make([]targetRow, 0) if err := 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, COALESCE(SUM(pi.total_qty), 0) AS computed_qty `). Joins("JOIN products p ON p.id = pw.product_id"). Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). Where("p.deleted_at IS NULL"). Where(` EXISTS ( SELECT 1 FROM flags f WHERE f.flagable_id = p.id AND f.flagable_type = ? ) `, entity.FlagableTypeProduct). Where("LOWER(p.name) = LOWER(?)", productName). Group("pw.id, pw.product_id, p.name, pw.qty"). Order("pw.id ASC"). Scan(&rows).Error; err != nil { return nil, err } return rows, nil } func loadTargetByProductWarehouseID(ctx context.Context, db *gorm.DB, productWarehouseID uint) ([]targetRow, error) { rows := make([]targetRow, 0) if err := 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, COALESCE(SUM(pi.total_qty), 0) AS computed_qty `). Joins("JOIN products p ON p.id = pw.product_id"). Joins("LEFT JOIN purchase_items pi ON pi.product_warehouse_id = pw.id"). Where("pw.id = ?", productWarehouseID). Group("pw.id, pw.product_id, p.name, pw.qty"). 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 levelAllNoFlagProducts: return "all products without flags (source: purchase_items by product_warehouse_id)" case levelProductName: return "specific product name with flags (source: purchase_items by product_warehouse_id)" case levelProductWarehouse: return "specific product_warehouse_id (source: purchase_items by product_warehouse_id)" default: return "unknown" } } func nearlyEqual(a, b float64) bool { return math.Abs(a-b) <= qtyEpsilon }