Files
edr-platform/apps/edr-freight-api/py/prod-check-invoices-payments.sql

115 lines
6.2 KiB
SQL

-- ============================================================================
-- Production DB drift check + fix for the batch/window flow.
--
-- WHY: BookingBatchService.reserve() calls billing.syncPayableDueDate, which
-- queries freight.invoices.payments (a jsonb ledger added by migration
-- 1828000000000-ExtendInvoicesForPartialPayment). If that column is MISSING on
-- production (snapshot/restore drift — the migration can read as "applied" in
-- freight.migrations while the DDL never took effect), every reserve() throws
-- `column Invoice.payments does not exist`, the batch fill loop aborts mid-pass,
-- and you see exactly:
-- * only ONE booking gets a pay window (the loop dies after the first reserve
-- whose invoice sync throws), and
-- * reservations never expire cleanly (the settle path hits the same query).
--
-- Run STEP 1 first (read-only). If it shows the columns are MISSING, run STEP 2
-- (idempotent, additive — safe to run even if partially applied).
-- ============================================================================
-- ---------------------------------------------------------------------------
-- STEP 1 — CHECK (read-only). Expect all 6 rows present; if any are missing,
-- production has the drift and STEP 2 is required.
-- ---------------------------------------------------------------------------
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'freight'
AND table_name = 'invoices'
AND column_name IN (
'payments', 'subtotal_amount', 'tax_amount',
'paid_amount', 'balance_amount', 'paid_at'
)
ORDER BY column_name;
-- Also confirm the enum has the partial-payment statuses:
SELECT unnest(enum_range(NULL::freight.invoices_status_enum))::text AS status;
-- Expect ISSUED and PARTIALLY_PAID to be present.
-- ---------------------------------------------------------------------------
-- STEP 2 — FIX (idempotent). Only run if STEP 1 showed missing columns.
-- Mirrors migration 1828000000000 up(); all ADD COLUMN IF NOT EXISTS, so
-- re-running is safe. Wrapped so the enum additions (which cannot run inside a
-- transaction block with immediate use) are applied first, then the columns.
-- ---------------------------------------------------------------------------
-- Enum values (no-op if they already exist).
ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'ISSUED' BEFORE 'PENDING';
ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'PARTIALLY_PAID' BEFORE 'PAID';
-- Money-tracking + payments ledger columns.
ALTER TABLE freight.invoices
ADD COLUMN IF NOT EXISTS subtotal_amount numeric(14, 2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS tax_amount numeric(14, 2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS paid_amount numeric(14, 2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS balance_amount numeric(14, 2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS paid_at timestamptz,
ADD COLUMN IF NOT EXISTS payments jsonb NOT NULL DEFAULT '[]';
-- Backfill derived money fields for existing rows (only rows not already set).
UPDATE freight.invoices
SET subtotal_amount = total_amount,
balance_amount = total_amount
WHERE subtotal_amount = 0 AND balance_amount = 0;
UPDATE freight.invoices
SET paid_amount = total_amount,
balance_amount = 0,
paid_at = COALESCE(paid_at, updated_at)
WHERE status = 'PAID' AND paid_amount = 0;
-- ---------------------------------------------------------------------------
-- STEP 3 — RE-CHECK. Re-run STEP 1; all 6 columns + both enum values should
-- now be present. After this, deploy the freight_feature/usermanagement branch
-- and the batch will reserve ALL fitting bookings + expire non-payers + top up.
-- ---------------------------------------------------------------------------
-- ============================================================================
-- STEP 4 — BROADER DRIFT AUDIT (read-only). The same snapshot drift that hid
-- invoices.payments can hide OTHER columns the batch flow selects. reserve()
-- and settleReserved() load the FULL Booking entity, so ANY missing booking
-- column throws mid-loop (e.g. we already hit
-- `column Booking.consolidation_resume_status does not exist`). This lists every
-- booking column the entity expects that is MISSING from production — expect
-- ZERO rows. Any row = a drifted migration whose DDL must be re-applied.
-- ============================================================================
WITH expected(col) AS (
SELECT unnest(ARRAY[
'reference','customer_id','company_id','company_profile_id','is_government',
'government_institution','train_id','status','contract_id','contract_route_id',
'booking_type','contract_kind','created_by_role','created_by_user_id',
'scheduled_date','estimated_shipment_date','expires_at','total_amount',
'adjusted_total_amount','adjusted_by_staff_id','adjusted_at','adjustment_reason',
'contract_validity_days','contract_valid_from','contract_valid_until',
'payment_status','contract_type','service_type_id','customs_clearing_enabled',
'customs_clearing_agent','equipment_return','origin_yard_id','destination_yard_id',
'trade_direction','freight_type','cargo_type_id','cargo_free_text','shipping_line_id',
'cargo_total_weight_vgm','is_hazardous','is_reefer','bulk_hazardous_quantity',
'bulk_reefer_quantity','payment_currency','pnr_code','fully_executed_at',
'pricing_breakdown','locked_at','priority_score','consolidation_partner_id',
'consolidation_resume_status','wagons_required','scheduling_status',
'hold_started_at','hold_expires_at','scheduled_at','train_schedule_id',
'loaded_at','arrived_at','payment_deadline','selected_for_batch_at',
'gl_station_yard_id','clearance_current_phase','duty_required',
'vessel_departure_date','ro_amendment_requested_at','ro_hold_reason',
'pre_clearance_finalized_at','gl_assigned_staff_id','gl_assigned_at'
])
)
SELECT e.col AS missing_booking_column
FROM expected e
LEFT JOIN information_schema.columns c
ON c.table_schema = 'freight' AND c.table_name = 'bookings' AND c.column_name = e.col
WHERE c.column_name IS NULL
ORDER BY e.col;
-- If any rows come back, tell me which columns — I'll give you the exact
-- migration(s) to re-apply (each is ADD COLUMN IF NOT EXISTS, idempotent).