jtlevine's picture
Phase 1.5: frontend region localization (tour, pages, currency)
d1c696f
// ────────────────────────────────────────────────────────────────────────────
// mockData.ts
// In-file fixtures for the Market Intelligence (Crop Pricing Agent) dashboard.
// Populates every React Query hook in ./api.ts so the UI renders without a
// live backend. Context: Tamil Nadu, India, mid-April 2026.
// ────────────────────────────────────────────────────────────────────────────
import type {
MandisResponse,
MarketPricesResponse,
PriceForecastsResponse,
SellRecommendationsResponse,
PriceConflictsResponse,
RawInputsResponse,
ExtractedDataResponse,
ReconciledDataResponse,
ModelInfoResponse,
DeliveryLogsResponse,
PipelineRunsResponse,
PipelineStats,
Mandi,
MarketPrice,
PriceForecast,
SellRecommendation,
SellOption,
PriceConflict,
DeliveryLog,
PipelineRun,
PipelineStep,
} from './api'
// ── Time helpers ─────────────────────────────────────────────────────────────
const NOW = new Date('2026-04-13T11:42:00+05:30')
function isoMinutesAgo(mins: number): string {
return new Date(NOW.getTime() - mins * 60_000).toISOString()
}
function isoHoursAgo(hours: number): string {
return new Date(NOW.getTime() - hours * 3_600_000).toISOString()
}
function dateDaysAgo(days: number): string {
return new Date(NOW.getTime() - days * 86_400_000).toISOString().slice(0, 10)
}
// ── Mandis (15 Tamil Nadu) ──────────────────────────────────────────────────
interface MandiSeed {
id: string
name: string
district: string
lat: number
lon: number
enam: boolean
quality: 'good' | 'fair' | 'poor'
commodities: string[]
market_type: string
}
const MANDI_SEEDS: MandiSeed[] = [
{ id: 'MND-CBE', name: 'Coimbatore', district: 'Coimbatore', lat: 11.0168, lon: 76.9558, enam: true, quality: 'good', market_type: 'regulated', commodities: ['PDY-PONNI', 'GNUT-BOLD', 'MAIZE-Y', 'COCO-COPRA', 'ONION-RED'] },
{ id: 'MND-ERD', name: 'Erode', district: 'Erode', lat: 11.3410, lon: 77.7172, enam: true, quality: 'good', market_type: 'regulated', commodities: ['TURM-FIN', 'PDY-PONNI', 'COTN-MCU', 'MAIZE-Y', 'ONION-RED'] },
{ id: 'MND-SLM', name: 'Salem', district: 'Salem', lat: 11.6643, lon: 78.1460, enam: true, quality: 'good', market_type: 'regulated', commodities: ['TURM-FIN', 'PDY-PONNI', 'GNUT-BOLD', 'MAIZE-Y', 'BANANA-ROB'] },
{ id: 'MND-MDU', name: 'Madurai', district: 'Madurai', lat: 9.9252, lon: 78.1198, enam: true, quality: 'good', market_type: 'regulated', commodities: ['PDY-PONNI', 'COTN-MCU', 'BANANA-ROB', 'ONION-RED', 'GNUT-BOLD'] },
{ id: 'MND-TRY', name: 'Tiruchirappalli', district: 'Tiruchirappalli', lat: 10.7905, lon: 78.7047, enam: true, quality: 'good', market_type: 'regulated', commodities: ['PDY-PONNI', 'BANANA-ROB', 'URAD-BLK', 'MOONG-GRN', 'GNUT-BOLD'] },
{ id: 'MND-TNJ', name: 'Thanjavur', district: 'Thanjavur', lat: 10.7870, lon: 79.1378, enam: true, quality: 'good', market_type: 'regulated', commodities: ['PDY-PONNI', 'URAD-BLK', 'MOONG-GRN', 'COCO-COPRA', 'BANANA-ROB'] },
{ id: 'MND-TIR', name: 'Tirunelveli', district: 'Tirunelveli', lat: 8.7139, lon: 77.7567, enam: false, quality: 'fair', market_type: 'regulated', commodities: ['PDY-PONNI', 'COTN-MCU', 'BANANA-ROB', 'COCO-COPRA', 'ONION-RED'] },
{ id: 'MND-VLR', name: 'Vellore', district: 'Vellore', lat: 12.9165, lon: 79.1325, enam: true, quality: 'fair', market_type: 'regulated', commodities: ['PDY-PONNI', 'GNUT-BOLD', 'MAIZE-Y', 'URAD-BLK', 'ONION-RED'] },
{ id: 'MND-CHN', name: 'Chennai Koyambedu', district: 'Chennai', lat: 13.0707, lon: 80.1943, enam: true, quality: 'good', market_type: 'terminal', commodities: ['ONION-RED', 'BANANA-ROB', 'COCO-COPRA', 'PDY-PONNI', 'TURM-FIN'] },
{ id: 'MND-DGL', name: 'Dindigul', district: 'Dindigul', lat: 10.3673, lon: 77.9803, enam: true, quality: 'good', market_type: 'regulated', commodities: ['BANANA-ROB', 'ONION-RED', 'TURM-FIN', 'GNUT-BOLD', 'PDY-PONNI'] },
{ id: 'MND-THN', name: 'Theni', district: 'Theni', lat: 10.0104, lon: 77.4768, enam: false, quality: 'fair', market_type: 'regulated', commodities: ['ONION-RED', 'BANANA-ROB', 'COTN-MCU', 'PDY-PONNI', 'COCO-COPRA'] },
{ id: 'MND-VPM', name: 'Villupuram', district: 'Villupuram', lat: 11.9401, lon: 79.4861, enam: true, quality: 'fair', market_type: 'regulated', commodities: ['PDY-PONNI', 'GNUT-BOLD', 'URAD-BLK', 'MOONG-GRN', 'MAIZE-Y'] },
{ id: 'MND-NKL', name: 'Namakkal', district: 'Namakkal', lat: 11.2189, lon: 78.1677, enam: true, quality: 'good', market_type: 'regulated', commodities: ['MAIZE-Y', 'PDY-PONNI', 'TURM-FIN', 'GNUT-BOLD', 'COTN-MCU'] },
{ id: 'MND-KRR', name: 'Karur', district: 'Karur', lat: 10.9601, lon: 78.0766, enam: true, quality: 'fair', market_type: 'regulated', commodities: ['COTN-MCU', 'PDY-PONNI', 'TURM-FIN', 'GNUT-BOLD', 'MAIZE-Y'] },
{ id: 'MND-KGI', name: 'Krishnagiri', district: 'Krishnagiri', lat: 12.5186, lon: 78.2137, enam: true, quality: 'good', market_type: 'regulated', commodities: ['MAIZE-Y', 'GNUT-BOLD', 'PDY-PONNI', 'ONION-RED', 'BANANA-ROB'] },
]
export const MANDIS: Mandi[] = MANDI_SEEDS.map((m, idx) => ({
mandi_id: m.id,
name: m.name,
district: m.district,
latitude: m.lat,
longitude: m.lon,
market_type: m.market_type,
enam_integrated: m.enam,
reporting_quality: m.quality,
commodities_traded: m.commodities,
last_updated: isoMinutesAgo(45 + idx * 7),
}))
// ── Commodities ─────────────────────────────────────────────────────────────
interface CommoditySeed {
id: string
name: string
name_ta: string
base_price: number // ₹/quintal
seasonal_index: number
category: string
}
const COMMODITIES: CommoditySeed[] = [
{ id: 'PDY-PONNI', name: 'Paddy (Ponni)', name_ta: 'பொன்னி நெல்', base_price: 2220, seasonal_index: 1.02, category: 'cereal' },
{ id: 'GNUT-BOLD', name: 'Groundnut (Bold)', name_ta: 'நிலக்கடலை', base_price: 6500, seasonal_index: 1.05, category: 'oilseed' },
{ id: 'TURM-FIN', name: 'Turmeric (Finger)', name_ta: 'மஞ்சள்', base_price: 12100, seasonal_index: 0.98, category: 'spice' },
{ id: 'COTN-MCU', name: 'Cotton (MCU-5)', name_ta: 'பருத்தி', base_price: 7350, seasonal_index: 0.96, category: 'fibre' },
{ id: 'COCO-COPRA', name: 'Coconut (Copra)', name_ta: 'தேங்காய் கொப்பரை', base_price: 10200, seasonal_index: 1.04, category: 'oilseed' },
{ id: 'MAIZE-Y', name: 'Maize (Yellow)', name_ta: 'மக்காச்சோளம்', base_price: 2080, seasonal_index: 1.01, category: 'cereal' },
{ id: 'URAD-BLK', name: 'Black Gram (Urad)', name_ta: 'உளுந்து', base_price: 8450, seasonal_index: 1.03, category: 'pulse' },
{ id: 'MOONG-GRN', name: 'Green Gram (Moong)', name_ta: 'பாசிப்பயறு', base_price: 8120, seasonal_index: 1.00, category: 'pulse' },
{ id: 'ONION-RED', name: 'Onion (Red)', name_ta: 'சிவப்பு வெங்காயம்', base_price: 2650, seasonal_index: 0.94, category: 'vegetable' },
{ id: 'BANANA-ROB', name: 'Banana (Robusta)', name_ta: 'வாழைப்பழம்', base_price: 1850, seasonal_index: 1.06, category: 'fruit' },
]
const COMMODITY_BY_ID = new Map(COMMODITIES.map((c) => [c.id, c]))
// ── Market Prices ───────────────────────────────────────────────────────────
// Seeded pseudo-random to keep numbers deterministic across reloads.
function seededRand(seed: number): () => number {
let s = seed >>> 0
return () => {
s = (s * 1664525 + 1013904223) >>> 0
return s / 4294967296
}
}
const rand = seededRand(20260413)
// Mandi regional bias: coastal vs hill vs terminal markets
const MANDI_PRICE_BIAS: Record<string, number> = {
'MND-CBE': 1.01, 'MND-ERD': 1.00, 'MND-SLM': 0.99, 'MND-MDU': 0.98,
'MND-TRY': 0.97, 'MND-TNJ': 0.96, 'MND-TIR': 0.94, 'MND-VLR': 1.00,
'MND-CHN': 1.08, 'MND-DGL': 1.02, 'MND-THN': 0.97, 'MND-VPM': 0.96,
'MND-NKL': 1.00, 'MND-KRR': 0.99, 'MND-KGI': 1.01,
}
function trendFor(): 'up' | 'down' | 'flat' {
const r = rand()
if (r < 0.45) return 'up'
if (r < 0.80) return 'down'
return 'flat'
}
const MARKET_PRICES: MarketPrice[] = (() => {
const out: MarketPrice[] = []
for (const mandi of MANDI_SEEDS) {
for (const cid of mandi.commodities) {
const c = COMMODITY_BY_ID.get(cid)
if (!c) continue
const bias = MANDI_PRICE_BIAS[mandi.id] ?? 1.0
const jitter = 0.94 + rand() * 0.12 // ±6%
const base = Math.round(c.base_price * bias * jitter)
// Agmarknet / eNAM have small natural disagreement
const agDelta = (rand() - 0.5) * 0.04
const enamDelta = (rand() - 0.5) * 0.05
const agmarknet = Math.round(base * (1 + agDelta))
const enam = mandi.enam ? Math.round(base * (1 + enamDelta)) : null
// Reconciled price sits between (or equals Agmarknet if eNAM missing)
const reconciled = enam !== null ? Math.round((agmarknet + enam) / 2) : agmarknet
out.push({
mandi_id: mandi.id,
mandi_name: mandi.name,
commodity_id: cid,
commodity_name: c.name,
category: c.category,
price_rs: reconciled,
agmarknet_price_rs: agmarknet,
enam_price_rs: enam,
reconciled_price_rs: reconciled,
confidence: 0.72 + rand() * 0.23,
price_trend: trendFor(),
date: dateDaysAgo(0),
})
}
}
return out
})()
// ── Price Conflicts (12 entries) ────────────────────────────────────────────
interface ConflictSeed {
mandi_id: string
commodity_id: string
agmarknet: number
enam: number
resolved: number
resolution: string
reasoning: string
steps: { tool: string; finding: string }[]
}
const CONFLICT_SEEDS: ConflictSeed[] = [
{
mandi_id: 'MND-ERD', commodity_id: 'TURM-FIN',
agmarknet: 11850, enam: 12780, resolved: 12410,
resolution: 'weighted_blend',
reasoning: 'eNAM quote reflects morning auction close; Agmarknet sample understates because it missed two large lots. Weighted toward eNAM.',
steps: [
{ tool: 'check_arrival_volume', finding: 'eNAM logged 1,840 qtl across 14 lots; Agmarknet only captured 4 lots (620 qtl).' },
{ tool: 'fetch_historical_spread', finding: 'Over last 30 days, eNAM has run 2.1% above Agmarknet on Erode turmeric, not 7.8%.' },
{ tool: 'cross_check_neighbour', finding: 'Salem (neighbouring market) reconciled at ₹12,380 — tightly clustered with eNAM.' },
],
},
{
mandi_id: 'MND-CBE', commodity_id: 'GNUT-BOLD',
agmarknet: 6420, enam: 6180, resolved: 6310,
resolution: 'midpoint',
reasoning: 'Spread within historical noise band. No quality or volume signal favouring either source — took midpoint.',
steps: [
{ tool: 'check_arrival_volume', finding: 'Both sources reported comparable arrivals (≈2,100 qtl).' },
{ tool: 'fetch_historical_spread', finding: 'Mean 30-day delta is 0.6%; today 3.8% sits inside 2σ band.' },
],
},
{
mandi_id: 'MND-CHN', commodity_id: 'ONION-RED',
agmarknet: 2890, enam: 2420, resolved: 2490,
resolution: 'prefer_enam',
reasoning: 'Chennai Koyambedu is a terminal market; eNAM captures wholesale auction floor. Agmarknet reporter appears to have logged retail-grade stock.',
steps: [
{ tool: 'check_arrival_volume', finding: 'eNAM logged 6,800 qtl of grade-B onion; Agmarknet only tagged 420 qtl with no grade label.' },
{ tool: 'inspect_grade', finding: 'Agmarknet row reads "onion" without variety tag — likely mixed grade, not comparable.' },
{ tool: 'cross_check_neighbour', finding: 'Vellore onion reconciled at ₹2,520; Villupuram at ₹2,470. Cluster supports eNAM.' },
],
},
{
mandi_id: 'MND-TNJ', commodity_id: 'PDY-PONNI',
agmarknet: 2180, enam: 2310, resolved: 2260,
resolution: 'weighted_blend',
reasoning: 'eNAM reflects direct procurement auction; Agmarknet sample biased low by a single late-evening lot. Weighted 70/30 toward eNAM.',
steps: [
{ tool: 'check_arrival_volume', finding: 'eNAM: 3,450 qtl across 22 lots. Agmarknet: 810 qtl, only 5 lots and one priced below MSP.' },
{ tool: 'check_msp_floor', finding: 'MSP for common paddy FY26 is ₹2,300; eNAM sits at MSP, Agmarknet row is below — likely reporting error.' },
],
},
{
mandi_id: 'MND-MDU', commodity_id: 'COTN-MCU',
agmarknet: 7820, enam: 7090, resolved: 7580,
resolution: 'prefer_agmarknet',
reasoning: 'eNAM session closed early due to weighbridge outage; thin liquidity pulled last-print down. Agmarknet full-day average more representative.',
steps: [
{ tool: 'check_session_status', finding: 'eNAM Madurai session flagged "partial" — closed 13:40, normal close 17:00.' },
{ tool: 'check_arrival_volume', finding: 'eNAM recorded only 290 qtl vs Agmarknet 1,640 qtl.' },
{ tool: 'fetch_historical_spread', finding: 'No historical pattern of eNAM trading below Agmarknet on Madurai cotton.' },
],
},
{
mandi_id: 'MND-DGL', commodity_id: 'BANANA-ROB',
agmarknet: 1720, enam: 1980, resolved: 1880,
resolution: 'weighted_blend',
reasoning: 'Dindigul banana auctions cleared at premium due to Chithirai festival demand. eNAM captured late-session premium lots; weighted toward eNAM.',
steps: [
{ tool: 'check_festival_calendar', finding: 'Chithirai festival window active in Madurai district; banana demand spikes 8-12% historically.' },
{ tool: 'check_arrival_volume', finding: 'Normal arrivals; demand-side shock rather than supply.' },
{ tool: 'cross_check_neighbour', finding: 'Madurai banana reconciled at ₹1,910 — consistent with eNAM side.' },
],
},
{
mandi_id: 'MND-SLM', commodity_id: 'TURM-FIN',
agmarknet: 12250, enam: 11620, resolved: 12010,
resolution: 'weighted_blend',
reasoning: 'eNAM weighted toward lower-grade salem-2 variety today; Agmarknet weighted toward salem-finger premium lots. Blended to reflect mixed-grade reality.',
steps: [
{ tool: 'inspect_grade', finding: 'eNAM lot tags: 62% salem-2, 38% finger. Agmarknet: 78% finger.' },
{ tool: 'fetch_historical_spread', finding: 'Salem-2 trades ~5% discount to finger historically, explains ~90% of spread.' },
],
},
{
mandi_id: 'MND-TRY', commodity_id: 'URAD-BLK',
agmarknet: 8620, enam: 8190, resolved: 8450,
resolution: 'prefer_agmarknet',
reasoning: 'eNAM row stale — last update 14:20 yesterday, not today. Used fresh Agmarknet print.',
steps: [
{ tool: 'check_timestamps', finding: 'eNAM last_update: 2026-04-12 14:20 IST. Agmarknet last_update: 2026-04-13 10:55 IST.' },
{ tool: 'check_arrival_volume', finding: 'Agmarknet shows normal session with 620 qtl cleared today.' },
],
},
{
mandi_id: 'MND-NKL', commodity_id: 'MAIZE-Y',
agmarknet: 2020, enam: 2170, resolved: 2100,
resolution: 'midpoint',
reasoning: 'Normal intra-day spread; no red flags on either side. Took midpoint.',
steps: [
{ tool: 'check_arrival_volume', finding: 'Both sources report ~3,200-3,400 qtl cleared.' },
{ tool: 'fetch_historical_spread', finding: '30-day mean delta: 2.1%; today: 7.4% — elevated but within tail.' },
],
},
{
mandi_id: 'MND-KRR', commodity_id: 'COTN-MCU',
agmarknet: 7180, enam: 7540, resolved: 7400,
resolution: 'weighted_blend',
reasoning: 'eNAM lots include premium ginning-ready stock; Agmarknet sample skewed toward damp lower-grade. Blend reflects field-weighted market.',
steps: [
{ tool: 'inspect_grade', finding: 'Agmarknet note: "moisture 11-12%" on 3 of 5 lots. eNAM lots tagged moisture ≤8%.' },
{ tool: 'cross_check_neighbour', finding: 'Madurai and Namakkal cotton both cleared ₹7,350-7,580 — cluster supports eNAM.' },
],
},
{
mandi_id: 'MND-VLR', commodity_id: 'ONION-RED',
agmarknet: 2410, enam: 2780, resolved: 2620,
resolution: 'weighted_blend',
reasoning: 'Chennai terminal pull lifted prices during afternoon session captured by eNAM. Agmarknet morning average ran behind.',
steps: [
{ tool: 'check_session_timing', finding: 'eNAM afternoon prints 14:10-16:40 came in 15% above morning.' },
{ tool: 'cross_check_neighbour', finding: 'Chennai Koyambedu onion reconciled ₹2,490; Vellore naturally trades closer to terminal.' },
],
},
{
mandi_id: 'MND-CBE', commodity_id: 'COCO-COPRA',
agmarknet: 10480, enam: 9820, resolved: 10180,
resolution: 'prefer_agmarknet',
reasoning: 'eNAM quote included a large low-grade ball copra lot at discount that dragged the average. Agmarknet better reflects milling copra base price.',
steps: [
{ tool: 'inspect_grade', finding: 'eNAM: 2,100 qtl milling + 900 qtl ball copra at ₹9,200. Mixed average obscures grade signal.' },
{ tool: 'fetch_historical_spread', finding: 'Milling copra has traded ₹10,200-10,600 band consistently for 3 weeks.' },
],
},
]
const PRICE_CONFLICTS: PriceConflict[] = CONFLICT_SEEDS.map((c) => {
const mandi = MANDI_SEEDS.find((m) => m.id === c.mandi_id)!
const commodity = COMMODITY_BY_ID.get(c.commodity_id)!
const delta_pct = Math.abs(c.agmarknet - c.enam) / ((c.agmarknet + c.enam) / 2) * 100
return {
mandi_id: c.mandi_id,
mandi_name: mandi.name,
commodity_id: c.commodity_id,
commodity_name: commodity.name,
agmarknet_price: c.agmarknet,
enam_price: c.enam,
delta_pct,
resolution: c.resolution,
reconciled_price: c.resolved,
reasoning: c.reasoning,
investigation_steps: c.steps,
}
})
// ── Price Forecasts ─────────────────────────────────────────────────────────
const PRICE_FORECASTS: PriceForecast[] = (() => {
const out: PriceForecast[] = []
const fRand = seededRand(424242)
for (const mandi of MANDI_SEEDS) {
for (const cid of mandi.commodities) {
const c = COMMODITY_BY_ID.get(cid)
if (!c) continue
const cur = MARKET_PRICES.find((p) => p.mandi_id === mandi.id && p.commodity_id === cid)
if (!cur) continue
const current = cur.reconciled_price_rs
// Direction weighted by seasonal_index: indices > 1 tend up.
const drift = (c.seasonal_index - 1) * 0.5 + (fRand() - 0.45) * 0.06
const p7 = Math.round(current * (1 + drift * 0.35 + (fRand() - 0.5) * 0.015))
const p14 = Math.round(current * (1 + drift * 0.55 + (fRand() - 0.5) * 0.02))
const p30 = Math.round(current * (1 + drift * 0.90 + (fRand() - 0.5) * 0.03))
const band7 = current * 0.018
const band14 = current * 0.032
const band30 = current * 0.055
const direction: 'up' | 'down' | 'flat' =
p30 > current * 1.008 ? 'up' : p30 < current * 0.992 ? 'down' : 'flat'
out.push({
mandi_id: mandi.id,
mandi_name: mandi.name,
commodity_id: cid,
commodity_name: c.name,
current_price_rs: current,
price_7d: p7,
price_14d: p14,
price_30d: p30,
ci_lower_7d: Math.round(p7 - band7),
ci_upper_7d: Math.round(p7 + band7),
ci_lower_14d: Math.round(p14 - band14),
ci_upper_14d: Math.round(p14 + band14),
ci_lower_30d: Math.round(p30 - band30),
ci_upper_30d: Math.round(p30 + band30),
direction,
confidence: 0.68 + fRand() * 0.24,
seasonal_index: c.seasonal_index,
})
}
}
return out
})()
// ── Sell Recommendations (4 farmer personas) ───────────────────────────────
interface FarmerSeed {
name: string
name_ta: string
lat: number
lon: number
home_mandi: string
commodity: string
quantity: number
readiness: 'strong' | 'moderate' | 'not_yet'
advice_en: string
advice_ta: string
rec_en: string
rec_ta: string
strengths: string[]
risks: string[]
}
const FARMERS: FarmerSeed[] = [
{
name: 'Lakshmi Murugan',
name_ta: 'லட்சுமி முருகன்',
lat: 10.87, lon: 79.10,
home_mandi: 'MND-TNJ',
commodity: 'PDY-PONNI',
quantity: 28,
readiness: 'strong',
advice_en: 'Paddy prices are holding at MSP with a modest upside in the next two weeks. Selling at Thanjavur now locks in the floor; moving 18 km to Tiruchirappalli gains about ₹140/q after transport.',
advice_ta: 'நெல் விலை குறைந்தபட்ச ஆதரவு விலையில் நிலைத்து உள்ளது. அடுத்த இரண்டு வாரங்களில் சிறிய உயர்வு எதிர்பார்க்கப்படுகிறது. திருச்சிக்கு 18 கி.மீ நகர்த்தினால் போக்குவரத்துக்குப் பிறகு ஒரு குவிண்டலுக்கு ₹140 கூடுதல்.',
rec_en: 'Move 28 quintals to Tiruchirappalli this week. Net ₹2,310/q after transport vs ₹2,180 at Thanjavur. Credit profile is strong — you qualify for input loan up to ₹48,000.',
rec_ta: 'இந்த வாரம் 28 குவிண்டல் நெல்லை திருச்சிக்கு எடுத்துச் செல்லுங்கள். போக்குவரத்துக்குப் பிறகு குவிண்டலுக்கு ₹2,310; தஞ்சாவூரில் ₹2,180. உங்கள் கடன் தகுதி சிறப்பாக உள்ளது — ₹48,000 வரை உள்ளீட்டு கடன் பெறலாம்.',
strengths: ['3 seasons of reliable sales history', 'Paddy under MSP floor price guarantee', 'Strong forecast confidence (82%)'],
risks: ['Storage decay if held past 2 weeks', 'Monsoon onset mid-June'],
},
{
name: 'Kumar Selvaraj',
name_ta: 'குமார் செல்வராஜ்',
lat: 11.35, lon: 77.74,
home_mandi: 'MND-ERD',
commodity: 'TURM-FIN',
quantity: 16,
readiness: 'strong',
advice_en: 'Turmeric at Erode is trending up with 3% upside forecast over 14 days. Holding for one week is the stronger play; Salem is also clearing close to Erode prices.',
advice_ta: 'ஈரோடு மஞ்சள் விலை உயர்ந்து வருகிறது; 14 நாட்களில் 3% கூடுதல் எதிர்பார்க்கப்படுகிறது. ஒரு வாரம் காத்திருப்பது சிறந்தது. சேலமும் அதே விலை ரீதியில் உள்ளது.',
rec_en: 'Hold for 7 days then sell at Erode. Projected ₹12,640/q vs ₹12,410 today — gain of ₹3,680 on your 16 quintals after storage loss.',
rec_ta: '7 நாட்கள் காத்திருந்து ஈரோட்டில் விற்கவும். எதிர்பார்க்கும் விலை குவிண்டலுக்கு ₹12,640; இன்றைய விலை ₹12,410. உங்கள் 16 குவிண்டல் சேமிப்பு இழப்புக்குப் பிறகு ₹3,680 கூடுதல் வருமானம்.',
strengths: ['High-quality finger turmeric grade', 'Storage facility at co-operative', 'Low debt-to-revenue ratio'],
risks: ['Price volatility if Chinese demand shifts'],
},
{
name: 'Meenakshi Pandian',
name_ta: 'மீனாட்சி பாண்டியன்',
lat: 10.38, lon: 78.00,
home_mandi: 'MND-DGL',
commodity: 'BANANA-ROB',
quantity: 34,
readiness: 'moderate',
advice_en: 'Banana prices are holding firm for Chithirai festival demand but shelf life is the constraint. Sell at Dindigul within 5 days. Moving to Madurai adds ₹70/q but transport risk is high for perishables.',
advice_ta: 'சித்திரை திருவிழா தேவையால் வாழைப்பழம் விலை நிலைத்து உள்ளது, ஆனால் சீக்கிரம் பழுக்கும். 5 நாட்களுக்குள் திண்டுக்கல்லில் விற்கவும். மதுரைக்கு நகர்த்தினால் ₹70 கூடுதல், ஆனால் கெடக்கூடிய சரக்கு ஆபத்து அதிகம்.',
rec_en: 'Sell at Dindigul within 5 days. Projected net ₹1,880/q. Festival tailwind closes April 20; after that, prices revert quickly.',
rec_ta: '5 நாட்களுக்குள் திண்டுக்கல்லில் விற்கவும். எதிர்பார்க்கும் நிகர விலை குவிண்டலுக்கு ₹1,880. ஏப்ரல் 20-ம் தேதிக்குப் பிறகு திருவிழா ஆதரவு முடிந்து விலை குறையும்.',
strengths: ['Established relationship with Dindigul commission agent', 'Quality grade-1 robusta'],
risks: ['Perishable — window closes in 5 days', 'Last season missed two payment cycles on input loan', 'Festival premium fades after April 20'],
},
{
name: 'Arun Rajendran',
name_ta: 'அருண் ராஜேந்திரன்',
lat: 11.22, lon: 78.17,
home_mandi: 'MND-NKL',
commodity: 'MAIZE-Y',
quantity: 42,
readiness: 'not_yet',
advice_en: 'Maize is flat with slight downside. Holding is risky — storage loss compounds. Sell half at Namakkal now, hold half for 2 weeks to average the price.',
advice_ta: 'மக்காச்சோளம் விலை நிலையாக உள்ளது, சிறிய சரிவு சாத்தியம். காத்திருப்பது ஆபத்து; சேமிப்பு இழப்பு அதிகரிக்கும். அரை பங்கு நாமக்கல்லில் இப்போது விற்கவும், மீதி 2 வாரங்கள் காத்திருக்கவும்.',
rec_en: 'Split sale recommended: 21 qtl at Namakkal now (₹2,100/q), hold 21 qtl for 14 days. Current credit history does not support a new input loan cycle — clear outstanding first.',
rec_ta: 'பிரித்து விற்பனை பரிந்துரை: இப்போது 21 குவிண்டல் நாமக்கல்லில் (குவிண்டலுக்கு ₹2,100), மீதி 21 குவிண்டல் 14 நாட்கள் காத்திருக்கவும். தற்போதைய கடன் வரலாற்றில் புதிய உள்ளீட்டு கடன் தகுதி இல்லை — முதலில் நிலுவை தொகையை செலுத்துங்கள்.',
strengths: ['Large volume gives negotiating leverage'],
risks: ['Outstanding input loan from last season', 'No storage facility — loss risk is high', 'Forecast direction weak'],
},
]
function buildOptionsFor(farmer: FarmerSeed): { best: SellOption; all: SellOption[] } {
const mandisWithCommodity = MANDI_SEEDS.filter((m) => m.commodities.includes(farmer.commodity))
const home = MANDI_SEEDS.find((m) => m.id === farmer.home_mandi)!
const fRand = seededRand(farmer.name.length * 997 + 13)
const options: SellOption[] = []
for (const mandi of mandisWithCommodity) {
const price = MARKET_PRICES.find((p) => p.mandi_id === mandi.id && p.commodity_id === farmer.commodity)
if (!price) continue
const market = price.reconciled_price_rs
const dx = (mandi.lat - farmer.lat) * 110.5
const dy = (mandi.lon - farmer.lon) * 100.3
const dist = Math.sqrt(dx * dx + dy * dy)
const transport = Math.round(2.8 * dist + 40) // ₹ per quintal
const storage = Math.round(market * 0.008 + fRand() * 12)
const fee = Math.round(market * 0.012)
const net = market - transport - storage - fee
options.push({
mandi_id: mandi.id,
mandi_name: mandi.name,
sell_timing: dist < 25 ? 'now' : dist < 80 ? 'this week' : 'within 10 days',
market_price_rs: market,
transport_cost_rs: transport,
storage_loss_rs: storage,
mandi_fee_rs: fee,
net_price_rs: net,
distance_km: dist,
drive_time_min: Math.round(dist * 1.6),
confidence: 0.72 + fRand() * 0.22,
})
}
// Ensure home mandi is always present (even if commodities list missed it)
if (!options.some((o) => o.mandi_id === home.id)) {
const price = MARKET_PRICES.find(
(p) => p.mandi_id === home.id && p.commodity_id === farmer.commodity,
)
if (price) {
options.push({
mandi_id: home.id,
mandi_name: home.name,
sell_timing: 'now',
market_price_rs: price.reconciled_price_rs,
transport_cost_rs: 60,
storage_loss_rs: Math.round(price.reconciled_price_rs * 0.008),
mandi_fee_rs: Math.round(price.reconciled_price_rs * 0.012),
net_price_rs: price.reconciled_price_rs - 60 - Math.round(price.reconciled_price_rs * 0.020),
distance_km: 8,
drive_time_min: 14,
confidence: 0.8,
})
}
}
const sorted = [...options].sort((a, b) => b.net_price_rs - a.net_price_rs)
return { best: sorted[0], all: options }
}
const SELL_RECOMMENDATIONS: SellRecommendation[] = FARMERS.map((f) => {
const { best, all } = buildOptionsFor(f)
const worst = Math.min(...all.map((o) => o.net_price_rs))
const gain = best.net_price_rs - worst
const expected = best.net_price_rs * f.quantity
const minRevenue = Math.round(expected * 0.88)
const maxLoan =
f.readiness === 'strong' ? Math.round(minRevenue * 0.55) :
f.readiness === 'moderate' ? Math.round(minRevenue * 0.32) :
0
return {
farmer_id: `FMR-${f.name.split(' ')[0].toUpperCase().slice(0, 4)}`,
farmer_name: f.name,
commodity_id: f.commodity,
commodity_name: COMMODITY_BY_ID.get(f.commodity)!.name,
quantity_quintals: f.quantity,
farmer_lat: f.lat,
farmer_lon: f.lon,
best_option: best,
all_options: all,
potential_gain_rs: gain,
recommendation_text: f.rec_en,
// Mock data is Tamil-only (India demo fixtures). Phase 1.4 rename:
// the canonical field is `recommendation_local` with a `local_language_code`.
recommendation_local: f.rec_ta,
local_language_code: 'ta',
credit_readiness: {
readiness: f.readiness,
expected_revenue_rs: expected,
min_revenue_rs: minRevenue,
max_advisable_input_loan_rs: maxLoan,
revenue_confidence: f.readiness === 'strong' ? 0.84 : f.readiness === 'moderate' ? 0.68 : 0.42,
loan_to_revenue_pct: expected > 0 ? (maxLoan / expected) * 100 : 0,
strengths: f.strengths,
risks: f.risks,
advice_en: f.advice_en,
advice_ta: f.advice_ta,
},
}
})
// ── Delivery Logs ───────────────────────────────────────────────────────────
const DELIVERY_LOGS: DeliveryLog[] = FARMERS.flatMap((f, idx) => {
const ok: DeliveryLog = {
farmer_id: `FMR-${f.name.split(' ')[0].toUpperCase().slice(0, 4)}`,
farmer_name: f.name,
phone: `+9194${(4300000 + idx * 137).toString().slice(0, 7)}`,
channel: 'sms',
sms_text: f.rec_en,
sms_text_local: f.rec_ta,
status: idx === 3 ? 'dry_run' : 'sent',
error: null,
created_at: isoHoursAgo(2 + idx * 3),
}
return [ok]
})
// ── Pipeline Runs (15 daily runs) ──────────────────────────────────────────
function makeRun(daysAgo: number, status: 'success' | 'failed' | 'partial'): PipelineRun {
const runRand = seededRand(1000 + daysAgo)
const stepNames = ['ingest', 'extract', 'reconcile', 'forecast', 'optimize', 'recommend', 'deliver']
const steps: PipelineStep[] = stepNames.map((name, i) => {
const base = 1.8 + runRand() * 4.2
// If partial, fail the 4th step (forecast). If failed, fail the 2nd.
let st: string = 'success'
if (status === 'partial' && i === 3) st = 'failed'
else if (status === 'partial' && i > 3) st = 'skipped'
else if (status === 'failed' && i === 1) st = 'failed'
else if (status === 'failed' && i > 1) st = 'skipped'
return {
step: name,
status: st,
duration_s: st === 'skipped' ? 0 : Math.round(base * 10) / 10,
}
})
const total = steps.reduce((s, x) => s + x.duration_s, 0)
const started = new Date(NOW.getTime() - daysAgo * 86_400_000 - 6 * 3600_000)
const ended = new Date(started.getTime() + total * 1000)
return {
run_id: `run-${started.toISOString().slice(0, 10)}-${String(daysAgo).padStart(2, '0')}`,
started_at: started.toISOString(),
ended_at: ended.toISOString(),
status,
duration_s: total,
steps,
total_cost_usd: 0.12 + runRand() * 0.08,
}
}
const PIPELINE_RUNS: PipelineRun[] = (() => {
const runs: PipelineRun[] = []
for (let d = 0; d < 15; d++) {
let status: 'success' | 'failed' | 'partial' = 'success'
if (d === 4) status = 'partial'
else if (d === 9) status = 'failed'
else if (d === 11) status = 'partial'
runs.push(makeRun(d, status))
}
return runs
})()
// ── Pipeline Stats ──────────────────────────────────────────────────────────
const PIPELINE_STATS: PipelineStats = {
total_runs: 127,
success_rate: 0.94,
mandis_monitored: MANDIS.length,
commodities_tracked: COMMODITIES.length,
price_conflicts_found: PRICE_CONFLICTS.length,
total_cost_usd: 18.42,
last_run: PIPELINE_RUNS[0].ended_at,
data_sources: ['Agmarknet', 'eNAM'],
}
// ── Raw / Extracted / Reconciled sample blobs ───────────────────────────────
const RAW_INPUTS: RawInputsResponse = {
raw_inputs: {
source: 'agmarknet+enam',
fetched_at: isoHoursAgo(5),
agmarknet_rows: MARKET_PRICES.length,
enam_rows: MARKET_PRICES.filter((p) => p.enam_price_rs !== null).length,
sample_agmarknet: {
mandi: 'Erode',
commodity: 'Turmeric (Finger)',
min: '11600',
max: '12100',
modal: '11850',
arrivals_qtl: '620',
date: dateDaysAgo(0),
},
sample_enam: {
mandi_code: 'TN-ERD',
commodity_code: 'TURM',
grade: 'finger',
auction_close_price: '12780',
lots_cleared: 14,
volume_qtl: 1840,
},
},
sources: ['agmarknet', 'enam'],
}
const EXTRACTED_DATA: ExtractedDataResponse = {
extracted_data: {
structured_rows: MARKET_PRICES.length,
columns: ['mandi_id', 'commodity_id', 'min_rs', 'max_rs', 'modal_rs', 'arrivals_qtl', 'date'],
sample: MARKET_PRICES.slice(0, 3).map((p) => ({
mandi_id: p.mandi_id,
commodity_id: p.commodity_id,
min_rs: Math.round(p.reconciled_price_rs * 0.97),
max_rs: Math.round(p.reconciled_price_rs * 1.04),
modal_rs: p.reconciled_price_rs,
arrivals_qtl: Math.round(600 + (p.reconciled_price_rs % 1700)),
date: p.date,
})),
},
total_mandis: MANDIS.length,
}
const RECONCILED_DATA: ReconciledDataResponse = {
reconciled_data: {
rows: MARKET_PRICES.length,
conflicts_resolved: PRICE_CONFLICTS.length,
methods: { weighted_blend: 6, midpoint: 2, prefer_agmarknet: 3, prefer_enam: 1 },
last_reconciled_at: isoHoursAgo(4),
},
total_mandis: MANDIS.length,
total_conflicts: PRICE_CONFLICTS.length,
}
// ── Model Info ──────────────────────────────────────────────────────────────
const MODEL_INFO: ModelInfoResponse = {
model_metrics: {
model_type: 'Chronos-Bolt-Tiny + XGBoost MOS',
rmse: 142.6,
mae: 98.2,
r2: 0.83,
features: [
'lag_1d', 'lag_7d', 'lag_30d',
'rolling_mean_14d', 'rolling_std_14d',
'arrivals_qtl', 'arrivals_ratio_30d',
'month_sin', 'month_cos',
'mandi_bias', 'commodity_seasonality',
'monsoon_phase', 'festival_flag',
'neighbour_mean_price', 'transport_cost_index',
],
feature_importances: {
lag_7d: 0.22,
arrivals_qtl: 0.14,
commodity_seasonality: 0.12,
lag_1d: 0.11,
rolling_mean_14d: 0.09,
neighbour_mean_price: 0.08,
mandi_bias: 0.07,
monsoon_phase: 0.06,
festival_flag: 0.05,
transport_cost_index: 0.03,
month_sin: 0.02,
month_cos: 0.01,
},
train_samples: 18420,
test_samples: 2060,
},
ml_stack: {
primary_model: {
type: 'Chronos-Bolt-Tiny',
features: 15,
metrics: { mae_inr_per_qtl: 98.2, mape_pct: 3.8 },
},
agents: {
extraction: 'Claude Haiku 4.5',
reconciliation: 'Claude Sonnet 4.6',
recommendation: 'Claude Sonnet 4.6',
},
trained_on: '2026-03-14',
},
}
// ── Response wrappers ───────────────────────────────────────────────────────
export const mandisResponse: MandisResponse = { mandis: MANDIS, total: MANDIS.length }
export const marketPricesResponse: MarketPricesResponse = {
market_prices: MARKET_PRICES,
total: MARKET_PRICES.length,
}
export const priceForecastsResponse: PriceForecastsResponse = {
price_forecasts: PRICE_FORECASTS,
total: PRICE_FORECASTS.length,
}
export const sellRecommendationsResponse: SellRecommendationsResponse = {
sell_recommendations: SELL_RECOMMENDATIONS,
total: SELL_RECOMMENDATIONS.length,
}
export const priceConflictsResponse: PriceConflictsResponse = {
price_conflicts: PRICE_CONFLICTS,
total: PRICE_CONFLICTS.length,
}
export const rawInputsResponse: RawInputsResponse = RAW_INPUTS
export const extractedDataResponse: ExtractedDataResponse = EXTRACTED_DATA
export const reconciledDataResponse: ReconciledDataResponse = RECONCILED_DATA
export const modelInfoResponse: ModelInfoResponse = MODEL_INFO
export const deliveryLogsResponse: DeliveryLogsResponse = {
delivery_logs: DELIVERY_LOGS,
total: DELIVERY_LOGS.length,
}
export const pipelineRunsResponse: PipelineRunsResponse = {
runs: PIPELINE_RUNS,
total: PIPELINE_RUNS.length,
}
export const pipelineStatsResponse: PipelineStats = PIPELINE_STATS