// ==============================|| Skill: Cash Exposure Agent ||============================== // // // Monitors the total live COD (Cash on Delivery) cash a single rider is carrying // across ALL their active orders. When total exposure exceeds the safe limit, // proposes a mandatory cash handoff stop at the nearest hub. // // This is distinct from HighValueCodSkill, which monitors per-order value. // This skill monitors per-rider TOTAL cash accumulation across their full run. const ACTIVE_COD_STATUSES = new Set(['accepted', 'picked', 'active', 'arrived']); const ref = (row) => row?.orderid || row?.bookingno || (row?.bookingid ? `#${row.bookingid}` : '—'); export const CashExposureSkill = { id: 'skill_cash_exposure', name: 'Cash Exposure Agent', description: 'Monitors total live COD cash per rider across all active orders. Flags when a rider carries unsafe cash levels and proposes hub handoff.', category: 'loss_prevention', icon: 'ShieldCheck', // OFF by default (2026-09-29): the rows this rule reads carry no COD amount per order: /admin/bookings does not preload bookingpayments. // Enabled, it would read undefined on every row and report an all-clear board — // the silent failure normalise.js warns about. Turn it on only once the data exists. dataGap: 'no COD amount per order: /admin/bookings does not preload bookingpayments', defaultEnabled: false, thresholds: { maxCashPerRider: { label: 'Max Safe Cash Per Rider', description: 'Maximum total COD cash (₹) a single rider should carry simultaneously across all active orders.', value: 10000, min: 2000, max: 50000, step: 1000, unit: '₹' }, warningCashPercent: { label: 'Warning Threshold (%)', description: 'Percentage of max cash limit at which to issue a warning (before it becomes critical).', value: 75, min: 50, max: 95, step: 5, unit: '%' } }, instructions: ` - A rider carrying excessive COD cash is a major financial risk if the phone dies, the rider is delayed, or there is a dispute. - Flag riders above the cash threshold for a mandatory hub cash handoff before continuing deliveries. - Critical: stop new COD orders being assigned to flagged riders until handoff is confirmed. `, tools: [ { name: 'enforce_cash_handoff', description: 'Proposes a mandatory cash handoff stop at the nearest hub for an overexposed rider.', parameters: { type: 'object', properties: { milerId: { type: 'number', description: 'Rider user ID carrying excessive cash' }, totalCash: { type: 'number', description: 'Total COD cash the rider currently holds (₹)' }, hubId: { type: 'string', description: 'Nearest hub for handoff' } }, required: ['milerId', 'totalCash'] }, handler: async (args) => ({ success: true, proposal: { action: 'cash_handoff', milerId: args.milerId, totalCash: args.totalCash, hubId: args.hubId || 'nearest', blastRadius: `Reroutes rider to nearest hub for cash handoff of ₹${args.totalCash?.toLocaleString('en-IN')}. Pauses new COD order assignments to this rider until confirmed.` } }) } ], evaluate: (rows = [], _now, currentThresholds = {}) => { const maxCash = currentThresholds.maxCashPerRider ?? 10000; const warningPct = (currentThresholds.warningCashPercent ?? 75) / 100; const warningLimit = maxCash * warningPct; const findings = []; // Group active COD orders by rider const byRider = {}; rows .filter((r) => ACTIVE_COD_STATUSES.has(String(r?.orderstatus || '').toLowerCase()) && r?.paymenttype && String(r.paymenttype).toLowerCase().includes('cod') && r?.userid ) .forEach((r) => { const uid = r.userid; if (!byRider[uid]) { byRider[uid] = { riderId: uid, riderName: r.ridername || r.miler_name || `Rider ${uid}`, orders: [] }; } byRider[uid].orders.push(r); }); const criticalRiders = []; const warningRiders = []; Object.values(byRider).forEach((riderData) => { const total = riderData.orders.reduce((sum, r) => { const amt = parseFloat(r.cod_amount || r.orderamount || r.totalamount || 0); return sum + (isNaN(amt) ? 0 : amt); }, 0); riderData.totalCash = total; if (total >= maxCash) { criticalRiders.push(riderData); } else if (total >= warningLimit) { warningRiders.push(riderData); } }); criticalRiders.sort((a, b) => b.totalCash - a.totalCash); warningRiders.sort((a, b) => b.totalCash - a.totalCash); if (criticalRiders.length) { findings.push({ id: 'cash_exposure_critical', skillId: 'skill_cash_exposure', severity: 'critical', title: `${criticalRiders.length} rider${criticalRiders.length === 1 ? '' : 's'} carrying ≥₹${maxCash.toLocaleString('en-IN')} in COD cash`, why: `Total COD cash per rider exceeds the safe limit of ₹${maxCash.toLocaleString('en-IN')}. Immediate hub handoff required.`, count: criticalRiders.length, rows: criticalRiders.flatMap((rd) => rd.orders), detail: criticalRiders.slice(0, 8).map( (rd) => `${rd.riderName} — ₹${rd.totalCash.toLocaleString('en-IN')} across ${rd.orders.length} active COD order${rd.orders.length === 1 ? '' : 's'}` ), proposal: { label: 'Enforce cash handoff at hub', tool: 'enforce_cash_handoff', scope: criticalRiders.flatMap((rd) => rd.orders), blastRadius: 'Reroutes affected riders to nearest hub for mandatory cash handoff. New COD assignments paused.' } }); } if (warningRiders.length) { findings.push({ id: 'cash_exposure_warning', skillId: 'skill_cash_exposure', severity: 'warning', title: `${warningRiders.length} rider${warningRiders.length === 1 ? '' : 's'} approaching cash limit (>${Math.round(warningPct * 100)}% of ₹${maxCash.toLocaleString('en-IN')})`, why: `Total COD cash is approaching the safe threshold. Plan a hub handoff stop for these riders.`, count: warningRiders.length, rows: warningRiders.flatMap((rd) => rd.orders), detail: warningRiders.slice(0, 8).map( (rd) => `${rd.riderName} — ₹${rd.totalCash.toLocaleString('en-IN')} across ${rd.orders.length} orders` ) }); } return findings; } };