From 9b5e568e7224d85a02beadc1c40eaa78f2368a7e Mon Sep 17 00:00:00 2001 From: FreightDesk Date: Mon, 8 Jun 2026 02:16:02 +0000 Subject: [PATCH] [OWL] Bug fixes + seed data + bulk parser route Fixes: - Negotiate route: added auth check (only shipper or bidder can negotiate) - Negotiate route: added notification to other party - All payment views: removed /100 division (amounts stored in rupees, not paise) - Migration 006: updated platform_config seed values to rupees - Migration 007: added current_lat/current_lng columns to vehicles table - Added bulk-parser route to marketplace.js - Added Bulk WhatsApp Parser link to portal sidebar Seed Data: - scripts/seed-demo.js: 5 shippers, 5 drivers, 8 loads, sample bids - Idempotent: skips if data already exists --- supabase/migrations/006_payment_escrow.sql | 8 +- supabase/migrations/007_location_tracking.sql | 4 + webapp/scripts/seed-demo.js | 138 ++++++++++++++++++ webapp/src/routes/marketplace.js | 37 ++++- webapp/src/views/pages/admin/moderation.ejs | 2 +- webapp/src/views/pages/payments/deposit.ejs | 4 +- webapp/src/views/pages/payments/index.ejs | 10 +- webapp/src/views/pages/payments/payout.ejs | 4 +- webapp/src/views/partials/portal-header.ejs | 4 + 9 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 webapp/scripts/seed-demo.js diff --git a/supabase/migrations/006_payment_escrow.sql b/supabase/migrations/006_payment_escrow.sql index 81b6f37..6ec85d2 100644 --- a/supabase/migrations/006_payment_escrow.sql +++ b/supabase/migrations/006_payment_escrow.sql @@ -83,13 +83,13 @@ CREATE TABLE IF NOT EXISTS platform_config ( updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); --- Default fee settings +-- Default fee settings (amounts in rupees) 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.min_deposit_amount', '100', 'Minimum deposit in rupees'), ('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') + ('escrow.payout_min_amount', '500', 'Minimum payout request in rupees'), + ('escrow.payout_fee', '0', 'Payout processing fee in rupees') ON CONFLICT (key) DO NOTHING; -- ============================================================ diff --git a/supabase/migrations/007_location_tracking.sql b/supabase/migrations/007_location_tracking.sql index e3374b6..c04a8d0 100644 --- a/supabase/migrations/007_location_tracking.sql +++ b/supabase/migrations/007_location_tracking.sql @@ -18,6 +18,10 @@ CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle ON vehicle_locations(ve CREATE INDEX IF NOT EXISTS idx_vehicle_locations_time ON vehicle_locations(recorded_at); CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle_time ON vehicle_locations(vehicle_id, recorded_at DESC); +-- Add current location columns to vehicles +ALTER TABLE vehicles ADD COLUMN IF NOT EXISTS current_lat DECIMAL(10,8); +ALTER TABLE vehicles ADD COLUMN IF NOT EXISTS current_lng DECIMAL(11,8); + -- Enable PostGIS-like functionality with btree_gist for spatial queries -- (In production, use PostGIS extension) CREATE INDEX IF NOT EXISTS idx_vehicles_location ON vehicles(current_lat, current_lng) diff --git a/webapp/scripts/seed-demo.js b/webapp/scripts/seed-demo.js new file mode 100644 index 0000000..ec53a21 --- /dev/null +++ b/webapp/scripts/seed-demo.js @@ -0,0 +1,138 @@ +#!/usr/bin/env node +/** + * FreightDesk — Demo Seed Data Script + * Run: node scripts/seed-demo.js + * + * Creates demo shippers, drivers, loads, and bids for testing. + * Requires SUPABASE_URL and SUPABASE_SERVICE_KEY in environment. + */ + +const supabase = require('../src/services/supabase'); + +const DEMO_SHIPPERS = [ + { name: 'Kahn Transport', phone: '+919876543210', email: 'kahn@example.com', company_name: 'Kahn Transport Pvt Ltd', city: 'Kochi', state: 'Kerala', address: 'MG Road, Ernakulam', is_verified: true }, + { name: 'Agarwal Logistics', phone: '+918765432109', email: 'agarwal@example.com', company_name: 'Agarwal Trading Co', city: 'Bangalore', state: 'Karnataka', address: 'KR Market', is_verified: true }, + { name: 'Rajesh Freight', phone: '+917654321098', email: 'rajesh@example.com', city: 'Chennai', state: 'Tamil Nadu', address: 'T Nagar', is_verified: true }, + { name: 'Sharma Carriers', phone: '+916543210987', email: 'sharma@example.com', company_name: 'Sharma & Sons', city: 'Mumbai', state: 'Maharashtra', address: 'Andheri', is_verified: true }, + { name: 'VIP Logistics', phone: '+915432109876', email: 'vip@example.com', city: 'Hyderabad', state: 'Telangana', address: 'Banjara Hills', is_verified: false }, +]; + +const DEMO_DRIVERS = [ + { driver_name: 'Suresh Kumar', phone: '+919988776655', vehicle_number: 'KL 01 AB 1234', vehicle_type: '14ft', capacity_tons: 7, city: 'Kochi', state: 'Kerala', is_verified: true }, + { driver_name: 'Ramesh Singh', phone: '+918877665544', vehicle_number: 'KA 05 CD 5678', vehicle_type: '17ft', capacity_tons: 9, city: 'Bangalore', state: 'Karnataka', is_verified: true }, + { driver_name: 'Abdul Rahman', phone: '+917766554433', vehicle_number: 'TN 09 EF 9012', vehicle_type: '19ft', capacity_tons: 12, city: 'Chennai', state: 'Tamil Nadu', is_verified: true }, + { driver_name: 'Prakash Yadav', phone: '+916655443322', vehicle_number: 'MH 12 GH 3456', vehicle_type: '20ft', capacity_tons: 14, city: 'Mumbai', state: 'Maharashtra', is_verified: true }, + { driver_name: 'Venkat Rao', phone: '+915544332211', vehicle_number: 'TS 08 IJ 7890', vehicle_type: '17ft', capacity_tons: 9, city: 'Hyderabad', state: 'Telangana', is_verified: false }, +]; + +const DEMO_LOADS = [ + { from_city: 'Bangalore', to_city: 'Kochi', load_type: 'ftl', weight_kg: 8000, material_type: 'Electronics', budget_min: 25000, budget_max: 35000, pickup_date: '2026-02-15', delivery_date: '2026-02-17' }, + { from_city: 'Chennai', to_city: 'Mumbai', load_type: 'ftl', weight_kg: 12000, material_type: 'Machine Parts', budget_min: 45000, budget_max: 55000, pickup_date: '2026-02-16', delivery_date: '2026-02-19' }, + { from_city: 'Mumbai', to_city: 'Hyderabad', load_type: 'ftl', weight_kg: 10000, material_type: 'Chemicals', budget_min: 30000, budget_max: 40000, pickup_date: '2026-02-17', delivery_date: '2026-02-20' }, + { from_city: 'Hyderabad', to_city: 'Bangalore', load_type: 'ftl', weight_kg: 9000, material_type: 'Textiles', budget_min: 20000, budget_max: 28000, pickup_date: '2026-02-18', delivery_date: '2026-02-21' }, + { from_city: 'Kochi', to_city: 'Chennai', load_type: 'ftl', weight_kg: 7000, material_type: 'Spices', budget_min: 18000, budget_max: 25000, pickup_date: '2026-02-19', delivery_date: '2026-02-22' }, + { from_city: 'Bangalore', to_city: 'Mumbai', load_type: 'ptl', weight_kg: 3000, material_type: 'Auto Parts', budget_min: 12000, budget_max: 18000, pickup_date: '2026-02-20', delivery_date: '2026-02-23' }, + { from_city: 'Delhi', to_city: 'Bangalore', load_type: 'ftl', weight_kg: 15000, material_type: 'Furniture', budget_min: 55000, budget_max: 70000, pickup_date: '2026-02-21', delivery_date: '2026-02-25' }, + { from_city: 'Chennai', to_city: 'Kochi', load_type: 'ftl', weight_kg: 6000, material_type: 'Tea', budget_min: 15000, budget_max: 22000, pickup_date: '2026-02-22', delivery_date: '2026-02-24' }, +]; + +async function seed() { + console.log('🌱 Seeding FreightDesk demo data...\n'); + + // Check if already seeded + const { count: existingLoads } = await supabase.from('loads').select('*', { count: 'exact', head: true }); + if (existingLoads > 0) { + console.log(`āš ļø Found ${existingLoads} existing loads. Skipping seed. (Delete manually to re-seed)`); + process.exit(0); + } + + // Seed shippers + console.log('šŸ“¦ Creating shippers...'); + const { data: shippers, error: shipperError } = await supabase.from('shippers').insert(DEMO_SHIPPERS).select(); + if (shipperError) { console.error('Shipper error:', shipperError); process.exit(1); } + console.log(` āœ“ ${shippers.length} shippers created`); + + // Seed vehicles (drivers) + console.log('šŸš› Creating drivers/vehicles...'); + const driverRecords = DEMO_DRIVERS.map(d => ({ + number: d.vehicle_number, + vehicle_type: d.vehicle_type, + capacity_tons: d.capacity_tons, + city: d.city, + state: d.state, + is_verified: d.is_verified, + driver_name: d.driver_name, + phone: d.phone, + })); + const { data: vehicles, error: vehicleError } = await supabase.from('vehicles').insert(driverRecords).select(); + if (vehicleError) { console.error('Vehicle error:', vehicleError); process.exit(1); } + console.log(` āœ“ ${vehicles.length} drivers/vehicles created`); + + // Seed loads + console.log('šŸ“‹ Creating marketplace loads...'); + const loadRecords = DEMO_LOADS.map((l, i) => ({ + ...l, + shipper_id: shippers[i % shippers.length]?.id, + status: 'pending lead', + is_open: true, + expires_at: new Date(Date.now() + 7 * 86400000).toISOString(), + views: Math.floor(Math.random() * 50), + pickup_address: 'Pickup location TBD', + delivery_address: 'Delivery location TBD', + })); + const { data: loads, error: loadError } = await supabase.from('loads').insert(loadRecords).select(); + if (loadError) { console.error('Load error:', loadError); process.exit(1); } + console.log(` āœ“ ${loads.length} loads created`); + + // Seed some bids + console.log('šŸ’° Creating sample bids...'); + const bidRecords = []; + for (const load of loads.slice(0, 4)) { + const numBids = Math.floor(Math.random() * 3) + 1; + for (let i = 0; i < numBids; i++) { + const vehicle = vehicles[i % vehicles.length]; + const baseLoad = DEMO_LOADS[loads.indexOf(load)]; + const bidAmount = baseLoad.budget_min + Math.floor(Math.random() * (baseLoad.budget_max - baseLoad.budget_min) * 0.3); + bidRecords.push({ + load_id: load.id, + shipper_id: load.shipper_id, + driver_id: vehicle.id, + amount: bidAmount, + message: `Available for immediate pickup. ${vehicle.vehicle_type} truck. Contact: ${vehicle.phone}`, + status: 'pending', + }); + } + } + if (bidRecords.length > 0) { + const { error: bidError } = await supabase.from('bids').insert(bidRecords); + if (bidError) { console.error('Bid error:', bidError); process.exit(1); } + console.log(` āœ“ ${bidRecords.length} bids created`); + } + + // Ensure platform config + console.log('āš™ļø Setting platform config...'); + await supabase.from('platform_config').upsert([ + { key: 'escrow.platform_fee_percent', value: '5', description: 'Platform commission percentage' }, + { key: 'escrow.min_deposit_amount', value: '100', description: 'Minimum deposit in rupees' }, + { key: 'escrow.hold_period_hours', value: '72', description: 'Hours to hold funds after delivery' }, + { key: 'escrow.payout_min_amount', value: '500', description: 'Minimum payout in rupees' }, + ], { onConflict: 'key' }); + + console.log('\nāœ… Seed complete!'); + console.log('\nšŸ“Š Demo data:'); + console.log(` ${shippers.length} shippers (${shippers.filter(s => s.is_verified).length} verified)`); + console.log(` ${vehicles.length} drivers (${vehicles.filter(v => v.is_verified).length} verified)`); + console.log(` ${loads.length} marketplace loads`); + console.log(` ${bidRecords.length} bids`); + console.log('\n🌐 Access the app:'); + console.log(' Landing: http://localhost:3000/'); + console.log(' Admin: http://localhost:3000/login'); + console.log(' Marketplace: http://localhost:3000/marketplace'); + console.log(' Portal: http://localhost:3000/portal'); + process.exit(0); +} + +seed().catch(err => { + console.error('āŒ Seed failed:', err); + process.exit(1); +}); diff --git a/webapp/src/routes/marketplace.js b/webapp/src/routes/marketplace.js index f054c8c..b76a3ed 100644 --- a/webapp/src/routes/marketplace.js +++ b/webapp/src/routes/marketplace.js @@ -123,6 +123,11 @@ router.get('/post', requirePortalAuth, requireRole('shipper'), (req, res) => { res.render('pages/marketplace/post', { error: null, formData: {} }); }); +// Bulk WhatsApp parser page +router.get('/bulk-parser', requirePortalAuth, requireRole('shipper'), (req, res) => { + res.render('pages/marketplace/bulk-parser'); +}); + router.post('/post', requirePortalAuth, requireRole('shipper'), asyncHandler(async (req, res) => { const { from_city, to_city, via, load_type, weight_kg, material_type, @@ -247,13 +252,43 @@ router.post('/bid/:bidId/negotiate', requirePortalAuth, asyncHandler(async (req, const { proposed_amount, message } = req.body; if (!proposed_amount || parseInt(proposed_amount) <= 0) return res.status(400).json({ error: 'Valid amount required' }); + // Verify user is party to this bid + const { data: bid } = await supabase + .from('bids') + .select('*, loads(shipper_id)') + .eq('id', req.params.bidId) + .single(); + + if (!bid) return res.status(404).json({ error: 'Bid not found' }); + + const userId = req.session.portalUser.id; + const isShipper = req.session.portalUser.role === 'shipper'; + const isDriver = req.session.portalUser.role === 'driver' && req.session.portalUser.driver_id === bid.driver_id; + + if (!isShipper && !isDriver) { + return res.status(403).json({ error: 'Only the shipper or bidder can negotiate' }); + } + const { error } = await supabase.from('negotiations').insert({ - bid_id: req.params.bidId, proposed_by: req.session.portalUser.id, + bid_id: req.params.bidId, proposed_by: userId, proposed_amount: parseInt(proposed_amount), message: message || null, }); if (error) return res.status(400).json({ error: error.message }); await supabase.from('bids').update({ status: 'negotiating' }).eq('id', req.params.bidId); + + // Notify the other party + const notifyUserId = isShipper ? bid.driver_id : bid.loads?.shipper_id; + if (notifyUserId) { + await supabase.from('notifications').insert({ + user_id: notifyUserId, + type: 'negotiation', + title: 'Counter Offer', + message: `₹${parseInt(proposed_amount).toLocaleString('en-IN')} counter offer on your bid`, + data: { bid_id: bid.id, load_id: bid.load_id }, + }); + } + res.json({ success: true }); })); diff --git a/webapp/src/views/pages/admin/moderation.ejs b/webapp/src/views/pages/admin/moderation.ejs index 00dc06a..537853b 100644 --- a/webapp/src/views/pages/admin/moderation.ejs +++ b/webapp/src/views/pages/admin/moderation.ejs @@ -114,7 +114,7 @@ <%= p.vehicles?.driver_name || 'N/A' %>
<%= p.vehicles?.number || '' %> - ₹ <%= (p.amount / 100).toLocaleString('en-IN') %> + ₹ <%= (p.amount).toLocaleString('en-IN') %> <%= p.upi_id ? 'UPI' : 'Bank' %> diff --git a/webapp/src/views/pages/payments/deposit.ejs b/webapp/src/views/pages/payments/deposit.ejs index 4389878..67ac005 100644 --- a/webapp/src/views/pages/payments/deposit.ejs +++ b/webapp/src/views/pages/payments/deposit.ejs @@ -57,12 +57,12 @@

