mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
298 lines
8.0 KiB
Go
298 lines
8.0 KiB
Go
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
|
|
}
|