Merge branch 'cmd/consolidate-and-repoint-stocks' into 'development'

cmd: check validate and fix bug risks in commands

See merge request mbugroup/lti-api!460
This commit is contained in:
Adnan Zahir
2026-04-24 01:49:27 +07:00
8 changed files with 52 additions and 29 deletions
@@ -30,12 +30,13 @@ type options struct {
}
type duplicateGroup struct {
WarehouseID uint `json:"warehouse_id"`
WarehouseName string `json:"warehouse_name"`
ProductID uint `json:"product_id"`
ProductName string `json:"product_name"`
AreaName string `json:"area_name"`
LocationName string `json:"location_name"`
WarehouseID uint `json:"warehouse_id"`
WarehouseName string `json:"warehouse_name"`
ProductID uint `json:"product_id"`
ProductName string `json:"product_name"`
AreaName string `json:"area_name"`
LocationName string `json:"location_name"`
ProjectFlockKandangID *uint `json:"project_flock_kandang_id,omitempty"`
SurvivorID uint `json:"survivor_id"`
SurvivorQty float64 `json:"survivor_qty"`
@@ -128,11 +129,12 @@ WITH duplicates AS (
p.name AS product_name,
COALESCE(a.name, 'N/A') AS area_name,
COALESCE(l.name, 'N/A') AS location_name,
pw.project_flock_kandang_id,
pw.id,
pw.qty,
MIN(pw.id) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS survivor_id,
COUNT(*) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS duplicate_count,
SUM(pw.qty) OVER (PARTITION BY pw.warehouse_id, pw.product_id) AS total_qty
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, pw.project_flock_kandang_id) AS duplicate_count,
SUM(pw.qty) OVER (PARTITION BY pw.warehouse_id, pw.product_id, pw.project_flock_kandang_id) AS total_qty
FROM product_warehouses pw
JOIN warehouses w ON w.id = pw.warehouse_id
JOIN products p ON p.id = pw.product_id
@@ -147,6 +149,7 @@ SELECT
product_name,
area_name,
location_name,
project_flock_kandang_id,
survivor_id,
(SELECT qty FROM duplicates d2 WHERE d2.id = survivor_id LIMIT 1) AS survivor_qty,
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
FROM duplicates
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
`, filters)
@@ -378,15 +381,20 @@ func renderConsolidation(mode string, groups []duplicateGroup, summary consolida
}
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 {
pfkID := "-"
if g.ProjectFlockKandangID != nil {
pfkID = fmt.Sprintf("%d", *g.ProjectFlockKandangID)
}
fmt.Fprintf(
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.LocationName,
g.WarehouseName,
g.ProductName,
pfkID,
g.SurvivorID,
g.SurvivorQty,
g.AbsorbedCount,
@@ -729,6 +729,9 @@ func reflowAndRecalculateProductWarehouse(
if err != nil {
return fmt.Errorf("resolve flag group for product_warehouse %d: %w", productWarehouseID, err)
}
if flagGroupCode == "" {
return nil
}
if _, err := fifoSvc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode,
@@ -778,6 +781,9 @@ func resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, produc
Order("rr.id ASC").
Limit(1).
Take(&selected).Error
if err == gorm.ErrRecordNotFound {
return "", nil
}
if err != nil {
return "", err
}
+10 -2
View File
@@ -400,8 +400,10 @@ 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 {
if err := ensureNoBlockedWarehouseRefs(ctx, db, rows, refs.BlockedWarehouseRefs); err != nil {
return err
if opts.DeleteWrongWarehouses {
if err := ensureNoBlockedWarehouseRefs(ctx, db, rows, refs.BlockedWarehouseRefs); err != nil {
return err
}
}
if err := ensureNoPurchaseItemWarehouseConflicts(ctx, db, rows); err != nil {
return err
@@ -813,6 +815,9 @@ func reflowAndRecalculateProductWarehouse(
if err != nil {
return fmt.Errorf("resolve flag group for product_warehouse %d: %w", productWarehouseID, err)
}
if flagGroupCode == "" {
return nil
}
if _, err := fifoSvc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: flagGroupCode,
@@ -862,6 +867,9 @@ func resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, produc
Order("rr.id ASC").
Limit(1).
Take(&selected).Error
if err == gorm.ErrRecordNotFound {
return "", nil
}
if err != nil {
return "", err
}
+16 -15
View File
@@ -39,7 +39,7 @@ type sourceWarehouseCheck struct {
KandangName string `json:"kandang_name"`
SourceWarehouseID uint `json:"source_warehouse_id"`
SourceWarehouseName string `json:"source_warehouse_name"`
Case string `json:"case"`
Case string `json:"case" gorm:"column:case_type"`
DeletedAt *string `json:"deleted_at"`
StockInProductWH float64 `json:"stock_in_product_wh"`
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) {
filters := buildFilters(opts)
filters, args := buildFilters(opts)
query := fmt.Sprintf(`
WITH case_a_warehouses AS (
-- 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))
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
}
@@ -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) {
filters := buildFilters(opts)
filters, args := buildFilters(opts)
query := fmt.Sprintf(`
SELECT
a.name AS area_name,
@@ -270,13 +270,11 @@ SELECT
COALESCE(SUM(sl.stock), 0) AS stock_logs_total,
COUNT(DISTINCT sl.id) AS stock_logs_count
FROM warehouses fw
JOIN locations loc ON loc.id = fw.location_id
JOIN areas a ON a.id = loc.area_id
JOIN kandangs k ON k.location_id = fw.location_id AND k.deleted_at IS NULL
JOIN locations kl ON kl.id = k.location_id
JOIN products p ON true
JOIN locations kl ON kl.id = fw.location_id
JOIN areas a ON a.id = kl.area_id
JOIN product_warehouses pw ON pw.warehouse_id = fw.id
JOIN products p ON p.id = pw.product_id
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
WHERE fw.deleted_at IS NULL
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))
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
}
@@ -371,15 +369,18 @@ func verifyOrphanedReferences(ctx context.Context, db *gorm.DB, sourceChecks []s
return results, nil
}
func buildFilters(opts *options) []string {
func buildFilters(opts *options) ([]string, []any) {
filters := make([]string, 0, 2)
args := make([]any, 0, 2)
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 != "" {
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 {
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.