/** * Demo data seeder — creates realistic missions across all lifecycle stages * for the product demo video. Keeps existing user missions intact. * * Usage: * tsx packages/db/src/seeds/demo-data.ts * (or set infra/.env vars first and then run) */ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { eq, and, sql } from "drizzle-orm"; import { randomUUID } from "node:crypto"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const envPath = resolve(__dirname, "../../../../infra/.env"); function loadEnv(path: string): void { let raw: string; try { raw = readFileSync(path, "utf8"); } catch { return; } for (const line of raw.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eqIdx = trimmed.indexOf("="); if (eqIdx === -1) continue; const key = trimmed.slice(0, eqIdx).trim(); const val = trimmed.slice(eqIdx + 1).trim(); if (!process.env[key]) { process.env[key] = val; } } } loadEnv(envPath); import { getDb, closeDb } from "../index.js"; import * as s from "../schema.js"; const DB = getDb(); // ── Constants ───────────────────────────────────────────────────── const USER_ID = "3599b3f5-2123-4175-be72-65a56346cdbe"; type NewMissionStatusSnapshot = typeof s.missionStatusSnapshots.$inferInsert; type NewRoutePlan = typeof s.routePlans.$inferInsert; // Existing missions we will enhance with proper data const EXISTING_FIX_MISSIONS: Array<{ id: string; title: string; domain: string; state: string }> = [ { id: "a047c8fb-cdcd-4ae3-bb86-f87d7ae9848d", title: "The user was physically assaulted by an individual named Raj Manohar.", domain: "consumer", state: "facts_confirmed", }, { id: "d05bc133-bb2e-4bcb-b43d-9b1ee7d43483", title: "I paid 50 on a scanner but it took 5000 from my account .", domain: "payment", state: "facts_confirmed", }, ]; function hash(text: string): string { return createHash("sha256").update(text, "utf8").digest("hex"); } // ── Helpers ────────────────────────────────────────────────────── function makeFacts(base: Record): Record { return { dates: [{ iso: "2026-05-28", label: "incident_date" }], amounts: [], keyFacts: {}, missingInfoQuestions: [], ...base, }; } function makeSnapshot(missionId: string, state: string, overrides?: Record) { return { missionId, state, headline: state_to_headline(state), nextUserAction: null, nextSystemAction: null, primaryButton: null, danger: false, confidence: null, ...overrides, } as NewMissionStatusSnapshot; } function state_to_headline(state: string): string { const h: Record = { created: "Mission created — let's gather the details.", intake_in_progress: "AI is analyzing your grievance…", needs_user_input: "Please provide more details about your issue.", human_review_required: "⚠️ This case needs urgent human review — helpline info below.", facts_pending_confirmation: "I've structured your grievance — please review the facts.", facts_confirmed: "Facts confirmed — ready for legal grounding.", route_plan_ready: "Route plan ready — steps grounded in applicable law.", actions_preview_ready: "Action preview ready — review before approving.", actions_awaiting_consent: "Awaiting your consent to send.", action_executing: "Sending your action now…", sent_by_email: "Action sent by email — waiting for confirmation.", user_confirmed_external_submission: "Action confirmed — waiting for response.", waiting_for_response: "Waiting for response from the authority.", resolver_reply_received: "Reply received — review and update status.", resolution_pending: "Resolution pending your input.", resolution_claimed: "Outcome recorded — thank you.", resolved: "Mission resolved ✓", closed: "Mission closed.", }; return h[state] ?? `State: ${state}`; } // ── Create route plan steps ────────────────────────────────────── function makeRouteSteps(domain: string, title: string): NewRoutePlan["steps"] { const base: Array<{ stepNo: number; actionType: string; title: string; description: string; authority: string; authorityUrl: string; riskLevel: string; deadlines: Record; }> = [ { stepNo: 0, actionType: "send_email", title: `Email ${title}`, description: `Draft and send a formal complaint email regarding ${title.toLowerCase()}.`, authority: "Concerned Authority", authorityUrl: "https://example.gov.in", riskLevel: "low", deadlines: {}, }, { stepNo: 1, actionType: "send_email", title: `Follow up with Regulator`, description: `Escalate to the relevant regulatory body if no response is received.`, authority: "Regulatory Body", authorityUrl: domain === "cyber_fraud" ? "https://cybercrime.gov.in" : "https://consumerhelpline.gov.in", riskLevel: "medium", deadlines: { followUpDays: 7 }, }, { stepNo: 2, actionType: "post_social", title: `Public post on social media`, description: `Draft a public post about the unresolved grievance to generate attention.`, authority: "X / Threads", authorityUrl: "https://x.com", riskLevel: "medium", deadlines: {}, }, ]; return base as any; } function makeRouteCitations(): NewRoutePlan["citations"] { return [ { chunkId: randomUUID(), actName: "Consumer Protection Act 2019", section: "2(9)", relevance: "Defines consumer rights" }, { chunkId: randomUUID(), actName: "IT Act 2000", section: "66A", relevance: "Cyber fraud provisions" }, ] as any; } // ── Missions to create ─────────────────────────────────────────── interface MissionTemplate { title: string; domain: string; state: string; safetyLevel: string; description: string; opposingParty: Record; reliefRequested: Record; facts: Record; evidence: boolean; routePlan: boolean; actions: boolean; actionsExecuted: boolean; publicPage: boolean; resolution: boolean; swarm: boolean; takedown: boolean; caseLookup: boolean; followup: boolean; } const NEW_MISSIONS: MissionTemplate[] = [ // 1. Flipkart defective phone - route_plan_ready { title: "Bought a defective smartphone from Flipkart, replacement refused", domain: "consumer", state: "route_plan_ready", safetyLevel: "normal", description: "I ordered a Samsung Galaxy M35 from Flipkart on 15 May 2026. The phone arrived with a dead pixel cluster and battery draining 50% in 2 hours. Flipkart customer care refused replacement saying the 7-day return window expired (they counted the day I placed the order as day 1). I have unboxing video evidence.", opposingParty: { name: "Flipkart Internet Pvt Ltd", kind: "company", handles: { x: "@Flipkart", email: "support@flipkart.com" } }, reliefRequested: { type: "replacement", description: "Full replacement of defective phone or full refund of ₹15,999", amount: 15999, currency: "INR" }, facts: makeFacts({ summary: "Samsung Galaxy M35 purchased from Flipkart. Dead pixel cluster and battery defect. Flipkart refused replacement claiming 7-day window expired. Unboxing video available as evidence.", issueDomain: "consumer", confidence: 0.88 }), evidence: true, routePlan: true, actions: false, actionsExecuted: false, publicPage: false, resolution: false, swarm: true, takedown: false, caseLookup: true, followup: false, }, // 2. Amazon fake product - actions_preview_ready { title: "Amazon sold counterfeit 'Boat' earphones, need legal action", domain: "consumer", state: "actions_preview_ready", safetyLevel: "normal", description: "I ordered 'Boat Airdopes 141' from Amazon for ₹1,999. The product received is a cheap counterfeit - different packaging, poor sound quality, wrong serial number. When I raised a return request, Amazon asked me to ship it back but the courier pickup never happened (3 attempts missed). I've filed a complaint on the National Consumer Helpline portal (NCH ref: 20260528-1234).", opposingParty: { name: "Amazon Seller Services Pvt Ltd", kind: "company", handles: { x: "@AmazonHelp", email: "resolution@amazon.in" } }, reliefRequested: { type: "refund", description: "Full refund of ₹1,999 for counterfeit product + compensation for harassment", amount: 5000, currency: "INR" }, facts: makeFacts({ summary: "Counterfeit Boat Airdopes purchased from Amazon at ₹1,999. Product is clearly fake - different packaging, wrong serial numbers. Amazon's return process failed 3 times. National Consumer Helpline complaint filed.", issueDomain: "consumer", confidence: 0.92 }), evidence: true, routePlan: true, actions: true, actionsExecuted: false, publicPage: false, resolution: false, swarm: true, takedown: false, caseLookup: false, followup: false, }, // 3. Paytm failed refund - actions_awaiting_consent { title: "Paytm failed to refund ₹12,000 for cancelled flight ticket", domain: "payment", state: "actions_awaiting_consent", safetyLevel: "normal", description: "I booked a Delhi-Mumbai Indigo flight via Paytm on 20 May 2026. The flight was cancelled by Indigo. Paytm promised refund within 7 working days. It's been 11 days - no refund. Customer care gives different excuses each time. I have screenshots of the booking and the cancellation notice.", opposingParty: { name: "Paytm Payments Bank Ltd", kind: "company", handles: { x: "@PaytmCare", email: "complaints@paytm.com" } }, reliefRequested: { type: "refund", description: "Immediate refund of ₹12,000 for cancelled flight", amount: 12000, currency: "INR" }, facts: makeFacts({ summary: "Indigo flight cancelled via Paytm. Refund of ₹12,000 not processed for 11 days despite 7-day promise. Multiple customer care interactions with no resolution.", issueDomain: "payment", confidence: 0.95 }), evidence: true, routePlan: true, actions: true, actionsExecuted: false, publicPage: false, resolution: false, swarm: true, takedown: false, caseLookup: false, followup: false, }, // 4. Zomato order issue - action_executing { title: "Zomato delivery person misbehaved and order was incomplete", domain: "consumer", state: "action_executing", safetyLevel: "normal", description: "I ordered from Zomato (order #ZM-2026-05-28-8765). The delivery person was aggressive, demanded extra payment, and two items were missing from the order. Zomato support initially promised a refund but then closed my ticket saying 'delivery completed satisfactorily'. I have photos of the incomplete order and chat transcripts.", opposingParty: { name: "Zomato Ltd", kind: "company", handles: { x: "@ZomatoSupport", email: "support@zomato.com" } }, reliefRequested: { type: "refund", description: "Refund for missing items + compensation for harassment by delivery partner", amount: 850, currency: "INR" }, facts: makeFacts({ summary: "Zomato order #ZM-2026-05-28-8765 - delivery person harassed and demanded extra payment. 2 items missing. Zomato closed the ticket prematurely. Photo evidence and chat transcripts available.", issueDomain: "consumer", confidence: 0.91 }), evidence: true, routePlan: true, actions: true, actionsExecuted: true, publicPage: false, resolution: false, swarm: false, takedown: false, caseLookup: false, followup: false, }, // 5. HDFC bank error - sent_by_email { title: "HDFC Bank charged wrong EMI amount for 3 consecutive months", domain: "payment", state: "user_confirmed_external_submission", safetyLevel: "normal", description: "I took a personal loan of ₹2,00,000 from HDFC Bank in Jan 2026 at 10.5% ROI. The EMI should be ₹6,800/month. Since March 2026, HDFC has been charging ₹8,200/month - an excess of ₹1,400/month. I've visited the branch 4 times, called customer care 12+ times. They keep saying 'system error, will be fixed next month' but it keeps happening.", opposingParty: { name: "HDFC Bank Ltd", kind: "company", handles: { x: "@HDFCBank_Care", email: "customerservice@hdfcbank.com" } }, reliefRequested: { type: "refund", description: "Refund of excess EMI charged (₹4,200 total) + correction of EMI amount", amount: 4200, currency: "INR" }, facts: makeFacts({ summary: "HDFC Bank personal loan EMI overcharged by ₹1,400/month for 3 months (₹4,200 total excess). Branch visits and calls unresolved. System error excuse given repeatedly.", issueDomain: "payment", confidence: 0.94 }), evidence: true, routePlan: true, actions: true, actionsExecuted: true, publicPage: false, resolution: false, swarm: false, takedown: false, caseLookup: false, followup: true, }, // 6. Airtel wrong bill - user_confirmed_external_submission { title: "Airtel charging for internet pack I never subscribed to", domain: "consumer", state: "waiting_for_response", safetyLevel: "normal", description: "Airtel has been charging me ₹299/month for a 'Data Add-on Pack' since April 2026. I never subscribed to this. When I check MyAirtel app, there's no such active pack visible. Customer care says I subscribed via SMS but they can't show proof. I've been overcharged ₹897 so far (3 months).", opposingParty: { name: "Bharti Airtel Ltd", kind: "company", handles: { x: "@Airtel_Presence", email: "complaints@airtel.in" } }, reliefRequested: { type: "refund", description: "Refund of ₹897 for unauthorized charges + stop the unwanted pack", amount: 897, currency: "INR" }, facts: makeFacts({ summary: "Airtel charged ₹299/month for 3 months for an unsolicited data pack. No proof of subscription available. Overcharged ₹897 total.", issueDomain: "consumer", confidence: 0.93 }), evidence: true, routePlan: true, actions: true, actionsExecuted: true, publicPage: false, resolution: false, swarm: false, takedown: false, caseLookup: false, followup: true, }, // 7. Swiggy refund - resolution_claimed { title: "Swiggy gave stale food, partial refund not acceptable", domain: "consumer", state: "resolution_claimed", safetyLevel: "normal", description: "Ordered paneer butter masala from Swiggy (order #SW-2026-05-25-4321). The food was stale and smelled fermented. Swiggy offered ₹50 refund on a ₹420 order. I want full refund. Food safety is non-negotiable.", opposingParty: { name: "Swiggy Ltd", kind: "company", handles: { x: "@SwiggySupport", email: "support@swiggy.in" } }, reliefRequested: { type: "refund", description: "Full refund of ₹420 for stale food", amount: 420, currency: "INR" }, facts: makeFacts({ summary: "Stale paneer butter masala delivered by Swiggy (order ₹420). Swiggy offered only ₹50 refund. Photo evidence of stale food available.", issueDomain: "consumer", confidence: 0.96 }), evidence: true, routePlan: true, actions: true, actionsExecuted: true, publicPage: false, resolution: true, swarm: false, takedown: false, caseLookup: false, followup: false, }, // 8. Amazon refund resolved - resolved with public page + ledger { title: "Amazon Fresh delivered rotten vegetables, resolved after complaint", domain: "consumer", state: "resolved", safetyLevel: "normal", description: "Amazon Fresh delivered a box of rotten tomatoes and spoiled spinach on 22 May 2026 (order #AF-20260522-7890). After escalating to National Consumer Helpline, Amazon issued full refund of ₹678 and a ₹200 voucher as compensation.", opposingParty: { name: "Amazon Fresh / Amazon Seller Services", kind: "company", handles: { x: "@AmazonHelp", email: "fresh-support@amazon.in" } }, reliefRequested: { type: "refund", description: "Full refund for rotten vegetables delivered by Amazon Fresh", amount: 678, currency: "INR" }, facts: makeFacts({ summary: "Rotten vegetables delivered by Amazon Fresh. After NCH escalation, full refund of ₹678 + ₹200 compensation voucher received. Resolution achieved.", issueDomain: "consumer", confidence: 0.99 }), evidence: true, routePlan: true, actions: true, actionsExecuted: true, publicPage: true, resolution: true, swarm: false, takedown: false, caseLookup: true, followup: false, }, // 9. Domestic violence - human_review_required (emergency) { title: "URGENT: Physical harassment and threats from neighbour", domain: "cyber_fraud", state: "human_review_required", safetyLevel: "emergency", description: "My neighbour Rajesh has been threatening me and my family. He claims to have connections with local goons. Last night he physically pushed my mother. We have CCTV footage. The local police station refuses to file an FIR saying it's a 'civil dispute'. We are scared for our safety.", opposingParty: { name: "Rajesh Sharma", kind: "individual", handles: {} }, reliefRequested: { type: "investigation", description: "Police protection + FIR filing against threatening neighbour", amount: null, currency: "INR" }, facts: makeFacts({ summary: "URGENT: Neighbour Rajesh Sharma physically assaulted family member, issued threats. Police refusing to file FIR. CCTV evidence available. Emergency situation requiring immediate intervention.", issueDomain: "cyber_fraud", confidence: 0.85, missingInfoQuestions: ["Have you called the police emergency number (112)?", "Do you have medical records of the assault?"] }), evidence: true, routePlan: false, actions: false, actionsExecuted: false, publicPage: false, resolution: false, swarm: false, takedown: true, caseLookup: false, followup: false, // Takendown needs an action reference, so create a minimal action _extraAction: "emergency_report", } as MissionTemplate, // 10. SBI card issue - resolver_reply_received { title: "SBI Credit Card charged for fraudulent transaction, bank not helping", domain: "payment", state: "resolver_reply_received", safetyLevel: "normal", description: "On 15 May 2026, a fraudulent transaction of ₹47,500 was made on my SBI Credit Card from an unknown merchant 'Digital Solutions Pvt Ltd'. I reported this to SBI within 2 hours (ticket #SBI-CC-20260515-8765). SBI says they're 'investigating' but after 15 days, no resolution. I've been charged late fees because the fraudulent amount is outstanding.", opposingParty: { name: "State Bank of India", kind: "bank", handles: { x: "@SBICard_Care", email: "customercare@sbicard.com" } }, reliefRequested: { type: "refund", description: "Reverse fraudulent ₹47,500 transaction + waive late fees + compensation for harassment", amount: 50000, currency: "INR" }, facts: makeFacts({ summary: "Fraudulent transaction of ₹47,500 on SBI Credit Card. Reported within 2 hours but no resolution after 15 days. Late fees being charged. RBI Ombudsman complaint being considered.", issueDomain: "payment", confidence: 0.90 }), evidence: true, routePlan: true, actions: true, actionsExecuted: true, publicPage: false, resolution: false, swarm: false, takedown: false, caseLookup: true, followup: true, }, ]; // ── Main seeder ────────────────────────────────────────────────── async function main() { console.log("[demo] Seeding demo data…\n"); // Step 1: Enhance existing missions that are incomplete console.log("[demo] Enhancing existing missions…"); for (const fix of EXISTING_FIX_MISSIONS) { const missionId = fix.id; // Add missing snapshots if needed const existingSnaps = await DB.select({ state: s.missionStatusSnapshots.state }) .from(s.missionStatusSnapshots) .where(eq(s.missionStatusSnapshots.missionId, missionId)); const snapStates = new Set(existingSnaps.map((r) => r.state)); // Ensure created snapshot if (!snapStates.has("created")) { await DB.insert(s.missionStatusSnapshots).values(makeSnapshot(missionId, "created", { createdAt: new Date("2026-05-28T08:00:00Z") })); } // Ensure facts_pending_confirmation snapshot if (!snapStates.has("facts_pending_confirmation") && fix.state !== "created") { await DB.insert(s.missionStatusSnapshots).values(makeSnapshot(missionId, "facts_pending_confirmation", { createdAt: new Date("2026-05-28T08:05:00Z") })); } // Ensure facts_confirmed snapshot if (!snapStates.has("facts_confirmed") && fix.state === "facts_confirmed") { await DB.insert(s.missionStatusSnapshots).values( makeSnapshot(missionId, "facts_confirmed", { createdAt: new Date("2026-05-28T08:10:00Z") }) ); } // Add events const existingEvents = await DB.select({ eventType: s.missionEvents.eventType }) .from(s.missionEvents) .where(eq(s.missionEvents.missionId, missionId)); const eventTypes = new Set(existingEvents.map((r) => r.eventType)); if (!eventTypes.has("mission.created")) { await DB.insert(s.missionEvents).values({ missionId, eventType: "mission.created", actorType: "user", payload: { title: fix.title, source: "browser", issueDomain: fix.domain }, createdAt: new Date("2026-05-28T08:00:00Z"), }); } if (!eventTypes.has("intake.completed")) { await DB.insert(s.missionEvents).values({ missionId, eventType: "intake.completed", actorType: "ai", payload: { summary: fix.title, confidence: 0.85 }, createdAt: new Date("2026-05-28T08:05:00Z"), }); } // Add conversations + messages for these missions const existingConvs = await DB.select({ id: s.conversations.id }) .from(s.conversations) .where(and(eq(s.conversations.userId, USER_ID), eq(s.conversations.activeMissionId, missionId))); if (existingConvs.length === 0) { const convId = randomUUID(); await DB.insert(s.conversations).values({ id: convId, userId: USER_ID, channel: "browser", activeMissionId: missionId, lastMessageAt: new Date("2026-05-28T08:00:00Z"), }); await DB.insert(s.messages).values({ conversationId: convId, missionId, direction: "inbound", channel: "browser", body: fix.title, createdAt: new Date("2026-05-28T08:00:00Z"), }); } console.log(` ✓ Enhanced mission: "${fix.title.slice(0, 50)}…"`); } // Step 2: Create new missions console.log("\n[demo] Creating new missions…"); for (const tmpl of NEW_MISSIONS) { const missionId = randomUUID(); const createdAt = new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000); // Within last 7 days // Insert mission await DB.insert(s.missions).values({ id: missionId, userId: USER_ID, title: tmpl.title, issueDomain: tmpl.domain, state: tmpl.state, safetyLevel: tmpl.safetyLevel, opposingParty: tmpl.opposingParty as any, reliefRequested: tmpl.reliefRequested as any, facts: tmpl.facts as any, createdAt, updatedAt: createdAt, }); // ── Events ── const eventBase = { missionId, actorType: "system" as const }; await DB.insert(s.missionEvents).values([ { ...eventBase, eventType: "mission.created", actorType: "user", payload: { title: tmpl.title, source: "browser", issueDomain: tmpl.domain }, createdAt: new Date(createdAt) }, { ...eventBase, eventType: "intake.completed", actorType: "ai", payload: { summary: tmpl.facts.summary, confidence: (tmpl.facts.confidence as number) ?? 0.85 }, createdAt: new Date(createdAt.getTime() + 5 * 60 * 1000) }, ]); if (tmpl.routePlan) { await DB.insert(s.missionEvents).values({ ...eventBase, eventType: "route_plan_created", actorType: "ai", payload: { stepsCount: 3, claimsCount: 2, confidence: 0.9 }, createdAt: new Date(createdAt.getTime() + 10 * 60 * 1000), }); } // ── Snapshots ── const snapBase = { missionId, danger: false }; const snapshots: NewMissionStatusSnapshot[] = [ makeSnapshot(missionId, "created", { ...snapBase, createdAt: new Date(createdAt) }), ]; if (["facts_pending_confirmation", "facts_confirmed", "route_plan_ready", "actions_preview_ready", "actions_awaiting_consent", "action_executing", "sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved", "human_review_required"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "facts_pending_confirmation", { ...snapBase, createdAt: new Date(createdAt.getTime() + 5 * 60 * 1000), headline: `I've structured your grievance — please review the facts. ${(tmpl.facts.missingInfoQuestions as string[] | undefined)?.length ? "I need a few more details." : ""}`, nextUserAction: "Review and confirm the extracted facts.", nextSystemAction: "Ground your case in the relevant law and pick a route.", primaryButton: "Confirm facts", confidence: 0.85, })); } if (["facts_confirmed", "route_plan_ready", "actions_preview_ready", "actions_awaiting_consent", "action_executing", "sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "facts_confirmed", { ...snapBase, createdAt: new Date(createdAt.getTime() + 8 * 60 * 1000), headline: "Facts confirmed — ready for legal grounding.", nextUserAction: "Proceed to route plan.", })); } if (["route_plan_ready", "actions_preview_ready", "actions_awaiting_consent", "action_executing", "sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "route_plan_ready", { ...snapBase, createdAt: new Date(createdAt.getTime() + 12 * 60 * 1000), headline: `Route plan ready — 3 steps, grounded in applicable law.`, nextUserAction: "Review the route plan and approve the first action.", primaryButton: "View route plan", confidence: 0.9, })); } if (["actions_preview_ready", "actions_awaiting_consent", "action_executing", "sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "actions_preview_ready", { ...snapBase, createdAt: new Date(createdAt.getTime() + 15 * 60 * 1000), headline: `Action preview ready: Email ${tmpl.title}`, nextUserAction: "Review and approve the action to proceed.", primaryButton: "Approve & send", })); } if (["actions_awaiting_consent", "action_executing", "sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "actions_awaiting_consent", { ...snapBase, createdAt: new Date(createdAt.getTime() + 18 * 60 * 1000), headline: `Awaiting your consent to send the email.`, nextUserAction: "Grant consent to send the complaint email.", primaryButton: "Grant consent", })); } if (["action_executing", "sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "action_executing", { ...snapBase, createdAt: new Date(createdAt.getTime() + 20 * 60 * 1000), headline: "Sending your complaint email now…", })); } if (["sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "sent_by_email", { ...snapBase, createdAt: new Date(createdAt.getTime() + 21 * 60 * 1000), headline: `Action "${tmpl.title}" sent by email — waiting for confirmation.`, nextUserAction: "Confirm that you sent the email externally.", primaryButton: "I have submitted this", })); } if (["user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "user_confirmed_external_submission", { ...snapBase, createdAt: new Date(createdAt.getTime() + 22 * 60 * 1000), headline: `Action "${tmpl.title}" confirmed — waiting for response.`, nextUserAction: "Check for replies from the authority and report back.", nextSystemAction: "Follow up if no response within 7 days.", })); } if (["waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "waiting_for_response", { ...snapBase, createdAt: new Date(createdAt.getTime() + 23 * 60 * 1000), headline: `Email sent to authority — waiting for response.`, nextUserAction: "Check for replies from the authority and report back.", nextSystemAction: "Follow up if no response within 7 days.", })); } if (["resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "resolver_reply_received", { ...snapBase, createdAt: new Date(createdAt.getTime() + 2 * 24 * 60 * 60 * 1000), headline: `Reply received from the authority regarding "${tmpl.title}".`, nextUserAction: "Review the reply and update the mission status.", })); } if (["resolution_claimed", "resolved"].includes(tmpl.state)) { snapshots.push(makeSnapshot(missionId, "resolution_claimed", { ...snapBase, createdAt: new Date(createdAt.getTime() + 3 * 24 * 60 * 60 * 1000), headline: `Outcome recorded: Refund received — ₹${(tmpl.reliefRequested.amount as number) ?? 0}`, nextUserAction: "Thank you for updating. Your case contributes to the public ledger.", })); } if (tmpl.state === "resolved") { snapshots.push(makeSnapshot(missionId, "resolved", { ...snapBase, createdAt: new Date(createdAt.getTime() + 4 * 24 * 60 * 60 * 1000), headline: `Mission resolved ✓ — refund of ₹${(tmpl.reliefRequested.amount as number) ?? 0} received.`, nextUserAction: "View the outcome in the ledger.", primaryButton: "View result", })); } if (tmpl.state === "human_review_required") { snapshots.push(makeSnapshot(missionId, "human_review_required", { ...snapBase, createdAt: new Date(createdAt.getTime() + 5 * 60 * 1000), headline: "⚠️ This case has been flagged for urgent human review.", nextUserAction: "Please contact the emergency helpline: 112 (Police), 1930 (Cyber Crime)", nextSystemAction: "Awaiting human moderator assignment.", primaryButton: "View details", danger: true, confidence: 0.85, })); } await DB.insert(s.missionStatusSnapshots).values(snapshots); // ── Conversation + Messages ── const convId = randomUUID(); const convMsgId = randomUUID(); await DB.insert(s.conversations).values({ id: convId, userId: USER_ID, channel: "browser", activeMissionId: missionId, lastMessageAt: new Date(createdAt), }); await DB.insert(s.messages).values([ { id: convMsgId, conversationId: convId, missionId, direction: "inbound", channel: "browser", body: tmpl.description, createdAt: new Date(createdAt), }, { id: randomUUID(), conversationId: convId, missionId, direction: "outbound", channel: "browser", body: `Thank you for reporting. I'm analyzing your case regarding ${tmpl.title.toLowerCase()}.`, createdAt: new Date(createdAt.getTime() + 1000), }, ]); // ── Route Plan ── let rpId: string | null = null; if (tmpl.routePlan) { rpId = randomUUID(); await DB.insert(s.routePlans).values({ id: rpId, missionId, summary: `Route plan for ${tmpl.title}`, steps: makeRouteSteps(tmpl.domain, tmpl.title), citations: makeRouteCitations(), riskNotes: [{ level: "low", note: "Standard consumer complaint process" }], expectedTimeline: { minDays: 7, maxDays: 30 }, confidence: "0.9", createdAt: new Date(createdAt.getTime() + 12 * 60 * 1000), }); } // ── Actions ── if (tmpl.actions && rpId) { const actionId = randomUUID(); const inputPayload = { actionType: "send_email", destination: tmpl.opposingParty.handles && (tmpl.opposingParty.handles as Record).email ? (tmpl.opposingParty.handles as Record).email : "authority@example.gov.in", public: false, body: `Dear Sir/Madam,\n\nI am writing to report: ${tmpl.description}\n\nI request immediate resolution.\n\nSincerely,\n[User]`, subject: `Complaint: ${tmpl.title}`, attachments: [], piiDisclosed: [], risks: [], }; const payloadHash = hash(JSON.stringify(inputPayload)); // Build action status and proofState based on how far the mission progressed let actionStatus = "preview_ready"; let proofState: string = "draft_only"; let hasSubmission = false; let consentGrantId: string | null = null; if (["actions_awaiting_consent"].includes(tmpl.state)) { actionStatus = "awaiting_consent"; } else if (["action_executing", "sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { actionStatus = "executed"; consentGrantId = randomUUID(); if (["action_executing"].includes(tmpl.state)) { proofState = "sent_by_email"; } else if (["sent_by_email"].includes(tmpl.state)) { proofState = "sent_by_email"; } else { proofState = "user_confirmed_external_submission"; } hasSubmission = true; } const actionValues: Record = { id: actionId, missionId, routePlanId: rpId, actionType: "send_email", title: `Email ${tmpl.title}`, description: `Send formal complaint email regarding ${tmpl.title.toLowerCase()}`, inputPayload: inputPayload as any, canonicalPayloadHash: payloadHash, consentRequired: true, status: actionStatus, proofState: proofState === "draft_only" ? null : proofState, riskLevel: "low", createdBy: "user", createdAt: new Date(createdAt.getTime() + 15 * 60 * 1000), updatedAt: new Date(createdAt.getTime() + 15 * 60 * 1000), }; // Consent grant (must be inserted before action that references it) if (consentGrantId) { await DB.insert(s.consentGrants).values({ id: consentGrantId, missionId, userId: USER_ID, actionType: "send_email", actionPreviewHash: payloadHash, consentText: `I authorize sending the complaint regarding ${tmpl.title}`, channel: "web", grantedAt: new Date(createdAt.getTime() + 18 * 60 * 1000), }); actionValues.consentGrantId = consentGrantId; } await DB.insert(s.actions).values(actionValues as any); // Submissions if (hasSubmission) { await DB.insert(s.submissions).values({ id: randomUUID(), missionId, actionId, routePlanId: rpId, status: "completed", proofState: proofState!, payloadHash, externalReference: randomUUID(), sentAt: new Date(createdAt.getTime() + 21 * 60 * 1000), } as any); } // Action attempt (for executed actions) if (["action_executing", "sent_by_email", "user_confirmed_external_submission", "waiting_for_response", "resolver_reply_received", "resolution_claimed", "resolved"].includes(tmpl.state)) { await DB.insert(s.actionAttempts).values({ id: randomUUID(), actionId, attemptNo: 1, adapterName: "send_email", status: proofState === "sent_by_email" ? "completed" : "completed", startedAt: new Date(createdAt.getTime() + 20 * 60 * 1000), finishedAt: new Date(createdAt.getTime() + 21 * 60 * 1000), }); } } // ── Evidence ── if (tmpl.evidence) { const fileId = randomUUID(); await DB.insert(s.files).values({ id: fileId, missionId, uploaderUserId: USER_ID, storageKey: `demo/${missionId}/evidence.png`, originalFilename: "evidence.png", mimeType: "image/png", sha256: hash("demo-evidence"), sizeBytes: 50 * 1024, createdAt: new Date(createdAt), } as any); await DB.insert(s.evidenceItems).values({ id: randomUUID(), missionId, fileId, uploaderUserId: USER_ID, type: "screenshot", sourceChannel: "browser", piiLevel: "low", createdAt: new Date(createdAt), }); } // ── Resolution Outcome ── if (tmpl.resolution) { const outcomeId = randomUUID(); const amount = (tmpl.reliefRequested.amount as number) ?? 0; await DB.insert(s.resolutionOutcomes).values({ id: outcomeId, missionId, outcomeType: amount > 0 ? "refund_received" : "complaint_acknowledged", amountRecovered: String(amount), userSatisfaction: 4, anonymizedPublicSummary: `Consumer reported issue with ${tmpl.opposingParty.name as string}. After escalation through CourtMitra, the complaint was resolved with ₹${amount} recovered.`, publishConsent: true, createdAt: new Date(createdAt.getTime() + 4 * 24 * 60 * 60 * 1000), }); } // ── Public Case Page ── if (tmpl.publicPage) { const slug = `case-${Math.random().toString(36).slice(2, 10)}`; await DB.insert(s.publicCasePages).values({ id: randomUUID(), missionId, slug, visibility: "private_link", redactedSummary: `Consumer reported issue with ${(tmpl.opposingParty.name as string) ?? "service provider"}. Case resolved through CourtMitra platform with refund of ₹${(tmpl.reliefRequested.amount as number) ?? 0}.`, createdAt: new Date(createdAt.getTime() + 4 * 24 * 60 * 60 * 1000), }); } // ── Swarm ── if (tmpl.swarm) { // Check if entity already exists const entityName = (tmpl.opposingParty.name as string) ?? "Unknown"; const existingEntities = await DB.select({ id: s.entities.id }) .from(s.entities) .where(and(eq(s.entities.normalizedName, entityName), eq(s.entities.domain, tmpl.domain))); let entityId: string; if (existingEntities.length > 0) { entityId = existingEntities[0]!.id; } else { entityId = randomUUID(); await DB.insert(s.entities).values({ id: entityId, normalizedName: entityName, domain: tmpl.domain, aliases: [entityName], identifiers: { type: "company", jurisdiction: "India" } as any, }); } // Check if swarm exists const existingSwarms = await DB.select({ id: s.swarms.id }) .from(s.swarms) .where(and(eq(s.swarms.entityId, entityId), eq(s.swarms.issueDomain, tmpl.domain))); let swarmId: string; if (existingSwarms.length > 0) { swarmId = existingSwarms[0]!.id; await DB.update(s.swarms) .set({ caseCount: sql`${s.swarms.caseCount} + 1`, updatedAt: new Date() }) .where(eq(s.swarms.id, swarmId)); } else { swarmId = randomUUID(); await DB.insert(s.swarms).values({ id: swarmId, entityId, issueDomain: tmpl.domain, patternSignature: { complaintType: "consumer_rights" } as any, caseCount: 1, status: "active", }); } } // ── Takedown ── if (tmpl.takedown) { const actionRows = await DB.select({ id: s.actions.id }) .from(s.actions) .where(eq(s.actions.missionId, missionId)) .limit(1); // Create a minimal action if none exists (emergency mission) let actionId: string; if (actionRows.length === 0) { actionId = randomUUID(); await DB.insert(s.actions).values({ id: actionId, missionId, actionType: "send_email", title: `Report: ${tmpl.title}`, status: "proposed", riskLevel: "high", createdBy: "user", } as any); } else { actionId = actionRows[0]!.id; } await DB.insert(s.takedownRequests).values({ id: randomUUID(), missionId, actionId, intermediaryName: "Meta / Instagram", grievanceOfficerName: "Grievance Officer", grievanceOfficerEmail: "grievance-officer-in@meta.com", contentUrl: "https://instagram.com/offending-profile", legalGrounds: "Defamatory content / impersonation under IT Act 2000 Section 66D", status: "pending", complianceDeadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }); } // ── Case Lookup ── if (tmpl.caseLookup) { await DB.insert(s.caseLookups).values({ missionId, cnrNumber: `CNR-${tmpl.domain.toUpperCase()}-${Math.random().toString(36).slice(2, 8).toUpperCase()}`, caseNumber: `CASE/${Math.floor(Math.random() * 10000)}/2026`, courtName: "District Consumer Disputes Redressal Forum", petitioner: "Anonymous Consumer", respondent: tmpl.opposingParty.name as string, caseStatus: "pending", source: "eCourt", lookupStatus: "completed", lastOrderSummary: `Notice issued to ${tmpl.opposingParty.name as string}. Next hearing in 30 days.`, nextHearingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split("T")[0], lastOrderDate: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString().split("T")[0], }); } // ── Followup ── if (tmpl.followup) { await DB.insert(s.followups).values({ id: randomUUID(), missionId, type: "authority_followup", dueAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), status: "scheduled", payload: { note: "Automatic follow-up if no response received" } as any, }); } console.log(` ✓ "${tmpl.title.slice(0, 60)}…" → ${tmpl.state} (${tmpl.domain})`); } // ── Step 3: Refresh materialized views ── console.log("\n[demo] Refreshing materialized views…"); try { await DB.execute(sql`REFRESH MATERIALIZED VIEW CONCURRENTLY ledger_stats_view`); console.log(" ✓ ledger_stats_view refreshed"); } catch { await DB.execute(sql`REFRESH MATERIALIZED VIEW ledger_stats_view`); console.log(" ✓ ledger_stats_view refreshed (non-concurrent)"); } try { await DB.execute(sql`REFRESH MATERIALIZED VIEW CONCURRENTLY repeat_offender_view`); console.log(" ✓ repeat_offender_view refreshed"); } catch { await DB.execute(sql`REFRESH MATERIALIZED VIEW repeat_offender_view`); console.log(" ✓ repeat_offender_view refreshed (non-concurrent)"); } // ── Summary ── const missionCount = await DB.select({ count: sql`count(*)::int` }).from(s.missions); const entitiesCount = await DB.select({ count: sql`count(*)::int` }).from(s.entities); const swarmsCount = await DB.select({ count: sql`count(*)::int` }).from(s.swarms); const takedownsCount = await DB.select({ count: sql`count(*)::int` }).from(s.takedownRequests); const publicPagesCount = await DB.select({ count: sql`count(*)::int` }).from(s.publicCasePages); const ledgersCount = await DB.select({ count: sql`count(*)::int` }).from(s.resolutionOutcomes); console.log("\n[demo] ── Summary ──"); console.log(` Missions: ${missionCount[0]!.count}`); console.log(` Entities/Swarms: ${entitiesCount[0]!.count} entities, ${swarmsCount[0]!.count} swarms`); console.log(` Takedown Reqs: ${takedownsCount[0]!.count}`); console.log(` Public Pages: ${publicPagesCount[0]!.count}`); console.log(` Resolution Outcomes: ${ledgersCount[0]!.count}`); console.log("\n[demo] Done."); await closeDb(); } main().catch((err) => { console.error("[demo] FATAL:", err); process.exit(1); });