diff --git a/internal/database/migrations/20260528100000_normalize_warehouse_jamali_10_to_25.down.sql b/internal/database/migrations/20260528100000_normalize_warehouse_jamali_10_to_25.down.sql new file mode 100644 index 00000000..a707590d --- /dev/null +++ b/internal/database/migrations/20260528100000_normalize_warehouse_jamali_10_to_25.down.sql @@ -0,0 +1,99 @@ +BEGIN; + +-- ============================================================ +-- Rollback dynamic via audit snapshots di schema `migration_audit.jamali_w10_*`. +-- Semua reverse dibaca dari snapshot yang dibuat oleh UP migration — +-- tidak ada IDs/qty yang hardcode. Robust terhadap data drift antara +-- dump time dan UP apply time (misalnya row baru warehouse_id=10 +-- yang muncul setelah dump diambil). +-- +-- LIMITASI: FK relinks di stock_logs / stock_allocations / recording_eggs / +-- marketing_products / dll. TIDAK direverse di sini (skip audit per-row +-- untuk hemat storage ~40MB). Setelah down, 9 PW W10 yang di-restore +-- akan kosong dari child rows (semua child masih pointing ke W25 PW +-- yang sebelumnya menerima merge). Untuk rollback penuh, restore DB +-- dari backup pre-migration. +-- ============================================================ + +-- Guard: pastikan audit tables ada (kalau tidak, fail-loud) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'migration_audit' + AND table_name = 'jamali_w10_pw_deleted_snapshot' + ) THEN + RAISE EXCEPTION 'Audit table migration_audit.jamali_w10_* tidak ditemukan. UP migration belum dijalankan atau audit sudah di-drop. Restore dari DB backup jika perlu.'; + END IF; +END $$; + +-- 1. Un-soft-delete warehouse 10 (kalau memang di-softdelete oleh UP) +UPDATE warehouses w +SET deleted_at = NULL, updated_at = NOW() +FROM migration_audit.jamali_w10_warehouse_softdeleted a +WHERE w.id = a.id; + +-- 2. Un-soft-delete stock_transfers self-loop yang disoft-delete UP step 7.1 +UPDATE stock_transfers st +SET deleted_at = NULL, updated_at = NOW() +FROM migration_audit.jamali_w10_st_softdeleted a +WHERE st.id = a.id; + +-- 3. Reverse stock_transfers redirect (CASE-based dari snapshot was_from_w10/was_to_w10) +UPDATE stock_transfers st +SET from_warehouse_id = CASE WHEN a.was_from_w10 THEN 10 ELSE st.from_warehouse_id END, + to_warehouse_id = CASE WHEN a.was_to_w10 THEN 10 ELSE st.to_warehouse_id END, + updated_at = NOW() +FROM migration_audit.jamali_w10_st_redirected a +WHERE st.id = a.id; + +-- 3b. Self-loop transfers (W10<->W25 awal) juga punya from_warehouse_id=25 atau +-- to_warehouse_id=25 setelah UP step 7.2. Karena snapshot jamali_w10_st_softdeleted +-- punya kolom from_warehouse_id & to_warehouse_id asli, pakai itu untuk reverse. +UPDATE stock_transfers st +SET from_warehouse_id = 10, updated_at = NOW() +FROM migration_audit.jamali_w10_st_softdeleted a +WHERE st.id = a.id AND a.from_warehouse_id = 10; + +UPDATE stock_transfers st +SET to_warehouse_id = 10, updated_at = NOW() +FROM migration_audit.jamali_w10_st_softdeleted a +WHERE st.id = a.id AND a.to_warehouse_id = 10; + +-- 4. Reverse purchase_items.warehouse_id 25 -> 10 +UPDATE purchase_items +SET warehouse_id = 10 +WHERE id IN (SELECT id FROM migration_audit.jamali_w10_purchase_items); + +-- 5. Reverse W10-only PW (warehouse_id 25 -> 10, restore pfk asli dari snapshot) +UPDATE product_warehouses pw +SET warehouse_id = 10, project_flock_kandang_id = a.original_pfk +FROM migration_audit.jamali_w10_pw_w10only_snapshot a +WHERE pw.id = a.id; + +-- 6. Subtract qty dari W25 PW (reverse merge) +-- WARNING: kalau W25 qty sudah dikonsumsi pasca-UP (sales/recording/dll), +-- hasil bisa negatif. Tidak ada CHECK constraint di product_warehouses.qty, +-- jadi silent. Operator harus verifikasi manual post-down: +-- SELECT id, qty FROM product_warehouses WHERE qty < 0; +UPDATE product_warehouses pw +SET qty = pw.qty - a.merged_qty +FROM migration_audit.jamali_w10_qty_merge a +WHERE pw.id = a.target_pw_id; + +-- 7. Re-INSERT 9 W10 PW rows yang di-DELETE oleh UP (PK asli + qty asli) +INSERT INTO product_warehouses (id, product_id, warehouse_id, qty, project_flock_kandang_id) +SELECT id, product_id, 10, qty, project_flock_kandang_id +FROM migration_audit.jamali_w10_pw_deleted_snapshot; + +-- 8. Cleanup audit tables (drop satu per satu, tidak wildcard untuk safety) +DROP TABLE migration_audit.jamali_w10_pw_deleted_snapshot; +DROP TABLE migration_audit.jamali_w10_qty_merge; +DROP TABLE migration_audit.jamali_w10_pw_w10only_snapshot; +DROP TABLE migration_audit.jamali_w10_st_softdeleted; +DROP TABLE migration_audit.jamali_w10_st_redirected; +DROP TABLE migration_audit.jamali_w10_purchase_items; +DROP TABLE migration_audit.jamali_w10_warehouse_softdeleted; +-- Schema migration_audit dipertahankan (bisa dipakai migration lain di masa depan) + +COMMIT; diff --git a/internal/database/migrations/20260528100000_normalize_warehouse_jamali_10_to_25.up.sql b/internal/database/migrations/20260528100000_normalize_warehouse_jamali_10_to_25.up.sql new file mode 100644 index 00000000..663b98d1 --- /dev/null +++ b/internal/database/migrations/20260528100000_normalize_warehouse_jamali_10_to_25.up.sql @@ -0,0 +1,241 @@ +BEGIN; + +-- ============================================================ +-- Normalisasi warehouse 10 (Jamali NON_AKTIF) -> 25 (Gudang Farm Jamali) +-- Background: Dua warehouse LOKASI di area & lokasi sama (area_id=6, +-- location_id=16). W10 sudah ditandai NON_AKTIF tapi masih punya 13 +-- product_warehouses, 3,590 stock_logs, ~790K stock_allocations, +-- 332 marketing_products, 17 purchase_items, dan 14 stock_transfers. +-- Migration ini konsolidasikan semua relasi ke W25 lalu soft-delete W10. +-- +-- Klasifikasi data: +-- A. 9 product_warehouses W10 overlap dengan W25 (sama product_id, pfk=NULL) +-- -> merge qty ke W25, relink semua FK ke product_warehouses.id, +-- lalu DELETE W10 PW rows. +-- B. 4 product_warehouses W10-only -> UPDATE warehouse_id=25. +-- Rows 1188/1189/1190 punya pfk=98 (anomali LOKASI, seharusnya NULL +-- per aturan di CLAUDE.md [2026-05-06]) -> normalisasi sekalian. +-- C. 17 purchase_items.warehouse_id=10 -> UPDATE 25 (no unique conflict). +-- D. 3 stock_transfers W10<->W25 (PND-LTI-00107/00109/00119) akan jadi +-- self-loop W25<->W25 setelah merge -> soft-delete. +-- E. 12 stock_transfers EGG_FARM_CUTOVER to_warehouse_id=10 -> UPDATE 25. +-- F. warehouse_id=10 sendiri -> soft-delete. +-- +-- UP membuat 7 snapshot table di schema `migration_audit.jamali_w10_*` +-- sebelum mutasi. DOWN baca snapshot itu untuk reverse dynamic (tidak +-- hardcode IDs/qty), sehingga apapun yang ada di production saat UP +-- dijalankan akan ter-audit dan ter-reverse. FK relinks +-- (stock_logs/stock_allocations/dll) TIDAK di-audit (storage ~40MB) +-- — limitation: tidak bisa di-reverse DOWN, full rollback = DB backup. +-- ============================================================ + +-- STEP -1: Buat schema audit + snapshot tables (idempotent rerun via DROP IF EXISTS) +CREATE SCHEMA IF NOT EXISTS migration_audit; + +DROP TABLE IF EXISTS migration_audit.jamali_w10_pw_deleted_snapshot; +DROP TABLE IF EXISTS migration_audit.jamali_w10_qty_merge; +DROP TABLE IF EXISTS migration_audit.jamali_w10_pw_w10only_snapshot; +DROP TABLE IF EXISTS migration_audit.jamali_w10_st_softdeleted; +DROP TABLE IF EXISTS migration_audit.jamali_w10_st_redirected; +DROP TABLE IF EXISTS migration_audit.jamali_w10_purchase_items; +DROP TABLE IF EXISTS migration_audit.jamali_w10_warehouse_softdeleted; + +-- Snapshot 9 W10 PW yang akan di-DELETE (overlap dgn W25, pfk=NULL) +CREATE TABLE migration_audit.jamali_w10_pw_deleted_snapshot AS +SELECT pw10.id, pw10.product_id, pw10.qty, pw10.project_flock_kandang_id +FROM product_warehouses pw10 +JOIN product_warehouses pw25 + ON pw25.product_id = pw10.product_id + AND pw25.warehouse_id = 25 + AND pw25.project_flock_kandang_id IS NULL +WHERE pw10.warehouse_id = 10 AND pw10.project_flock_kandang_id IS NULL; + +-- Snapshot qty delta per W25 target (untuk reverse subtract) +CREATE TABLE migration_audit.jamali_w10_qty_merge AS +SELECT pw25.id AS target_pw_id, pw10.id AS source_pw_id, pw10.qty AS merged_qty +FROM product_warehouses pw10 +JOIN product_warehouses pw25 + ON pw25.product_id = pw10.product_id + AND pw25.warehouse_id = 25 + AND pw25.project_flock_kandang_id IS NULL +WHERE pw10.warehouse_id = 10 AND pw10.project_flock_kandang_id IS NULL; + +-- Snapshot W10-only PW (yang akan di-UPDATE warehouse_id 10->25) +CREATE TABLE migration_audit.jamali_w10_pw_w10only_snapshot AS +SELECT pw10.id, pw10.project_flock_kandang_id AS original_pfk +FROM product_warehouses pw10 +WHERE pw10.warehouse_id = 10 + AND pw10.id NOT IN (SELECT id FROM migration_audit.jamali_w10_pw_deleted_snapshot); + +-- Snapshot stock_transfers yang akan di-soft-delete (self-loop W10<->W25) +-- Simpan from/to_warehouse_id asli supaya DOWN bisa reverse direction tepat +CREATE TABLE migration_audit.jamali_w10_st_softdeleted AS +SELECT id, movement_number, from_warehouse_id, to_warehouse_id +FROM stock_transfers +WHERE deleted_at IS NULL + AND ((from_warehouse_id = 10 AND to_warehouse_id = 25) + OR (from_warehouse_id = 25 AND to_warehouse_id = 10)); + +-- Snapshot stock_transfers yang akan di-UPDATE (W10<->other, bukan self-loop) +CREATE TABLE migration_audit.jamali_w10_st_redirected AS +SELECT id, + (from_warehouse_id = 10) AS was_from_w10, + (to_warehouse_id = 10) AS was_to_w10 +FROM stock_transfers +WHERE deleted_at IS NULL + AND (from_warehouse_id = 10 OR to_warehouse_id = 10) + AND id NOT IN (SELECT id FROM migration_audit.jamali_w10_st_softdeleted); + +-- Snapshot purchase_items IDs (cheap, ~17 rows) +CREATE TABLE migration_audit.jamali_w10_purchase_items AS +SELECT id FROM purchase_items WHERE warehouse_id = 10; + +-- Snapshot warehouses soft-delete flag (1 row, kalau memang masih aktif) +CREATE TABLE migration_audit.jamali_w10_warehouse_softdeleted AS +SELECT id FROM warehouses WHERE id = 10 AND deleted_at IS NULL; + +-- STEP 0: Pre-check sanity (idempotent guards) +DO $$ +DECLARE v_count INT; +BEGIN + SELECT COUNT(*) INTO v_count FROM warehouses + WHERE id IN (10, 25) AND type = 'LOKASI' AND area_id = 6 AND location_id = 16; + IF v_count <> 2 THEN + RAISE EXCEPTION 'Pre-check: warehouse 10/25 schema mismatch (got % rows)', v_count; + END IF; + + SELECT COUNT(*) INTO v_count FROM purchase_items a + JOIN purchase_items b ON a.purchase_id = b.purchase_id + AND a.product_id = b.product_id + AND a.id <> b.id + WHERE a.warehouse_id = 10 AND b.warehouse_id = 25; + IF v_count > 0 THEN + RAISE EXCEPTION 'Pre-check: % purchase_items unique conflict (purchase_id,product_id)', v_count; + END IF; +END $$; + +-- STEP 1: Merge qty W10 -> W25 untuk overlap (pfk=NULL) +UPDATE product_warehouses pw25 +SET qty = pw25.qty + pw10.qty +FROM product_warehouses pw10 +WHERE pw10.warehouse_id = 10 AND pw10.project_flock_kandang_id IS NULL + AND pw25.warehouse_id = 25 AND pw25.project_flock_kandang_id IS NULL + AND pw25.product_id = pw10.product_id; + +-- STEP 2: Build temp mapping (W10 PW id -> W25 PW id) untuk overlap saja +CREATE TEMP TABLE _pw_map ON COMMIT DROP AS +SELECT pw10.id AS old_id, pw25.id AS new_id +FROM product_warehouses pw10 +JOIN product_warehouses pw25 + ON pw25.product_id = pw10.product_id + AND pw25.warehouse_id = 25 + AND pw25.project_flock_kandang_id IS NULL +WHERE pw10.warehouse_id = 10 AND pw10.project_flock_kandang_id IS NULL; + +CREATE INDEX ON _pw_map(old_id); + +-- STEP 3: Relink semua FK ke product_warehouses.id (hanya rows di _pw_map) +UPDATE stock_logs SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE stock_logs.product_warehouse_id = m.old_id; + +UPDATE stock_allocations SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE stock_allocations.product_warehouse_id = m.old_id; + +UPDATE recording_eggs SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE recording_eggs.product_warehouse_id = m.old_id; + +UPDATE recording_stocks SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE recording_stocks.product_warehouse_id = m.old_id; + +UPDATE recording_depletions SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE recording_depletions.product_warehouse_id = m.old_id; +UPDATE recording_depletions SET source_product_warehouse_id = m.new_id + FROM _pw_map m WHERE recording_depletions.source_product_warehouse_id = m.old_id; + +UPDATE adjustment_stocks SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE adjustment_stocks.product_warehouse_id = m.old_id; + +UPDATE marketing_products SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE marketing_products.product_warehouse_id = m.old_id; + +UPDATE marketing_delivery_products SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE marketing_delivery_products.product_warehouse_id = m.old_id; + +UPDATE project_chickins SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE project_chickins.product_warehouse_id = m.old_id; + +UPDATE project_chickin_details SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE project_chickin_details.product_warehouse_id = m.old_id; + +UPDATE project_flock_populations SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE project_flock_populations.product_warehouse_id = m.old_id; + +UPDATE laying_transfers SET source_product_warehouse_id = m.new_id + FROM _pw_map m WHERE laying_transfers.source_product_warehouse_id = m.old_id; + +UPDATE laying_transfer_sources SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE laying_transfer_sources.product_warehouse_id = m.old_id; + +UPDATE laying_transfer_targets SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE laying_transfer_targets.product_warehouse_id = m.old_id; + +UPDATE stock_transfer_details SET source_product_warehouse_id = m.new_id + FROM _pw_map m WHERE stock_transfer_details.source_product_warehouse_id = m.old_id; +UPDATE stock_transfer_details SET dest_product_warehouse_id = m.new_id + FROM _pw_map m WHERE stock_transfer_details.dest_product_warehouse_id = m.old_id; + +UPDATE purchase_items SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE purchase_items.product_warehouse_id = m.old_id; + +-- FIFO v2 tables (kosong di dump 2026-05-25, defensive) +UPDATE fifo_stock_v2_operation_log SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE fifo_stock_v2_operation_log.product_warehouse_id = m.old_id; +UPDATE fifo_stock_v2_reflow_checkpoints SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE fifo_stock_v2_reflow_checkpoints.product_warehouse_id = m.old_id; +UPDATE fifo_stock_v2_shadow_allocations SET product_warehouse_id = m.new_id + FROM _pw_map m WHERE fifo_stock_v2_shadow_allocations.product_warehouse_id = m.old_id; + +-- STEP 4: Hard-delete W10 PW yang sudah merged (9 rows expected) +DELETE FROM product_warehouses WHERE id IN (SELECT old_id FROM _pw_map); + +-- STEP 5: Sisa W10 PW (4 rows: 1188/1189/1190/1196) -> warehouse_id=25, +-- pfk dinormalisasi ke NULL sekalian (LOKASI rule) +UPDATE product_warehouses +SET warehouse_id = 25, project_flock_kandang_id = NULL +WHERE warehouse_id = 10; + +-- STEP 6: purchase_items.warehouse_id (17 rows) +UPDATE purchase_items SET warehouse_id = 25 WHERE warehouse_id = 10; + +-- STEP 7: stock_transfers +-- 7.1 Soft-delete self-loop (W10<->W25 akan jadi W25<->W25) +UPDATE stock_transfers +SET deleted_at = NOW(), updated_at = NOW() +WHERE deleted_at IS NULL + AND ((from_warehouse_id = 10 AND to_warehouse_id = 25) + OR (from_warehouse_id = 25 AND to_warehouse_id = 10)); + +-- 7.2 Sisa W10<->other -> 25 (12 EGG_FARM_CUTOVER ke W10) +UPDATE stock_transfers SET from_warehouse_id = 25, updated_at = NOW() WHERE from_warehouse_id = 10; +UPDATE stock_transfers SET to_warehouse_id = 25, updated_at = NOW() WHERE to_warehouse_id = 10; + +-- STEP 8: Soft-delete warehouse 10 sendiri +UPDATE warehouses SET deleted_at = NOW(), updated_at = NOW() +WHERE id = 10 AND deleted_at IS NULL; + +-- STEP 9: Post-check (fail-fast jika ada residu) +DO $$ +DECLARE v_count INT; +BEGIN + SELECT COUNT(*) INTO v_count FROM product_warehouses WHERE warehouse_id = 10; + IF v_count <> 0 THEN RAISE EXCEPTION 'product_warehouses W10 residual %', v_count; END IF; + + SELECT COUNT(*) INTO v_count FROM purchase_items WHERE warehouse_id = 10; + IF v_count <> 0 THEN RAISE EXCEPTION 'purchase_items W10 residual %', v_count; END IF; + + SELECT COUNT(*) INTO v_count FROM stock_transfers + WHERE deleted_at IS NULL AND (from_warehouse_id = 10 OR to_warehouse_id = 10); + IF v_count <> 0 THEN RAISE EXCEPTION 'stock_transfers W10 residual %', v_count; END IF; +END $$; + +COMMIT;