Current Balance

- ₹ <%= ((account?.balance || 0) / 100).toLocaleString('en-IN') %> + ₹ <%= (account?.balance || 0).toLocaleString('en-IN') %>
Available
<% if (account?.held_balance > 0) { %>
- ₹ <%= (account.held_balance / 100).toLocaleString('en-IN') %> in escrow + ₹ <%= (account.held_balance).toLocaleString('en-IN') %> in escrow
<% } %>
diff --git a/webapp/src/views/pages/payments/index.ejs b/webapp/src/views/pages/payments/index.ejs index e7297c5..f77d877 100644 --- a/webapp/src/views/pages/payments/index.ejs +++ b/webapp/src/views/pages/payments/index.ejs @@ -13,12 +13,12 @@

Account Balance

- ₹ <%= ((account?.balance || 0) / 100).toLocaleString('en-IN') %> + ₹ <%= (account?.balance || 0).toLocaleString('en-IN') %>
Available Balance
<% if (account?.held_balance > 0) { %>
- ₹ <%= (account.held_balance / 100).toLocaleString('en-IN') %> in escrow + ₹ <%= (account.held_balance).toLocaleString('en-IN') %> in escrow
<% } %>
@@ -37,13 +37,13 @@
- ₹ <%= ((account?.total_deposited || 0) / 100).toLocaleString('en-IN') %> + ₹ <%= ((account?.total_deposited || 0)).toLocaleString('en-IN') %>
Total Deposited
- ₹ <%= ((account?.total_withdrawn || 0) / 100).toLocaleString('en-IN') %> + ₹ <%= ((account?.total_withdrawn || 0)).toLocaleString('en-IN') %>
Total Withdrawn
@@ -81,7 +81,7 @@ <%= tx.type === 'deposit' || tx.type === 'release' ? '+' : '-' %> - ₹ <%= (tx.amount / 100).toLocaleString('en-IN') %> + ₹ <%= (tx.amount).toLocaleString('en-IN') %> <% if (tx.loads) { %> diff --git a/webapp/src/views/pages/payments/payout.ejs b/webapp/src/views/pages/payments/payout.ejs index 1172d20..a6dac4d 100644 --- a/webapp/src/views/pages/payments/payout.ejs +++ b/webapp/src/views/pages/payments/payout.ejs @@ -18,7 +18,7 @@
- ₹ <%= ((account?.balance || 0) / 100).toLocaleString('en-IN') %> + ₹ <%= (account?.balance || 0).toLocaleString('en-IN') %>
Available for withdrawal
@@ -86,7 +86,7 @@ <% for (const p of payouts) { %> - ₹ <%= (p.amount / 100).toLocaleString('en-IN') %> + ₹ <%= (p.amount).toLocaleString('en-IN') %> <% if (p.upi_id) { %> UPI: <%= p.upi_id %> diff --git a/webapp/src/views/partials/portal-header.ejs b/webapp/src/views/partials/portal-header.ejs index 9e08c1b..5c39dab 100644 --- a/webapp/src/views/partials/portal-header.ejs +++ b/webapp/src/views/partials/portal-header.ejs @@ -49,6 +49,10 @@ <% } %> 🔔 Notifications
+