cmd: check validate and fix bug risks in commands

This commit is contained in:
Adnan Zahir
2026-04-24 01:48:30 +07:00
parent 20479c2ca2
commit 9a19bff8d3
8 changed files with 52 additions and 29 deletions
@@ -36,6 +36,7 @@ type duplicateGroup struct {
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
AreaName string `json:"area_name"` AreaName string `json:"area_name"`
LocationName string `json:"location_name"` LocationName string `json:"location_name"`
ProjectFlockKandangID *uint `json:"project_flock_kandang_id,omitempty"`
SurvivorID uint `json:"survivor_id"` SurvivorID uint `json:"survivor_id"`
SurvivorQty float64 `json:"survivor_qty"` SurvivorQty float64 `json:"survivor_qty"`
@@ -128,11 +129,12 @@ WITH duplicates AS (
p.name AS product_name, p.name AS product_name,
COALESCE(a.name, 'N/A') AS area_name, COALESCE(a.name, 'N/A') AS area_name,
COALESCE(l.name, 'N/A') AS location_name, COALESCE(l.name, 'N/A') AS location_name,
pw.project_flock_kandang_id,
pw.id, pw.id,
pw.qty, pw.qty,
MIN(pw.id) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS survivor_id, MIN(pw.id) OVER (PARTITION BY pw.warehouse_id, pw.product_id, pw.project_flock_kandang_id) AS survivor_id,
COUNT(*) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS duplicate_count, COUNT(*) OVER (PARTITION BY pw.warehouse_id, pw.product_id, pw.project_flock_kandang_id) AS duplicate_count,
SUM(pw.qty) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS total_qty SUM(pw.qty) OVER (PARTITION BY pw.warehouse_id, pw.product_id, pw.project_flock_kandang_id) AS total_qty
FROM product_warehouses pw FROM product_warehouses pw
JOIN warehouses w ON w.id = pw.warehouse_id JOIN warehouses w ON w.id = pw.warehouse_id
JOIN products p ON p.id = pw.product_id JOIN products p ON p.id = pw.product_id
@@ -147,6 +149,7 @@ SELECT
product_name, product_name,
area_name, area_name,
location_name, location_name,
project_flock_kandang_id,
survivor_id, survivor_id,
(SELECT qty FROM duplicates d2 WHERE d2.id = survivor_id LIMIT 1) AS survivor_qty, (SELECT qty FROM duplicates d2 WHERE d2.id = survivor_id LIMIT 1) AS survivor_qty,
duplicate_count - 1 AS absorbed_count, duplicate_count - 1 AS absorbed_count,
@@ -154,7 +157,7 @@ SELECT
STRING_AGG(id::text, ', ' ORDER BY id::text) FILTER (WHERE id <> survivor_id) AS absorbed_ids STRING_AGG(id::text, ', ' ORDER BY id::text) FILTER (WHERE id <> survivor_id) AS absorbed_ids
FROM duplicates FROM duplicates
WHERE duplicate_count > 1 WHERE duplicate_count > 1
GROUP BY warehouse_id, warehouse_name, product_id, product_name, area_name, location_name, survivor_id, total_qty, duplicate_count GROUP BY warehouse_id, warehouse_name, product_id, product_name, area_name, location_name, project_flock_kandang_id, survivor_id, total_qty, duplicate_count
ORDER BY area_name, location_name, warehouse_name, product_name ORDER BY area_name, location_name, warehouse_name, product_name
`, filters) `, filters)
@@ -378,15 +381,20 @@ func renderConsolidation(mode string, groups []duplicateGroup, summary consolida
} }
w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
fmt.Fprintln(w, "AREA\tLOCATION\tWAREHOUSE\tPRODUCT\tSURVIVOR_ID\tSURVIVOR_QTY\tABSORBED_COUNT\tTOTAL_MERGED_QTY\tABSORBED_IDS") fmt.Fprintln(w, "AREA\tLOCATION\tWAREHOUSE\tPRODUCT\tPFK_ID\tSURVIVOR_ID\tSURVIVOR_QTY\tABSORBED_COUNT\tTOTAL_MERGED_QTY\tABSORBED_IDS")
for _, g := range groups { for _, g := range groups {
pfkID := "-"
if g.ProjectFlockKandangID != nil {
pfkID = fmt.Sprintf("%d", *g.ProjectFlockKandangID)
}
fmt.Fprintf( fmt.Fprintf(
w, w,
"%s\t%s\t%s\t%s\t%d\t%.3f\t%d\t%.3f\t%s\n", "%s\t%s\t%s\t%s\t%s\t%d\t%.3f\t%d\t%.3f\t%s\n",
g.AreaName, g.AreaName,
g.LocationName, g.LocationName,
g.WarehouseName, g.WarehouseName,
g.ProductName, g.ProductName,
pfkID,
g.SurvivorID, g.SurvivorID,
g.SurvivorQty, g.SurvivorQty,
g.AbsorbedCount, g.AbsorbedCount,
@@ -729,6 +729,9 @@ func reflowAndRecalculateProductWarehouse(
if err != nil { if err != nil {
return fmt.Errorf("resolve flag group for product_warehouse %d: %w", productWarehouseID, err) return fmt.Errorf("resolve flag group for product_warehouse %d: %w", productWarehouseID, err)
} }
if flagGroupCode == "" {
return nil
}
if _, err := fifoSvc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{ if _, err := fifoSvc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode, FlagGroupCode: flagGroupCode,
@@ -778,6 +781,9 @@ func resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, produc
Order("rr.id ASC"). Order("rr.id ASC").
Limit(1). Limit(1).
Take(&selected).Error Take(&selected).Error
if err == gorm.ErrRecordNotFound {
return "", nil
}
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -400,9 +400,11 @@ func buildReferencePlan(ctx context.Context, db *gorm.DB) (*referencePlan, error
} }
func runPrechecks(ctx context.Context, db *gorm.DB, rows []planRow, refs *referencePlan, opts *options) error { func runPrechecks(ctx context.Context, db *gorm.DB, rows []planRow, refs *referencePlan, opts *options) error {
if opts.DeleteWrongWarehouses {
if err := ensureNoBlockedWarehouseRefs(ctx, db, rows, refs.BlockedWarehouseRefs); err != nil { if err := ensureNoBlockedWarehouseRefs(ctx, db, rows, refs.BlockedWarehouseRefs); err != nil {
return err return err
} }
}
if err := ensureNoPurchaseItemWarehouseConflicts(ctx, db, rows); err != nil { if err := ensureNoPurchaseItemWarehouseConflicts(ctx, db, rows); err != nil {
return err return err
} }
@@ -813,6 +815,9 @@ func reflowAndRecalculateProductWarehouse(
if err != nil { if err != nil {
return fmt.Errorf("resolve flag group for product_warehouse %d: %w", productWarehouseID, err) return fmt.Errorf("resolve flag group for product_warehouse %d: %w", productWarehouseID, err)
} }
if flagGroupCode == "" {
return nil
}
if _, err := fifoSvc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{ if _, err := fifoSvc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode, FlagGroupCode: flagGroupCode,
@@ -862,6 +867,9 @@ func resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, produc
Order("rr.id ASC"). Order("rr.id ASC").
Limit(1). Limit(1).
Take(&selected).Error Take(&selected).Error
if err == gorm.ErrRecordNotFound {
return "", nil
}
if err != nil { if err != nil {
return "", err return "", err
} }
+16 -15
View File
@@ -39,7 +39,7 @@ type sourceWarehouseCheck struct {
KandangName string `json:"kandang_name"` KandangName string `json:"kandang_name"`
SourceWarehouseID uint `json:"source_warehouse_id"` SourceWarehouseID uint `json:"source_warehouse_id"`
SourceWarehouseName string `json:"source_warehouse_name"` SourceWarehouseName string `json:"source_warehouse_name"`
Case string `json:"case"` Case string `json:"case" gorm:"column:case_type"`
DeletedAt *string `json:"deleted_at"` DeletedAt *string `json:"deleted_at"`
StockInProductWH float64 `json:"stock_in_product_wh"` StockInProductWH float64 `json:"stock_in_product_wh"`
ActivePurchaseItems int64 `json:"active_purchase_items"` ActivePurchaseItems int64 `json:"active_purchase_items"`
@@ -145,7 +145,7 @@ func parseFlags() (*options, error) {
} }
func verifySourceWarehouses(ctx context.Context, db *gorm.DB, opts *options) ([]sourceWarehouseCheck, error) { func verifySourceWarehouses(ctx context.Context, db *gorm.DB, opts *options) ([]sourceWarehouseCheck, error) {
filters := buildFilters(opts) filters, args := buildFilters(opts)
query := fmt.Sprintf(` query := fmt.Sprintf(`
WITH case_a_warehouses AS ( WITH case_a_warehouses AS (
-- Case A: Kandang-level warehouses (type != 'LOKASI' or NULL) -- Case A: Kandang-level warehouses (type != 'LOKASI' or NULL)
@@ -234,7 +234,7 @@ ORDER BY asw.area_name ASC, asw.kandang_location_name ASC, w.name ASC
`, andClause(filters)) `, andClause(filters))
rows := make([]sourceWarehouseCheck, 0) rows := make([]sourceWarehouseCheck, 0)
if err := db.WithContext(ctx).Raw(query).Scan(&rows).Error; err != nil { if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
return nil, err return nil, err
} }
@@ -257,7 +257,7 @@ ORDER BY asw.area_name ASC, asw.kandang_location_name ASC, w.name ASC
} }
func verifyDestinationWarehouses(ctx context.Context, db *gorm.DB, opts *options, sourceChecks []sourceWarehouseCheck) ([]destinationWarehouseCheck, error) { func verifyDestinationWarehouses(ctx context.Context, db *gorm.DB, opts *options, sourceChecks []sourceWarehouseCheck) ([]destinationWarehouseCheck, error) {
filters := buildFilters(opts) filters, args := buildFilters(opts)
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT SELECT
a.name AS area_name, a.name AS area_name,
@@ -270,13 +270,11 @@ SELECT
COALESCE(SUM(sl.stock), 0) AS stock_logs_total, COALESCE(SUM(sl.stock), 0) AS stock_logs_total,
COUNT(DISTINCT sl.id) AS stock_logs_count COUNT(DISTINCT sl.id) AS stock_logs_count
FROM warehouses fw FROM warehouses fw
JOIN locations loc ON loc.id = fw.location_id JOIN locations kl ON kl.id = fw.location_id
JOIN areas a ON a.id = loc.area_id JOIN areas a ON a.id = kl.area_id
JOIN kandangs k ON k.location_id = fw.location_id AND k.deleted_at IS NULL JOIN product_warehouses pw ON pw.warehouse_id = fw.id
JOIN locations kl ON kl.id = k.location_id JOIN products p ON p.id = pw.product_id
JOIN products p ON true
JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = 'products' AND UPPER(f.name) IN ('PAKAN', 'OVK') JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = 'products' AND UPPER(f.name) IN ('PAKAN', 'OVK')
LEFT JOIN product_warehouses pw ON pw.warehouse_id = fw.id AND pw.product_id = p.id
LEFT JOIN stock_logs sl ON sl.product_warehouse_id = pw.id LEFT JOIN stock_logs sl ON sl.product_warehouse_id = pw.id
WHERE fw.deleted_at IS NULL WHERE fw.deleted_at IS NULL
AND UPPER(COALESCE(fw.type, '')) = 'LOKASI' AND UPPER(COALESCE(fw.type, '')) = 'LOKASI'
@@ -293,7 +291,7 @@ ORDER BY a.name ASC, kl.name ASC, fw.name ASC, p.name ASC
`, andClause(filters)) `, andClause(filters))
rows := make([]destinationWarehouseCheck, 0) rows := make([]destinationWarehouseCheck, 0)
if err := db.WithContext(ctx).Raw(query).Scan(&rows).Error; err != nil { if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
return nil, err return nil, err
} }
@@ -371,15 +369,18 @@ func verifyOrphanedReferences(ctx context.Context, db *gorm.DB, sourceChecks []s
return results, nil return results, nil
} }
func buildFilters(opts *options) []string { func buildFilters(opts *options) ([]string, []any) {
filters := make([]string, 0, 2) filters := make([]string, 0, 2)
args := make([]any, 0, 2)
if opts.AreaName != "" { if opts.AreaName != "" {
filters = append(filters, fmt.Sprintf("a.name = '%s'", opts.AreaName)) filters = append(filters, "a.name = ?")
args = append(args, opts.AreaName)
} }
if opts.KandangLocationName != "" { if opts.KandangLocationName != "" {
filters = append(filters, fmt.Sprintf("kl.name = '%s'", opts.KandangLocationName)) filters = append(filters, "kl.name = ?")
args = append(args, opts.KandangLocationName)
} }
return filters return filters, args
} }
func andClause(filters []string) string { func andClause(filters []string) string {
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.