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
122 lines
5.3 KiB
SQL
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);
|