freightdesk/supabase/migrations/006_payment_escrow.sql
FreightDesk 4923357e29
Some checks are pending
FreightDesk CI/CD / Lint & Test (push) Waiting to run
FreightDesk CI/CD / Build Docker Image (push) Blocked by required conditions
FreightDesk CI/CD / Deploy to Coolify (push) Blocked by required conditions
[OWL] Payment escrow system + marketplace payment integration
Database (migration 006):
- escrow_accounts: per-user balance tracking (available + held)
- escrow_transactions: all financial transactions (deposit/hold/release/refund/payout/fee)
- payout_requests: driver withdrawal requests with bank/UPI details
- platform_config: fee settings (5% commission, min deposit, hold period)
- disputes: payment dispute tracking
- Enhanced loads table: payment_status, escrow_amount, platform_fee, settled_at

Escrow Routes (/escrow):
- GET /escrow — payment dashboard with balance and transactions
- GET/POST /escrow/deposit — deposit funds (simulated, production: Razorpay)
- POST /escrow/hold — move funds to escrow for a specific load
- POST /escrow/release — release funds to driver after delivery
- GET/POST /escrow/payout — driver payout request (UPI or bank)
- POST /escrow/admin/payouts/:id/approve — admin approves payout
- POST /escrow/dispute — raise payment dispute

Views:
- Payment dashboard (balance, transactions, quick actions)
- Deposit page with quick amounts
- Payout request page with bank/UPI forms
- Payment status card on load detail (shipper view)
- Hold/Release/Dispute actions integrated into marketplace flow

Payment Flow:
1. Shipper deposits funds → balance
2. Shipper accepts bid → hold in escrow (driver freight + 5% fee)
3. Delivery confirmed → release to driver
4. Driver requests payout → admin approves → bank transfer
2026-06-08 01:50:02 +00:00

122 lines
5.3 KiB
SQL

-- ============================================================
-- FreightDesk — Migration 006: Payment Escrow System
-- Escrow payments, transactions, settlements, payouts
-- ============================================================
-- ============================================================
-- 1. ESCROW ACCOUNTS (one per user — shipper or driver)
-- ============================================================
CREATE TABLE IF NOT EXISTS escrow_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
role TEXT NOT NULL CHECK (role IN ('shipper', 'driver')),
balance INTEGER DEFAULT 0, -- available balance in paise
held_balance INTEGER DEFAULT 0, -- funds in escrow (in paise)
total_deposited INTEGER DEFAULT 0,
total_withdrawn INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, role)
);
CREATE INDEX IF NOT EXISTS idx_escrow_user ON escrow_accounts(user_id);
-- ============================================================
-- 2. ESCROW TRANSACTIONS
-- ============================================================
CREATE TABLE IF NOT EXISTS escrow_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
escrow_account_id UUID REFERENCES escrow_accounts(id),
load_id UUID REFERENCES loads(id),
bid_id UUID REFERENCES bids(id),
type TEXT NOT NULL CHECK (type IN (
'deposit', -- shipper deposits funds
'hold', -- funds moved to escrow hold
'release', -- funds released to driver
'refund', -- funds refunded to shipper
'payout', -- driver withdraws to bank
'platform_fee', -- FreightDesk commission
'adjustment' -- manual admin adjustment
)),
amount INTEGER NOT NULL, -- in paise
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'failed', 'reversed')),
reference_id TEXT, -- external payment reference
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_escrow_tx_account ON escrow_transactions(escrow_account_id);
CREATE INDEX IF NOT EXISTS idx_escrow_tx_load ON escrow_transactions(load_id);
CREATE INDEX IF NOT EXISTS idx_escrow_tx_type ON escrow_transactions(type);
CREATE INDEX IF NOT EXISTS idx_escrow_tx_status ON escrow_transactions(status);
-- ============================================================
-- 3. PAYOUT REQUESTS (driver withdrawal requests)
-- ============================================================
CREATE TABLE IF NOT EXISTS payout_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
driver_id UUID REFERENCES vehicles(id),
amount INTEGER NOT NULL,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'processed')),
bank_name TEXT,
account_number TEXT,
ifsc_code TEXT,
upi_id TEXT,
processed_by UUID,
processed_at TIMESTAMP WITH TIME ZONE,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_payout_driver ON payout_requests(driver_id);
CREATE INDEX IF NOT EXISTS idx_payout_status ON payout_requests(status);
-- ============================================================
-- 4. PLATFORM CONFIG (fee settings)
-- ============================================================
CREATE TABLE IF NOT EXISTS platform_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
description TEXT,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Default fee settings
INSERT INTO platform_config (key, value, description) VALUES
('escrow.platform_fee_percent', '5', 'Platform commission percentage'),
('escrow.min_deposit_amount', '10000', 'Minimum deposit amount in paise (₹100)'),
('escrow.hold_period_hours', '72', 'Hours to hold funds after delivery before auto-release'),
('escrow.payout_min_amount', '50000', 'Minimum payout request in paise (₹500)'),
('escrow.payout_fee', '0', 'Payout processing fee in paise')
ON CONFLICT (key) DO NOTHING;
-- ============================================================
-- 5. LOAD PAYMENT STATUS (tracks payment state per load)
-- ============================================================
ALTER TABLE loads ADD COLUMN IF NOT EXISTS payment_status TEXT DEFAULT 'none'
CHECK (payment_status IN ('none', 'deposited', 'in_escrow', 'released', 'refunded', 'disputed'));
ALTER TABLE loads ADD COLUMN IF NOT EXISTS escrow_amount INTEGER;
ALTER TABLE loads ADD COLUMN IF NOT EXISTS platform_fee INTEGER;
ALTER TABLE loads ADD COLUMN IF NOT EXISTS settled_at TIMESTAMP WITH TIME ZONE;
-- ============================================================
-- 6. DISPUTES TABLE
-- ============================================================
CREATE TABLE IF NOT EXISTS disputes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
load_id UUID NOT NULL REFERENCES loads(id),
raised_by UUID NOT NULL,
raised_against UUID NOT NULL,
reason TEXT NOT NULL,
status TEXT DEFAULT 'open' CHECK (status IN ('open', 'under_review', 'resolved', 'closed')),
resolution TEXT,
resolved_by UUID,
resolved_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_disputes_load ON disputes(load_id);
CREATE INDEX IF NOT EXISTS idx_disputes_status ON disputes(status);