162 lines
6.5 KiB
JavaScript
162 lines
6.5 KiB
JavaScript
// ==============================|| 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;
|
|
}
|
|
};
|