mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
add production DB drift check and fix for invoice payments
This commit is contained in:
114
apps/edr-freight-api/py/prod-check-invoices-payments.sql
Normal file
114
apps/edr-freight-api/py/prod-check-invoices-payments.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
-- ============================================================================
|
||||
-- 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).
|
||||
@@ -455,8 +455,6 @@ function PriceConfirmModal({
|
||||
const hasPairingBlock = pairingErrors.length > 0;
|
||||
const capacityErrors = validation?.capacityErrors ?? [];
|
||||
const hasCapacityBlock = capacityErrors.length > 0;
|
||||
const confirmDisabled =
|
||||
loading || validationLoading || hasPairingBlock || hasCapacityBlock;
|
||||
|
||||
// Authoritative server breakdown — the SAME BookingPricingService pass that
|
||||
// prices the booking on create, so it carries every line the booking will be
|
||||
@@ -479,6 +477,17 @@ function PriceConfirmModal({
|
||||
};
|
||||
}, [validation, baseTotal]);
|
||||
|
||||
// Block confirm until the authoritative server price is in hand. The client
|
||||
// baseTotal fallback is display-only; booking on it (e.g. after a validation
|
||||
// error clears validationLoading with no data) would let the customer confirm
|
||||
// an un-validated, possibly wrong price.
|
||||
const confirmDisabled =
|
||||
loading ||
|
||||
validationLoading ||
|
||||
hasPairingBlock ||
|
||||
hasCapacityBlock ||
|
||||
!serverTotal;
|
||||
|
||||
// Fallback while the server preview loads: the contract's frozen unit rates
|
||||
// (container/bulk + hazard/reefer only) with the overweight surcharge folded
|
||||
// in. Replaced by the full server breakdown the moment it arrives.
|
||||
|
||||
Reference in New Issue
Block a user