mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
:
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
outputTable = "table"
|
||||
outputJSON = "json"
|
||||
|
||||
caseA = "A"
|
||||
caseB = "B"
|
||||
caseAll = "all"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
Output string
|
||||
AreaName string
|
||||
KandangLocationName string
|
||||
DBSSLMode string
|
||||
VerifyCase string
|
||||
}
|
||||
|
||||
type sourceWarehouseCheck struct {
|
||||
AreaName string `json:"area_name"`
|
||||
KandangLocationName string `json:"kandang_location_name"`
|
||||
KandangID uint `json:"kandang_id"`
|
||||
KandangName string `json:"kandang_name"`
|
||||
SourceWarehouseID uint `json:"source_warehouse_id"`
|
||||
SourceWarehouseName string `json:"source_warehouse_name"`
|
||||
Case string `json:"case"`
|
||||
DeletedAt *string `json:"deleted_at"`
|
||||
StockInProductWH float64 `json:"stock_in_product_wh"`
|
||||
ActivePurchaseItems int64 `json:"active_purchase_items"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type destinationWarehouseCheck struct {
|
||||
AreaName string `json:"area_name"`
|
||||
KandangLocationName string `json:"kandang_location_name"`
|
||||
FarmWarehouseID uint `json:"farm_warehouse_id"`
|
||||
FarmWarehouseName string `json:"farm_warehouse_name"`
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
CurrentQty float64 `json:"current_qty"`
|
||||
StockLogsTotal float64 `json:"stock_logs_total"`
|
||||
StockLogsCount int64 `json:"stock_logs_count"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type orphanedReferenceCheck struct {
|
||||
Table string `json:"table"`
|
||||
Column string `json:"column"`
|
||||
ReferenceCount int64 `json:"reference_count"`
|
||||
DeletedWarehouseIDs string `json:"deleted_warehouse_ids"`
|
||||
}
|
||||
|
||||
type verificationSummary struct {
|
||||
TotalSourceWarehouses int `json:"total_source_warehouses"`
|
||||
CleanSourceWarehouses int `json:"clean_source_warehouses"`
|
||||
DirtySourceWarehouses int `json:"dirty_source_warehouses"`
|
||||
TotalDestinationWarehouses int `json:"total_destination_warehouses"`
|
||||
MatchingDestinations int `json:"matching_destinations"`
|
||||
DiscrepancyDestinations int `json:"discrepancy_destinations"`
|
||||
TotalOrphanedReferences int64 `json:"total_orphaned_references"`
|
||||
OverallStatus string `json:"overall_status"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts, err := parseFlags()
|
||||
if err != nil {
|
||||
log.Fatalf("invalid flags: %v", err)
|
||||
}
|
||||
|
||||
if opts.DBSSLMode != "" {
|
||||
config.DBSSLMode = opts.DBSSLMode
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
db := database.Connect(config.DBHost, config.DBName)
|
||||
|
||||
// Verify source warehouses
|
||||
sourceChecks, err := verifySourceWarehouses(ctx, db, opts)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to verify source warehouses: %v", err)
|
||||
}
|
||||
|
||||
// Verify destination warehouses
|
||||
destChecks, err := verifyDestinationWarehouses(ctx, db, opts, sourceChecks)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to verify destination warehouses: %v", err)
|
||||
}
|
||||
|
||||
// Verify no orphaned references
|
||||
orphanedRefs, err := verifyOrphanedReferences(ctx, db, sourceChecks)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to verify orphaned references: %v", err)
|
||||
}
|
||||
|
||||
// Render results
|
||||
summary := buildSummary(sourceChecks, destChecks, orphanedRefs)
|
||||
renderVerification(opts.Output, sourceChecks, destChecks, orphanedRefs, summary)
|
||||
}
|
||||
|
||||
func parseFlags() (*options, error) {
|
||||
var opts options
|
||||
flag.StringVar(&opts.Output, "output", outputTable, "Output format: table or json")
|
||||
flag.StringVar(&opts.AreaName, "area-name", "", "Optional exact area name filter")
|
||||
flag.StringVar(&opts.KandangLocationName, "kandang-location-name", "", "Optional exact canonical kandang location filter")
|
||||
flag.StringVar(&opts.DBSSLMode, "db-sslmode", "", "Optional database sslmode override, for example: require")
|
||||
flag.StringVar(&opts.VerifyCase, "verify-case", caseAll, "Verify specific case: A, B, or all")
|
||||
flag.Parse()
|
||||
|
||||
opts.Output = strings.ToLower(strings.TrimSpace(opts.Output))
|
||||
opts.AreaName = strings.TrimSpace(opts.AreaName)
|
||||
opts.KandangLocationName = strings.TrimSpace(opts.KandangLocationName)
|
||||
opts.DBSSLMode = strings.TrimSpace(opts.DBSSLMode)
|
||||
opts.VerifyCase = strings.ToUpper(strings.TrimSpace(opts.VerifyCase))
|
||||
|
||||
if opts.Output == "" {
|
||||
opts.Output = outputTable
|
||||
}
|
||||
if opts.Output != outputTable && opts.Output != outputJSON {
|
||||
return nil, fmt.Errorf("unsupported --output=%s", opts.Output)
|
||||
}
|
||||
if opts.VerifyCase == "" {
|
||||
opts.VerifyCase = caseAll
|
||||
}
|
||||
if opts.VerifyCase != caseA && opts.VerifyCase != caseB && opts.VerifyCase != caseAll {
|
||||
return nil, fmt.Errorf("unsupported --verify-case=%s", opts.VerifyCase)
|
||||
}
|
||||
|
||||
return &opts, nil
|
||||
}
|
||||
|
||||
func verifySourceWarehouses(ctx context.Context, db *gorm.DB, opts *options) ([]sourceWarehouseCheck, error) {
|
||||
filters := buildFilters(opts)
|
||||
query := fmt.Sprintf(`
|
||||
WITH case_a_warehouses AS (
|
||||
-- Case A: Kandang-level warehouses (type != 'LOKASI' or NULL)
|
||||
SELECT
|
||||
w.id,
|
||||
a.name AS area_name,
|
||||
kl.name AS kandang_location_name,
|
||||
k.id,
|
||||
k.name,
|
||||
'A'::text AS case_type
|
||||
FROM warehouses w
|
||||
JOIN kandangs k ON k.id = w.kandang_id AND k.deleted_at IS NULL
|
||||
JOIN locations kl ON kl.id = k.location_id
|
||||
JOIN areas a ON a.id = kl.area_id
|
||||
WHERE w.deleted_at IS NOT NULL
|
||||
AND w.kandang_id IS NOT NULL
|
||||
AND UPPER(COALESCE(w.type, '')) <> 'LOKASI'
|
||||
),
|
||||
case_b_warehouses AS (
|
||||
-- Case B: Wrong-location warehouses (location_id != kandang.location_id)
|
||||
SELECT
|
||||
w.id,
|
||||
a.name AS area_name,
|
||||
kl.name AS kandang_location_name,
|
||||
k.id,
|
||||
k.name,
|
||||
'B'::text AS case_type
|
||||
FROM warehouses w
|
||||
JOIN kandangs k ON k.id = w.kandang_id AND k.deleted_at IS NULL
|
||||
JOIN locations kl ON kl.id = k.location_id
|
||||
JOIN areas a ON a.id = kl.area_id
|
||||
WHERE w.deleted_at IS NOT NULL
|
||||
AND w.kandang_id IS NOT NULL
|
||||
AND w.location_id IS DISTINCT FROM k.location_id
|
||||
),
|
||||
all_source_warehouses AS (
|
||||
SELECT id, area_name, kandang_location_name, id AS kandang_id, name, case_type FROM case_a_warehouses
|
||||
UNION ALL
|
||||
SELECT id, area_name, kandang_location_name, id AS kandang_id, name, case_type FROM case_b_warehouses
|
||||
)
|
||||
SELECT
|
||||
asw.area_name,
|
||||
asw.kandang_location_name,
|
||||
asw.kandang_id,
|
||||
asw.name AS kandang_name,
|
||||
w.id AS source_warehouse_id,
|
||||
w.name AS source_warehouse_name,
|
||||
asw.case_type,
|
||||
TO_CHAR(w.deleted_at, 'YYYY-MM-DD') AS deleted_at,
|
||||
COALESCE(SUM(pw.qty), 0) AS stock_in_product_wh,
|
||||
COUNT(DISTINCT pi.id) AS active_purchase_items
|
||||
FROM all_source_warehouses asw
|
||||
JOIN warehouses w ON w.id = asw.id
|
||||
LEFT JOIN product_warehouses pw ON pw.warehouse_id = w.id
|
||||
LEFT JOIN purchase_items pi ON pi.warehouse_id = w.id
|
||||
WHERE true
|
||||
%s
|
||||
GROUP BY
|
||||
asw.area_name,
|
||||
asw.kandang_location_name,
|
||||
asw.kandang_id,
|
||||
asw.name,
|
||||
w.id,
|
||||
w.name,
|
||||
asw.case_type,
|
||||
w.deleted_at
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine status for each row
|
||||
for i := range rows {
|
||||
if rows[i].StockInProductWH == 0 && rows[i].ActivePurchaseItems == 0 {
|
||||
rows[i].Status = "CLEAN"
|
||||
} else {
|
||||
rows[i].Status = "DIRTY"
|
||||
}
|
||||
|
||||
// Filter by case if requested
|
||||
if opts.VerifyCase != caseAll && rows[i].Case != opts.VerifyCase {
|
||||
rows = append(rows[:i], rows[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func verifyDestinationWarehouses(ctx context.Context, db *gorm.DB, opts *options, sourceChecks []sourceWarehouseCheck) ([]destinationWarehouseCheck, error) {
|
||||
filters := buildFilters(opts)
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
a.name AS area_name,
|
||||
kl.name AS kandang_location_name,
|
||||
fw.id AS farm_warehouse_id,
|
||||
fw.name AS farm_warehouse_name,
|
||||
p.id AS product_id,
|
||||
p.name AS product_name,
|
||||
COALESCE(pw.qty, 0) AS current_qty,
|
||||
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 UPPER(COALESCE(p.type, '')) 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'
|
||||
%s
|
||||
GROUP BY
|
||||
a.name,
|
||||
kl.name,
|
||||
fw.id,
|
||||
fw.name,
|
||||
p.id,
|
||||
p.name,
|
||||
pw.qty
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine status: check if current_qty matches stock_logs
|
||||
for i := range rows {
|
||||
if rows[i].CurrentQty > 0 {
|
||||
// Allow small floating point discrepancies
|
||||
if abs(rows[i].CurrentQty-rows[i].StockLogsTotal) < 0.001 {
|
||||
rows[i].Status = "MATCHED"
|
||||
} else {
|
||||
rows[i].Status = "DISCREPANCY"
|
||||
}
|
||||
} else {
|
||||
rows[i].Status = "EMPTY"
|
||||
}
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func verifyOrphanedReferences(ctx context.Context, db *gorm.DB, sourceChecks []sourceWarehouseCheck) ([]orphanedReferenceCheck, error) {
|
||||
if len(sourceChecks) == 0 {
|
||||
return []orphanedReferenceCheck{}, nil
|
||||
}
|
||||
|
||||
// Get unique warehouse IDs from source checks
|
||||
warehouseIDs := make([]uint, 0)
|
||||
for _, check := range sourceChecks {
|
||||
warehouseIDs = append(warehouseIDs, check.SourceWarehouseID)
|
||||
}
|
||||
|
||||
// Check common references
|
||||
var results []orphanedReferenceCheck
|
||||
|
||||
refChecks := []struct {
|
||||
table string
|
||||
column string
|
||||
}{
|
||||
{"purchase_items", "warehouse_id"},
|
||||
{"stock_transfers", "from_warehouse_id"},
|
||||
{"stock_transfers", "to_warehouse_id"},
|
||||
{"fifo_stock_v2_operation_log", "warehouse_id"},
|
||||
}
|
||||
|
||||
for _, ref := range refChecks {
|
||||
var count int64
|
||||
if err := db.Table(ref.table).
|
||||
Where(fmt.Sprintf("%s IN ?", ref.column), warehouseIDs).
|
||||
Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
// Get the specific warehouse IDs
|
||||
var ids []uint
|
||||
if err := db.Table(ref.table).
|
||||
Where(fmt.Sprintf("%s IN ?", ref.column), warehouseIDs).
|
||||
Pluck(ref.column, &ids).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idStrs := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
idStrs[i] = fmt.Sprintf("%d", id)
|
||||
}
|
||||
|
||||
results = append(results, orphanedReferenceCheck{
|
||||
Table: ref.table,
|
||||
Column: ref.column,
|
||||
ReferenceCount: count,
|
||||
DeletedWarehouseIDs: strings.Join(idStrs, ", "),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func buildFilters(opts *options) []string {
|
||||
filters := make([]string, 0, 2)
|
||||
if opts.AreaName != "" {
|
||||
filters = append(filters, fmt.Sprintf("a.name = '%s'", opts.AreaName))
|
||||
}
|
||||
if opts.KandangLocationName != "" {
|
||||
filters = append(filters, fmt.Sprintf("kl.name = '%s'", opts.KandangLocationName))
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
func andClause(filters []string) string {
|
||||
if len(filters) == 0 {
|
||||
return ""
|
||||
}
|
||||
return " AND " + strings.Join(filters, " AND ")
|
||||
}
|
||||
|
||||
func buildSummary(sourceChecks []sourceWarehouseCheck, destChecks []destinationWarehouseCheck, orphanedRefs []orphanedReferenceCheck) verificationSummary {
|
||||
summary := verificationSummary{
|
||||
TotalSourceWarehouses: len(sourceChecks),
|
||||
OverallStatus: "PASS",
|
||||
}
|
||||
|
||||
for _, check := range sourceChecks {
|
||||
if check.Status == "CLEAN" {
|
||||
summary.CleanSourceWarehouses++
|
||||
} else {
|
||||
summary.DirtySourceWarehouses++
|
||||
summary.OverallStatus = "FAIL"
|
||||
}
|
||||
}
|
||||
|
||||
summary.TotalDestinationWarehouses = len(destChecks)
|
||||
for _, check := range destChecks {
|
||||
if check.Status == "MATCHED" || check.Status == "EMPTY" {
|
||||
summary.MatchingDestinations++
|
||||
} else if check.Status == "DISCREPANCY" {
|
||||
summary.DiscrepancyDestinations++
|
||||
summary.OverallStatus = "FAIL"
|
||||
}
|
||||
}
|
||||
|
||||
for _, ref := range orphanedRefs {
|
||||
summary.TotalOrphanedReferences += ref.ReferenceCount
|
||||
summary.OverallStatus = "FAIL"
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
func renderVerification(mode string, sourceChecks []sourceWarehouseCheck, destChecks []destinationWarehouseCheck, orphanedRefs []orphanedReferenceCheck, summary verificationSummary) {
|
||||
if mode == outputJSON {
|
||||
payload := map[string]any{
|
||||
"source_warehouses": sourceChecks,
|
||||
"destination_warehouses": destChecks,
|
||||
"orphaned_references": orphanedRefs,
|
||||
"summary": summary,
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(payload)
|
||||
return
|
||||
}
|
||||
|
||||
// Table mode
|
||||
fmt.Println("\n=== SOURCE WAREHOUSES VERIFICATION ===")
|
||||
if len(sourceChecks) == 0 {
|
||||
fmt.Println("No deleted warehouses found")
|
||||
} else {
|
||||
w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "AREA\tLOKASI\tKANDANG\tWAREHOUSE\tCASE\tDELETED_AT\tSTOCK_IN_PW\tPURCHASE_ITEMS\tSTATUS")
|
||||
for _, check := range sourceChecks {
|
||||
fmt.Fprintf(
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t%.3f\t%d\t%s\n",
|
||||
check.AreaName,
|
||||
check.KandangLocationName,
|
||||
check.KandangName,
|
||||
check.SourceWarehouseName,
|
||||
check.Case,
|
||||
displayOptionalString(check.DeletedAt),
|
||||
check.StockInProductWH,
|
||||
check.ActivePurchaseItems,
|
||||
check.Status,
|
||||
)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
fmt.Println("\n=== DESTINATION WAREHOUSES VERIFICATION ===")
|
||||
if len(destChecks) == 0 {
|
||||
fmt.Println("No destination warehouses found")
|
||||
} else {
|
||||
w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "AREA\tLOKASI\tFARM_WAREHOUSE\tPRODUCT\tCURRENT_QTY\tSTOCK_LOGS_TOTAL\tLOGS_COUNT\tSTATUS")
|
||||
for _, check := range destChecks {
|
||||
fmt.Fprintf(
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\t%.3f\t%.3f\t%d\t%s\n",
|
||||
check.AreaName,
|
||||
check.KandangLocationName,
|
||||
check.FarmWarehouseName,
|
||||
check.ProductName,
|
||||
check.CurrentQty,
|
||||
check.StockLogsTotal,
|
||||
check.StockLogsCount,
|
||||
check.Status,
|
||||
)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
if len(orphanedRefs) > 0 {
|
||||
fmt.Println("\n=== ORPHANED REFERENCES (ERRORS) ===")
|
||||
w := tabwriter.NewWriter(os.Stdout, 2, 8, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "TABLE\tCOLUMN\tCOUNT\tWAREHOUSE_IDS")
|
||||
for _, ref := range orphanedRefs {
|
||||
fmt.Fprintf(
|
||||
w,
|
||||
"%s\t%s\t%d\t%s\n",
|
||||
ref.Table,
|
||||
ref.Column,
|
||||
ref.ReferenceCount,
|
||||
ref.DeletedWarehouseIDs,
|
||||
)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== SUMMARY ===\n")
|
||||
fmt.Printf("Source Warehouses: %d total, %d clean, %d dirty\n", summary.TotalSourceWarehouses, summary.CleanSourceWarehouses, summary.DirtySourceWarehouses)
|
||||
fmt.Printf("Destination Warehouses: %d total, %d matching, %d discrepancies\n", summary.TotalDestinationWarehouses, summary.MatchingDestinations, summary.DiscrepancyDestinations)
|
||||
fmt.Printf("Orphaned References: %d\n", summary.TotalOrphanedReferences)
|
||||
fmt.Printf("Overall Status: %s\n", summary.OverallStatus)
|
||||
}
|
||||
|
||||
func displayOptionalString(value *string) string {
|
||||
if value == nil || strings.TrimSpace(*value) == "" {
|
||||
return "-"
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func abs(x float64) float64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
# Stock Consolidation Operations Guide
|
||||
|
||||
This guide explains how to use the warehouse consolidation commands to fix misplaced PAKAN/OVK stocks and migrate them to the correct farm-level warehouses.
|
||||
|
||||
## Overview
|
||||
|
||||
The stock consolidation system handles two main scenarios:
|
||||
|
||||
| Case | Scenario | Root Cause | Solution |
|
||||
|------|----------|-----------|----------|
|
||||
| **Case B** | Invalid kandang references | Purchases pointed to warehouses with location mismatch | Move unused stocks to correct farm-level warehouse |
|
||||
| **Case A** | General kandang cleanup | Any kandang-level warehouse with unused PAKAN/OVK stocks | Consolidate to farm-level warehouse |
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
For a complete stock consolidation operation, follow this sequence:
|
||||
|
||||
```
|
||||
1. find-wrong-warehouse-records ← Diagnose issues
|
||||
2. repoint-wrong-warehouse-relations ← Fix Case B (invalid references)
|
||||
3. consolidate-kandang-to-farm-stocks ← Fix Case A (general cleanup)
|
||||
4. verify-stock-consolidation ← Audit and verify results
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
### 1. `find-wrong-warehouse-records` — Diagnostic Tool
|
||||
|
||||
**Purpose:** Identify problematic warehouses and their associated stocks before making any changes.
|
||||
|
||||
**Applies to:** Both Case A and Case B scenarios
|
||||
|
||||
**What it does:**
|
||||
- Lists warehouses with location mismatches (Case B)
|
||||
- Shows stock allocations that reference wrong warehouses
|
||||
- Helps identify scope of work needed
|
||||
|
||||
#### Usage:
|
||||
|
||||
```bash
|
||||
# Report 1: Find warehouses with location mismatches (Case B issues)
|
||||
./find-wrong-warehouse-records --report=warehouses
|
||||
|
||||
# Report 2: Find stock allocations in wrong warehouses (Case B impact)
|
||||
./find-wrong-warehouse-records --report=usage
|
||||
|
||||
# Filter by area
|
||||
./find-wrong-warehouse-records --report=warehouses --area-name "East Region"
|
||||
|
||||
# Filter by kandang location
|
||||
./find-wrong-warehouse-records --report=usage --kandang-location-name "Location 1"
|
||||
|
||||
# Filter by product type
|
||||
./find-wrong-warehouse-records --report=usage --usable-type=RECORDING_STOCK
|
||||
|
||||
# JSON output for analysis
|
||||
./find-wrong-warehouse-records --report=usage --output=json > analysis.json
|
||||
```
|
||||
|
||||
#### Output Columns (Warehouses Report):
|
||||
- **AREA**: Geographic area
|
||||
- **KANDANG_LOCATION**: Kandang's intended location
|
||||
- **KANDANG**: Kandang name
|
||||
- **WRONG_LOCATION**: Where the warehouse actually is
|
||||
- **WRONG_WAREHOUSE**: Problematic warehouse name
|
||||
- **CORRECT_WAREHOUSE**: Where stocks should be
|
||||
|
||||
#### Output Columns (Usage Report):
|
||||
- **USABLE_TYPE**: RECORDING_STOCK or MARKETING_DELIVERY
|
||||
- **PRODUCTS**: Which products are affected
|
||||
- **QTY_FROM_WRONG_STOCK**: How much stock is misplaced
|
||||
- **SOURCE_PURCHASES**: Which purchase orders are affected
|
||||
|
||||
**When to use:**
|
||||
- Before starting any consolidation
|
||||
- To understand the scope of issues
|
||||
- To get metrics on how much stock needs moving
|
||||
- To identify which areas are most affected
|
||||
|
||||
---
|
||||
|
||||
### 2. `repoint-wrong-warehouse-relations` — Fix Case B (Invalid References)
|
||||
|
||||
**Purpose:** Fix purchases pointed to invalid kandang warehouses (location mismatch).
|
||||
|
||||
**Applies to:** Case B only
|
||||
|
||||
**Cases it handles:**
|
||||
- ✅ Warehouses with `location_id ≠ kandang.location_id` (location mismatch)
|
||||
- ✅ Only PAKAN/OVK products
|
||||
- ✅ Only unused/leftover stocks (no active allocations)
|
||||
- ✅ Moves to farm-level warehouse at correct location
|
||||
|
||||
**What it does:**
|
||||
1. Finds product_warehouses in wrong locations
|
||||
2. Consolidates duplicates into survivor warehouses
|
||||
3. Updates all references across the system
|
||||
4. Recalculates FIFO stocks if needed
|
||||
5. Optionally soft-deletes the wrong warehouse
|
||||
|
||||
#### Usage:
|
||||
|
||||
```bash
|
||||
# Dry-run: See what would be moved (always run first!)
|
||||
./repoint-wrong-warehouse-relations
|
||||
|
||||
# Dry-run with specific filters
|
||||
./repoint-wrong-warehouse-relations --area-name "East Region"
|
||||
./repoint-wrong-warehouse-relations --kandang-location-name "Location 1"
|
||||
|
||||
# Actually apply the migration
|
||||
./repoint-wrong-warehouse-relations --apply
|
||||
|
||||
# Apply but keep the wrong warehouses (for audit trail)
|
||||
./repoint-wrong-warehouse-relations --apply --delete-wrong-warehouses=false
|
||||
|
||||
# JSON output for automation/logging
|
||||
./repoint-wrong-warehouse-relations --apply --output=json > migration.json
|
||||
```
|
||||
|
||||
#### Flags:
|
||||
- `--apply`: Apply changes (omit for dry-run)
|
||||
- `--output`: `table` (default) or `json`
|
||||
- `--area-name`: Filter by exact area name
|
||||
- `--kandang-location-name`: Filter by exact location name
|
||||
- `--delete-wrong-warehouses`: Soft-delete wrong warehouses (default: true)
|
||||
- `--db-sslmode`: PostgreSQL SSL mode override (e.g., `require`)
|
||||
|
||||
#### Output:
|
||||
|
||||
**Table mode shows:**
|
||||
- AREA, LOCATION, KANDANG: Where the issue is
|
||||
- WRONG_WAREHOUSE: Source (will be deleted)
|
||||
- TARGET_WAREHOUSE: Destination (farm-level)
|
||||
- PRODUCT: What's being moved
|
||||
- SURVIVOR_PW / ABSORBED_PW: Consolidation details
|
||||
- NEEDS_REFLOW: Whether FIFO recalculation is needed
|
||||
|
||||
**Summary shows:**
|
||||
```
|
||||
Summary: plan_rows=15 wrong_warehouses=3 survivor_pws=12 absorbed_pws=5
|
||||
needs_reflow_pws=3 deleted_product_warehouses=5 soft_deleted_warehouses=3
|
||||
|
||||
Updated product_warehouse refs:
|
||||
fifo_stock_v2_operation_log.product_warehouse_id=8
|
||||
fifo_stock_v2_reflow_checkpoints.product_warehouse_id=3
|
||||
purchase_items.warehouse_id=12
|
||||
|
||||
Updated warehouse refs:
|
||||
purchase_items.warehouse_id=12
|
||||
```
|
||||
|
||||
#### Safety Features:
|
||||
- **Dry-run first**: Always preview before applying
|
||||
- **Prechecks**: Verifies no blocked references or FIFO conflicts
|
||||
- **Atomic transactions**: All-or-nothing database updates
|
||||
- **Reference verification**: Confirms all references were updated
|
||||
- **Stock log recalculation**: Ensures FIFO accuracy after moves
|
||||
|
||||
---
|
||||
|
||||
### 3. `consolidate-kandang-to-farm-stocks` — Fix Case A (General Cleanup)
|
||||
|
||||
**Purpose:** Consolidate ALL kandang-level PAKAN/OVK stocks to farm-level warehouse.
|
||||
|
||||
**Applies to:** Case A only
|
||||
|
||||
**Cases it handles:**
|
||||
- ✅ ALL kandang-level warehouses (type ≠ 'LOKASI')
|
||||
- ✅ Only PAKAN/OVK products
|
||||
- ✅ Only unused/leftover stocks (no active allocations)
|
||||
- ✅ Moves to farm-level warehouse regardless of warehouse validity
|
||||
- ✅ No location validation (processes all kandang warehouses)
|
||||
|
||||
**What it does:**
|
||||
1. Finds all kandang-level warehouses with unused stocks
|
||||
2. Consolidates duplicates into survivor warehouses
|
||||
3. Updates all references across the system
|
||||
4. Recalculates FIFO stocks if needed
|
||||
5. Optionally soft-deletes the kandang warehouse
|
||||
|
||||
#### Usage:
|
||||
|
||||
```bash
|
||||
# Dry-run: See what would be consolidated
|
||||
./consolidate-kandang-to-farm-stocks
|
||||
|
||||
# Dry-run with filters
|
||||
./consolidate-kandang-to-farm-stocks --area-name "East Region"
|
||||
./consolidate-kandang-to-farm-stocks --kandang-location-name "Location 1"
|
||||
|
||||
# Actually apply the consolidation
|
||||
./consolidate-kandang-to-farm-stocks --apply
|
||||
|
||||
# Apply but keep kandang warehouses
|
||||
./consolidate-kandang-to-farm-stocks --apply --delete-kandang-warehouses=false
|
||||
|
||||
# JSON output for logging
|
||||
./consolidate-kandang-to-farm-stocks --apply --output=json > consolidation.json
|
||||
```
|
||||
|
||||
#### Flags:
|
||||
- `--apply`: Apply changes (omit for dry-run)
|
||||
- `--output`: `table` (default) or `json`
|
||||
- `--area-name`: Filter by exact area name
|
||||
- `--kandang-location-name`: Filter by exact location name
|
||||
- `--delete-kandang-warehouses`: Soft-delete kandang warehouses (default: true)
|
||||
- `--db-sslmode`: PostgreSQL SSL mode override
|
||||
|
||||
#### Output Format:
|
||||
Similar to Case B, shows:
|
||||
- Source kandang warehouse → Destination farm warehouse
|
||||
- Product and quantity details
|
||||
- Consolidation and FIFO reflow information
|
||||
|
||||
#### Key Differences from Case B:
|
||||
| Aspect | Case B | Case A |
|
||||
|--------|--------|--------|
|
||||
| Scope | Wrong-location warehouses only | ALL kandang-level warehouses |
|
||||
| Validation | Checks location mismatch | No validation checks |
|
||||
| When to use | After finding mismatches | General cleanup/consolidation |
|
||||
| Risk level | Lower (targeted fix) | Higher (broader scope) |
|
||||
|
||||
---
|
||||
|
||||
### 4. `verify-stock-consolidation` — Audit and Verify
|
||||
|
||||
**Purpose:** Verify that stock consolidations were successful and no stocks were lost.
|
||||
|
||||
**Applies to:** Both Case A and Case B (post-migration verification)
|
||||
|
||||
**What it checks:**
|
||||
|
||||
#### ✅ Source Warehouse Verification
|
||||
Ensures deleted warehouses are clean:
|
||||
- **CLEAN**: No remaining stock or purchase references
|
||||
- **DIRTY**: Still has orphaned data (migration incomplete)
|
||||
|
||||
#### ✅ Destination Warehouse Verification
|
||||
Ensures farm-level warehouses received stocks correctly:
|
||||
- **MATCHED**: Quantity in product_warehouse matches stock_logs
|
||||
- **DISCREPANCY**: Quantity mismatch (data integrity issue!)
|
||||
- **EMPTY**: No stocks (correct if nothing was supposed to move)
|
||||
|
||||
#### ✅ Orphaned Reference Detection
|
||||
Finds any remaining references to deleted warehouses in:
|
||||
- `purchase_items.warehouse_id`
|
||||
- `stock_transfers.from/to_warehouse_id`
|
||||
- `fifo_stock_v2_operation_log.warehouse_id`
|
||||
|
||||
#### Usage:
|
||||
|
||||
```bash
|
||||
# Verify all consolidations (Case A + B together)
|
||||
./verify-stock-consolidation
|
||||
|
||||
# Verify only Case B results
|
||||
./verify-stock-consolidation --verify-case=B
|
||||
|
||||
# Verify only Case A results
|
||||
./verify-stock-consolidation --verify-case=A
|
||||
|
||||
# Filter by area
|
||||
./verify-stock-consolidation --area-name "East Region"
|
||||
|
||||
# Filter by location
|
||||
./verify-stock-consolidation --kandang-location-name "Location 1"
|
||||
|
||||
# JSON output for reporting
|
||||
./verify-stock-consolidation --output=json > verification_report.json
|
||||
```
|
||||
|
||||
#### Flags:
|
||||
- `--verify-case`: `A`, `B`, or `all` (default)
|
||||
- `--output`: `table` (default) or `json`
|
||||
- `--area-name`: Filter by exact area name
|
||||
- `--kandang-location-name`: Filter by exact location name
|
||||
- `--db-sslmode`: PostgreSQL SSL mode override
|
||||
|
||||
#### Output Sections:
|
||||
|
||||
**1. Source Warehouses**
|
||||
```
|
||||
AREA LOKASI KANDANG WAREHOUSE CASE DELETED_AT STOCK PURCHASES STATUS
|
||||
Area A Location 1 Kandang A KWH-A-01 A 2026-04-23 0.000 0 CLEAN
|
||||
Area A Location 1 Kandang B WH-WRONG-001 B 2026-04-23 2.500 1 DIRTY ❌
|
||||
```
|
||||
|
||||
**2. Destination Warehouses**
|
||||
```
|
||||
AREA LOKASI FARM_WAREHOUSE PRODUCT QTY LOGS_TOTAL LOGS STATUS
|
||||
Area A Location 1 FWH-LOC-001 PAKAN A 2.500 2.500 3 MATCHED ✅
|
||||
Area A Location 1 FWH-LOC-001 OVK B 5.000 4.999 5 DISCREPANCY ❌
|
||||
```
|
||||
|
||||
**3. Orphaned References** (if any)
|
||||
```
|
||||
TABLE COLUMN COUNT WAREHOUSE_IDS
|
||||
purchase_items warehouse_id 3 1001, 1002, 1003
|
||||
stock_transfers from_warehouse_id 1 1001
|
||||
```
|
||||
|
||||
**4. Summary**
|
||||
```
|
||||
Source Warehouses: 10 total, 8 clean, 2 dirty
|
||||
Destination Warehouses: 15 total, 14 matching, 1 discrepancy
|
||||
Orphaned References: 4
|
||||
Overall Status: FAIL ❌
|
||||
```
|
||||
|
||||
#### Interpreting Results:
|
||||
|
||||
| Scenario | Meaning | Action |
|
||||
|----------|---------|--------|
|
||||
| ✅ Overall Status: PASS | All migrations successful | No action needed |
|
||||
| ❌ Dirty Source Warehouses | Stocks not fully moved | Re-run repoint/consolidate |
|
||||
| ❌ Discrepancy Destinations | Quantity mismatch | Investigate data integrity |
|
||||
| ❌ Orphaned References | Broken references remain | Manual cleanup needed |
|
||||
|
||||
---
|
||||
|
||||
## Complete Workflow Example
|
||||
|
||||
### Scenario: Consolidate East Region stocks
|
||||
|
||||
```bash
|
||||
# Step 1: Understand the scope (Case B issues)
|
||||
./find-wrong-warehouse-records --report=warehouses --area-name "East Region"
|
||||
./find-wrong-warehouse-records --report=usage --area-name "East Region"
|
||||
|
||||
# Review the output to understand:
|
||||
# - How many wrong warehouses
|
||||
# - How much stock needs moving
|
||||
# - Which products are affected
|
||||
|
||||
# Step 2: Fix Case B (invalid kandang references)
|
||||
./repoint-wrong-warehouse-relations --area-name "East Region"
|
||||
# Review dry-run output
|
||||
|
||||
./repoint-wrong-warehouse-relations --apply --area-name "East Region"
|
||||
# Watch for summary - should show successful updates
|
||||
|
||||
# Step 3: Fix Case A (general kandang cleanup)
|
||||
./consolidate-kandang-to-farm-stocks --area-name "East Region"
|
||||
# Review dry-run output
|
||||
|
||||
./consolidate-kandang-to-farm-stocks --apply --area-name "East Region"
|
||||
# Watch for summary - should show consolidation complete
|
||||
|
||||
# Step 4: Verify everything worked
|
||||
./verify-stock-consolidation --area-name "East Region"
|
||||
# Should show:
|
||||
# - All source warehouses: CLEAN
|
||||
# - All destination warehouses: MATCHED
|
||||
# - Orphaned references: 0
|
||||
# - Overall Status: PASS ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flags Reference
|
||||
|
||||
### Common Flags (All Commands)
|
||||
|
||||
| Flag | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| `--output` | Output format | `--output=json` |
|
||||
| `--area-name` | Filter by area | `--area-name "East Region"` |
|
||||
| `--kandang-location-name` | Filter by location | `--kandang-location-name "Location 1"` |
|
||||
| `--db-sslmode` | PostgreSQL SSL mode | `--db-sslmode=require` |
|
||||
|
||||
### Migration-Specific Flags
|
||||
|
||||
| Command | Flag | Description |
|
||||
|---------|------|-------------|
|
||||
| `repoint-wrong-warehouse-relations` | `--apply` | Apply changes |
|
||||
| `repoint-wrong-warehouse-relations` | `--delete-wrong-warehouses` | Delete wrong warehouses (default: true) |
|
||||
| `consolidate-kandang-to-farm-stocks` | `--apply` | Apply changes |
|
||||
| `consolidate-kandang-to-farm-stocks` | `--delete-kandang-warehouses` | Delete kandang warehouses (default: true) |
|
||||
| `verify-stock-consolidation` | `--verify-case` | Verify specific case (A, B, or all) |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Before Running Any Command
|
||||
|
||||
1. **Back up the database** — These operations modify stock data
|
||||
2. **Run in dry-run mode first** — Always preview changes before applying
|
||||
3. **Check during low-traffic periods** — Avoid peak hours
|
||||
4. **Have a rollback plan** — Know how to restore from backup if needed
|
||||
|
||||
### When Running Migrations
|
||||
|
||||
1. **Start small** — Use `--area-name` to test on one area first
|
||||
2. **Check the summary** — Verify numbers make sense
|
||||
3. **Watch for errors** — Stop if you see unexpected error messages
|
||||
4. **Run verification immediately after** — Don't wait to verify
|
||||
|
||||
### Red Flags (Stop and Investigate)
|
||||
|
||||
- ❌ More rows affected than expected
|
||||
- ❌ Negative quantities or zero counts where expecting data
|
||||
- ❌ Errors about blocked references
|
||||
- ❌ FIFO conflicts or in-flight artifacts
|
||||
- ❌ Very large numbers in NEEDS_REFLOW
|
||||
|
||||
### JSON Output for Automation
|
||||
|
||||
All commands support `--output=json` for:
|
||||
- Piping to other tools
|
||||
- Parsing in scripts
|
||||
- Generating reports
|
||||
- Integration with monitoring systems
|
||||
|
||||
```bash
|
||||
# Example: Extract all affected warehouses to CSV
|
||||
./find-wrong-warehouse-records --report=warehouses --output=json \
|
||||
| jq -r '.rows[] | [.area_name, .kandang_name, .wrong_warehouse_name] | @csv' \
|
||||
> affected_warehouses.csv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "No wrong warehouse relations found"
|
||||
- **Cause**: No matching Case B issues in the filter scope
|
||||
- **Solution**: Remove filters or use different criteria
|
||||
|
||||
### Issue: "found X rows still point to wrong warehouses"
|
||||
- **Cause**: References not fully migrated
|
||||
- **Solution**: Check for blocked references, re-run command
|
||||
|
||||
### Issue: "discrepancy_destinations > 0" in verification
|
||||
- **Cause**: Quantity mismatch in farm warehouse
|
||||
- **Solution**: Investigate manually or rollback and retry
|
||||
|
||||
### Issue: "DIRTY source warehouses" in verification
|
||||
- **Cause**: Deleted warehouses still have stock/references
|
||||
- **Solution**: May need manual cleanup or re-run migrations
|
||||
|
||||
---
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Commands use efficient SQL queries with proper filtering
|
||||
- Large operations (100K+ rows) may take a few minutes
|
||||
- Use area/location filters to reduce scope for testing
|
||||
- Dry-runs don't modify database and complete quickly
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Review the relevant section of this guide
|
||||
2. Check the command output for specific error messages
|
||||
3. Run verification to diagnose state issues
|
||||
4. Contact the development team with JSON outputs from failed operations
|
||||
Reference in New Issue
Block a user