freightdesk/webapp/src/services/parser.js
FreightDesk 1a4eaaa040 Initial commit: FreightDesk v1.0
- Express + EJS server-rendered app
- Supabase PostgreSQL database
- Auth: username/password with bcrypt
- Dashboard with business stats
- Load CRUD with filters
- WhatsApp message parser
- Payment tracking
- Shipper & vehicle management
- Reports (monthly, top shippers, routes)
- Government-app aesthetic (tricolor theme)
- Dark mode support
- Docker + Coolify deployment ready
- Seed data from existing business ledger (88 loads, 41 shippers, 70 vehicles)
2026-06-07 18:57:24 +00:00

190 lines
7 KiB
JavaScript

// WhatsApp message parser for FreightDesk
// Parses natural language freight messages into structured data
const { CITIES } = require('../config/constants');
// Known shipper names (from existing data)
const KNOWN_SHIPPERS = [
'Kahn Transport', 'Agarwal Packers and Movers', 'Agarwal', 'Sahara Packers',
'Ambika Packers', 'Century Polymers', 'DRS', 'Superstar', 'Superstar Packers',
'Chips', 'Chipps', 'Indian CBE', 'Indian CBE Shipper', 'KTC', 'ATC', 'TCI',
'Filatex', 'Dryfish John', 'Sun Packers', 'Thangavel', 'Nafees Alappuzha',
'Shivaprasad', 'Jinu Coin', 'Balmer Thuni', 'Pasupathy', 'Sulphi Baddest',
'KRS', 'Hirosh', 'Hirosh Roadways', 'Aero Rubber', 'Silverstar',
'DRS Agarwal', 'Gem', 'E20 Packers and Movers', 'CRT Transport',
'Mohamed Anas', 'Nair', 'Badadosth',
];
// Status keywords mapping
const STATUS_KEYWORDS = {
'pending lead': ['pending lead', 'lead', 'enquiry', 'enquiry'],
'assigned vehicle': ['assigned vehicle', 'vehicle assigned'],
'assigned': ['assigned', 'allotted'],
'loaded / in transit': ['loaded', 'in transit', 'on the way', 'dispatched', 'started'],
'delivered / pending collection': ['delivered', 'delivery done'],
'pending collection': ['pending collection', 'collection pending', 'to collect'],
'partially pending': ['partially pending', 'partial pending'],
'fully pending from shipper': ['fully pending', 'no payment'],
'settled': ['settled', 'complete', 'completed', 'closed'],
'commission received': ['commission received', 'comm received'],
'commission adjusted': ['commission adjusted', 'comm adjusted'],
'commission due': ['commission due', 'comm due'],
'reconciled': ['reconciled'],
'completed': ['completed', 'done'],
'handled directly by shipper': ['directly by shipper', 'handled directly'],
'available vehicle': ['available', 'vehicle available'],
'partial': ['partial'],
};
function parseWhatsAppMessage(text) {
const result = {
shipper: null,
vehicle: null,
from_city: null,
to_city: null,
via: null,
status: null,
freight_charged: null,
advance_received: null,
paid_to_driver: null,
commission: null,
driver_freight: null,
pending_from_shipper: null,
pending_to_driver: null,
notes: text,
confidence: 'low',
parsed_fields: [],
};
const lower = text.toLowerCase();
// 1. Parse shipper
for (const shipper of KNOWN_SHIPPERS) {
if (lower.includes(shipper.toLowerCase())) {
result.shipper = shipper;
result.parsed_fields.push('shipper');
break;
}
}
// 2. Parse vehicle number (Indian format: XX00XX0000)
const vehicleMatch = text.match(/\b([A-Z]{2}\s*\d{1,2}\s*[A-Z]{1,3}\s*\d{4})\b/i);
if (vehicleMatch) {
result.vehicle = vehicleMatch[1].replace(/\s/g, '').toUpperCase();
result.parsed_fields.push('vehicle');
}
// 3. Parse cities (from → to pattern)
const cityPattern = CITIES.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const routeMatch = text.match(new RegExp(`(${cityPattern})\\s*(?:to|→|-|via)\\s*(${cityPattern})`, 'i'));
if (routeMatch) {
result.from_city = routeMatch[1];
result.to_city = routeMatch[2];
result.parsed_fields.push('from_city', 'to_city');
} else {
// Try to find any known city
for (const city of CITIES) {
if (lower.includes(city.toLowerCase())) {
if (!result.to_city) {
result.to_city = city;
result.parsed_fields.push('to_city');
} else if (!result.from_city) {
result.from_city = city;
result.parsed_fields.push('from_city');
}
}
}
}
// 4. Parse via
const viaMatch = text.match(/via\s+([A-Za-z\s,]+?)(?:\s*(?:to|→|-|loaded|freight|₹|\d{4,}))/i);
if (viaMatch) {
result.via = viaMatch[1].trim();
result.parsed_fields.push('via');
}
// 5. Parse status
for (const [status, keywords] of Object.entries(STATUS_KEYWORDS)) {
for (const kw of keywords) {
if (lower.includes(kw)) {
result.status = status;
result.parsed_fields.push('status');
break;
}
}
if (result.status) break;
}
// 6. Parse amounts
// Freight: look for "freight", "charged", "total" followed by number
const freightMatch = text.match(/(?:freight|charged|total|amount|bill)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i);
if (freightMatch) {
result.freight_charged = parseInt(freightMatch[1].replace(/,/g, ''));
result.parsed_fields.push('freight_charged');
} else {
// Try standalone large numbers (4-6 digits) that could be freight
const amountMatches = text.match(/₹?\s*(\d{4,6})\b/g);
if (amountMatches) {
const amounts = amountMatches.map(m => parseInt(m.replace(/[₹,\s]/g, '')));
if (amounts.length > 0) {
result.freight_charged = Math.max(...amounts);
result.parsed_fields.push('freight_charged');
}
}
}
// Advance received
const advanceMatch = text.match(/(?:advance|received|paid by shipper)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i);
if (advanceMatch) {
result.advance_received = parseInt(advanceMatch[1].replace(/,/g, ''));
result.parsed_fields.push('advance_received');
}
// Paid to driver
const driverPaidMatch = text.match(/(?:paid to driver|driver advance|driver paid|to driver)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i);
if (driverPaidMatch) {
result.paid_to_driver = parseInt(driverPaidMatch[1].replace(/,/g, ''));
result.parsed_fields.push('paid_to_driver');
}
// Commission
const commissionMatch = text.match(/(?:commission|comm)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i);
if (commissionMatch) {
result.commission = parseInt(commissionMatch[1].replace(/,/g, ''));
result.parsed_fields.push('commission');
}
// Driver freight
const driverFreightMatch = text.match(/(?:driver freight|driver rate|driver amount)\s*[:\-]?\s*₹?\s*(\d[\d,]*)/i);
if (driverFreightMatch) {
result.driver_freight = parseInt(driverFreightMatch[1].replace(/,/g, ''));
result.parsed_fields.push('driver_freight');
}
// Auto-calculate commission if not parsed
if (!result.commission && result.freight_charged && result.paid_to_driver) {
result.commission = result.freight_charged - result.paid_to_driver;
result.parsed_fields.push('commission (auto)');
}
// Auto-calculate pending from shipper
if (!result.pending_from_shipper && result.freight_charged) {
result.pending_from_shipper = result.freight_charged - (result.advance_received || 0);
if (result.pending_from_shipper > 0) result.parsed_fields.push('pending_from_shipper (auto)');
}
// Auto-calculate pending to driver
if (!result.pending_to_driver && result.driver_freight) {
result.pending_to_driver = result.driver_freight - (result.paid_to_driver || 0);
if (result.pending_to_driver > 0) result.parsed_fields.push('pending_to_driver (auto)');
}
// Confidence based on how many fields were parsed
const fieldCount = result.parsed_fields.length;
if (fieldCount >= 6) result.confidence = 'high';
else if (fieldCount >= 3) result.confidence = 'medium';
return result;
}
module.exports = { parseWhatsAppMessage, KNOWN_SHIPPERS };