#!/usr/bin/env python3 """ Seed realistic, idempotent iPad investor-demo data into the current operator tenant. The script writes only canonical Velocity domains used by the iPad app: crm_*, intel_*, workflow_*, inventory_*, mobile-edge events, and calendar events. Rows are tagged with metadata_json/source identifiers so they are auditable and safe to re-run without duplication. """ from __future__ import annotations import argparse import asyncio import json import os import re import uuid from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any from dotenv import load_dotenv try: import asyncpg except ModuleNotFoundError: # pragma: no cover - exercised by operator environment asyncpg = None # type: ignore[assignment] SEED_SOURCE = "velocity_ipad_investor_demo_2026_04" DEFAULT_OPERATOR_EMAIL = "sayan@desineuron.in" NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL, "https://desineuron.in/project-velocity/ipad-investor-demo") DEMO_CLIENT_TARGET_COUNT = 50 def _load_env() -> None: repo_root = Path(__file__).resolve().parents[2] for candidate in (repo_root / "backend" / ".env", repo_root / ".env"): if candidate.exists(): load_dotenv(candidate, override=False) load_dotenv(override=False) def _stable_uuid(tenant_id: str, key: str) -> str: return str(uuid.uuid5(NAMESPACE, f"{tenant_id}:{key}")) def _json(value: Any) -> str: return json.dumps(value, separators=(",", ":"), ensure_ascii=True) def _db_kwargs() -> dict[str, Any]: if os.getenv("DATABASE_URL"): return {"dsn": os.environ["DATABASE_URL"]} required = ["VELOCITY_DB_NAME", "VELOCITY_DB_USER", "VELOCITY_DB_PASSWORD"] missing = [key for key in required if not os.getenv(key)] if missing: raise RuntimeError( "Missing database configuration: " + ", ".join(missing) + ". Set DATABASE_URL or VELOCITY_DB_* variables before running the seed." ) return { "host": os.getenv("VELOCITY_DB_HOST", "localhost"), "port": int(os.getenv("VELOCITY_DB_PORT", "5432")), "database": os.environ["VELOCITY_DB_NAME"], "user": os.environ["VELOCITY_DB_USER"], "password": os.environ["VELOCITY_DB_PASSWORD"], } def _expected_counts() -> dict[str, int]: return { "people": len(DEMO_CLIENTS), "leads": len(DEMO_CLIENTS), "projects": len(PROJECTS), "properties": len(PROJECTS), "property_media": len(PROJECTS) * 2, "interests": len(DEMO_CLIENTS), "opportunities": len(DEMO_CLIENTS), "scores": len(DEMO_CLIENTS) * 3, "interactions": len(DEMO_CLIENTS) * 3, "edge_events": len(DEMO_CLIENTS), "reminders": len(DEMO_CLIENTS), "calendar_events": len(DEMO_CLIENTS), "insight_recommendations": len(DEMO_CLIENTS), "comms_threads": len(DEMO_CLIENTS), "comms_messages": len(DEMO_CLIENTS) * 3, "comms_call_logs": len(DEMO_CLIENTS), "import_batches": 1, "import_proposals": 3, } async def _resolve_operator(conn: asyncpg.Connection, email: str, tenant_override: str | None) -> tuple[str, str | None]: if tenant_override: user_id = await conn.fetchval( "SELECT id::text FROM users_and_roles WHERE email = $1 AND tenant_id = $2 LIMIT 1", email, tenant_override, ) return tenant_override, user_id row = await conn.fetchrow( """ SELECT id::text AS user_id, tenant_id FROM users_and_roles WHERE email = $1 AND is_active = TRUE ORDER BY last_login DESC NULLS LAST, created_at DESC LIMIT 1 """, email, ) if row: return row["tenant_id"], row["user_id"] tenant_id = os.getenv("VELOCITY_DEFAULT_TENANT_ID", "tenant_velocity") return tenant_id, None DEMO_CLIENTS = [ { "key": "meera-sen", "name": "Meera Sen", "email": "meera.sen.investor@demo.desineuron.in", "phone": "+91-98745-11820", "buyer_type": "hni_end_user", "city": "Kolkata", "nationality": "Indian", "persona": ["family_upgrader", "waterfront_preference"], "budget": "12-18 Cr", "urgency": "high", "status": "qualified", "project": "Atri Aqua Sky Residences", "configuration": "4BHK Sky Villa", "unit": "Tower A / High floor", "budget_min": 120000000, "budget_max": 180000000, "stage": "proposal", "value": 154000000, "probability": 72, "next_action": "Share revised payment schedule and club-deck walkthrough slots.", "scores": (0.91, 0.86, 0.88), }, { "key": "arjun-malhotra", "name": "Arjun Malhotra", "email": "arjun.malhotra.nri@demo.desineuron.in", "phone": "+91-98102-44591", "buyer_type": "nri_investor", "city": "Dubai", "nationality": "Indian", "persona": ["nri_portfolio_buyer", "rental_yield_focused"], "budget": "8-12 Cr", "urgency": "medium", "status": "site_visit_scheduled", "project": "Alipore Azure Residences", "configuration": "3BHK Signature", "unit": "Tower B / 21st floor", "budget_min": 80000000, "budget_max": 120000000, "stage": "site_visit", "value": 98000000, "probability": 61, "next_action": "Coordinate Saturday family video walkthrough and NRI remittance checklist.", "scores": (0.82, 0.79, 0.63), }, { "key": "devika-roy", "name": "Devika Roy", "email": "devika.roy.familyoffice@demo.desineuron.in", "phone": "+91-99033-28741", "buyer_type": "family_office", "city": "Mumbai", "nationality": "Indian", "persona": ["portfolio_allocator", "low_visibility_buyer"], "budget": "20-30 Cr", "urgency": "critical", "status": "negotiation", "project": "Victoria Gardens Private Residences", "configuration": "Penthouse Duplex", "unit": "Private elevator stack", "budget_min": 200000000, "budget_max": 300000000, "stage": "negotiation", "value": 265000000, "probability": 78, "next_action": "Prepare founder-level commercial note before family office review.", "scores": (0.95, 0.91, 0.96), }, { "key": "rohan-kapoor", "name": "Rohan Kapoor", "email": "rohan.kapoor.startup@demo.desineuron.in", "phone": "+91-98311-73654", "buyer_type": "founder_buyer", "city": "Bengaluru", "nationality": "Indian", "persona": ["founder_liquidity_event", "design_led_buyer"], "budget": "6-10 Cr", "urgency": "high", "status": "contacted", "project": "Salt Lake Atelier Homes", "configuration": "3.5BHK Garden Home", "unit": "Podium garden facing", "budget_min": 60000000, "budget_max": 100000000, "stage": "qualified", "value": 83500000, "probability": 54, "next_action": "Send design moodboard and schedule Dream Weaver room concept.", "scores": (0.76, 0.83, 0.81), }, { "key": "saira-hussain", "name": "Saira Hussain", "email": "saira.hussain.doctor@demo.desineuron.in", "phone": "+91-97482-66019", "buyer_type": "end_user", "city": "Kolkata", "nationality": "Indian", "persona": ["quiet_luxury", "school_proximity"], "budget": "4-6 Cr", "urgency": "medium", "status": "site_visited", "project": "Ballygunge Meridian", "configuration": "3BHK", "unit": "South-east corner", "budget_min": 40000000, "budget_max": 60000000, "stage": "proposal", "value": 52000000, "probability": 66, "next_action": "Share school-route comparison and revised parking availability.", "scores": (0.74, 0.68, 0.62), }, { "key": "vikram-jalan", "name": "Vikram Jalan", "email": "vikram.jalan.broker@demo.desineuron.in", "phone": "+91-98300-91274", "buyer_type": "broker_referral", "city": "Kolkata", "nationality": "Indian", "persona": ["broker_network", "bulk_referral_potential"], "budget": "15-25 Cr", "urgency": "high", "status": "booking_initiated", "project": "Atri Aqua Sky Residences", "configuration": "4BHK River Deck", "unit": "Two adjacent units", "budget_min": 150000000, "budget_max": 250000000, "stage": "booking", "value": 212000000, "probability": 84, "next_action": "Confirm booking amount routing and broker mandate documentation.", "scores": (0.88, 0.77, 0.89), }, ] PROJECTS = { "Atri Aqua Sky Residences": { "developer": "Atri Group", "micro_market": "Batanagar Riverside", "address": "Maheshtala Riverside Corridor, Kolkata", "property_type": "apartment", "location": {"city": "Kolkata", "district": "Maheshtala", "lat": 22.4981, "lng": 88.2291}, "price_bands": [ {"unitType": "3BHK", "minINR": 72000000, "maxINR": 98000000}, {"unitType": "4BHK Sky Villa", "minINR": 130000000, "maxINR": 190000000}, ], "unit_mix": [{"bedrooms": 3, "count": 42, "sizeSqft": 2450}, {"bedrooms": 4, "count": 18, "sizeSqft": 3800}], }, "Alipore Azure Residences": { "developer": "Meridian Urban Estates", "micro_market": "Alipore", "address": "Judges Court Road, Alipore, Kolkata", "property_type": "apartment", "location": {"city": "Kolkata", "district": "Alipore", "lat": 22.5288, "lng": 88.3309}, "price_bands": [{"unitType": "3BHK Signature", "minINR": 85000000, "maxINR": 125000000}], "unit_mix": [{"bedrooms": 3, "count": 36, "sizeSqft": 2850}, {"bedrooms": 4, "count": 16, "sizeSqft": 4100}], }, "Victoria Gardens Private Residences": { "developer": "Heritage Habitat", "micro_market": "Maidan", "address": "Queen's Way Precinct, Kolkata", "property_type": "penthouse", "location": {"city": "Kolkata", "district": "Maidan", "lat": 22.5448, "lng": 88.3426}, "price_bands": [{"unitType": "Penthouse Duplex", "minINR": 220000000, "maxINR": 320000000}], "unit_mix": [{"bedrooms": 5, "count": 8, "sizeSqft": 6200}], }, "Salt Lake Atelier Homes": { "developer": "Studio Habitat", "micro_market": "Salt Lake Sector V", "address": "EM Bypass Connector, Salt Lake, Kolkata", "property_type": "apartment", "location": {"city": "Kolkata", "district": "Salt Lake", "lat": 22.5797, "lng": 88.4353}, "price_bands": [{"unitType": "3.5BHK Garden Home", "minINR": 68000000, "maxINR": 98000000}], "unit_mix": [{"bedrooms": 3, "count": 28, "sizeSqft": 2350}], }, "Ballygunge Meridian": { "developer": "Eastern Crest Realty", "micro_market": "Ballygunge", "address": "Ballygunge Circular Road, Kolkata", "property_type": "apartment", "location": {"city": "Kolkata", "district": "Ballygunge", "lat": 22.5276, "lng": 88.3651}, "price_bands": [{"unitType": "3BHK", "minINR": 42000000, "maxINR": 62000000}], "unit_mix": [{"bedrooms": 3, "count": 54, "sizeSqft": 1780}], }, } PROJECTS.update( { "Emaar Palm Heights Dubai": { "developer": "Emaar Properties", "micro_market": "Dubai Hills Estate", "address": "Dubai Hills Estate, Dubai, UAE", "property_type": "penthouse", "location": {"city": "Dubai", "district": "Dubai Hills", "lat": 25.1132, "lng": 55.2477}, "price_bands": [ {"unitType": "4BR Sky Penthouse", "minUSD": 3200000, "maxUSD": 5200000}, {"unitType": "3BR Residence", "minUSD": 1800000, "maxUSD": 2600000}, ], "unit_mix": [{"bedrooms": 4, "count": 16, "sizeSqft": 5200}, {"bedrooms": 3, "count": 42, "sizeSqft": 3100}], }, "Sobha Hartland Estates": { "developer": "Sobha Realty", "micro_market": "Mohammed Bin Rashid City", "address": "Sobha Hartland, MBR City, Dubai, UAE", "property_type": "villa", "location": {"city": "Dubai", "district": "MBR City", "lat": 25.1763, "lng": 55.3067}, "price_bands": [ {"unitType": "5BR Waterfront Villa", "minUSD": 4500000, "maxUSD": 7800000}, {"unitType": "4BR Garden Villa", "minUSD": 2900000, "maxUSD": 4200000}, ], "unit_mix": [{"bedrooms": 5, "count": 12, "sizeSqft": 8100}, {"bedrooms": 4, "count": 24, "sizeSqft": 5900}], }, "Emaar Urban Oasis Gurugram": { "developer": "Emaar India", "micro_market": "Golf Course Extension", "address": "Sector 62, Gurugram, India", "property_type": "apartment", "location": {"city": "Gurugram", "district": "Golf Course Extension", "lat": 28.4059, "lng": 77.0964}, "price_bands": [ {"unitType": "4BHK Signature", "minINR": 95000000, "maxINR": 155000000}, {"unitType": "Penthouse", "minINR": 180000000, "maxINR": 290000000}, ], "unit_mix": [{"bedrooms": 4, "count": 44, "sizeSqft": 4200}, {"bedrooms": 5, "count": 10, "sizeSqft": 6200}], }, "Sobha Neopolis Presidential": { "developer": "Sobha Limited", "micro_market": "Panathur", "address": "Panathur Main Road, Bengaluru, India", "property_type": "apartment", "location": {"city": "Bengaluru", "district": "Panathur", "lat": 12.9357, "lng": 77.7037}, "price_bands": [ {"unitType": "4BHK Presidential", "minINR": 85000000, "maxINR": 140000000}, {"unitType": "3BHK Luxury", "minINR": 42000000, "maxINR": 68000000}, ], "unit_mix": [{"bedrooms": 4, "count": 30, "sizeSqft": 3600}, {"bedrooms": 3, "count": 72, "sizeSqft": 2400}], }, "Emaar Ocean Crown Abu Dhabi": { "developer": "Emaar Properties", "micro_market": "Saadiyat Island", "address": "Saadiyat Cultural District, Abu Dhabi, UAE", "property_type": "branded_residence", "location": {"city": "Abu Dhabi", "district": "Saadiyat Island", "lat": 24.5442, "lng": 54.4332}, "price_bands": [ {"unitType": "Royal Beachfront Penthouse", "minUSD": 6200000, "maxUSD": 11000000}, {"unitType": "3BR Beach Residence", "minUSD": 2400000, "maxUSD": 3900000}, ], "unit_mix": [{"bedrooms": 5, "count": 6, "sizeSqft": 9200}, {"bedrooms": 3, "count": 28, "sizeSqft": 3400}], }, } ) PROPERTY_MEDIA_URLS = [ "https://images.unsplash.com/photo-1600585154340-be6161a56a0c", "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c", "https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3", "https://images.unsplash.com/photo-1600585154526-990dced4db0d", "https://images.unsplash.com/photo-1613490493576-7fde63acd811", "https://images.unsplash.com/photo-1600607687644-aac4c3eac7f4", "https://images.unsplash.com/photo-1600566752355-35792bedcfea", "https://images.unsplash.com/photo-1600607687920-4e2a09cf159d", ] def _media_url(seed_index: int, *, width: int, height: int) -> str: base = PROPERTY_MEDIA_URLS[seed_index % len(PROPERTY_MEDIA_URLS)] return f"{base}?auto=format&fit=crop&w={width}&h={height}&q=82" def _project_media_assets(project_name: str, project: dict[str, Any], index: int) -> list[dict[str, Any]]: slug = re.sub(r"[^a-z0-9]+", "-", project_name.lower()).strip("-") location = project["location"] return [ { "key": f"{slug}:hero", "media_type": "image", "url": _media_url(index, width=1600, height=1000), "thumbnail_url": _media_url(index, width=640, height=420), "sort_order": 0, "metadata": { "seed_source": SEED_SOURCE, "demo_asset_kind": "property_hero", "project_name": project_name, "developer": project["developer"], "city": location["city"], "district": location["district"], }, }, { "key": f"{slug}:floorplan", "media_type": "floorplan", "url": _media_url(index + 3, width=1600, height=1000), "thumbnail_url": _media_url(index + 3, width=640, height=420), "sort_order": 1, "metadata": { "seed_source": SEED_SOURCE, "demo_asset_kind": "floor_plan_reference", "project_name": project_name, "unit_mix": project["unit_mix"], }, }, ] def _phone_for(index: int, country: str) -> str: if country == "UAE": return f"+971-50-{7200000 + index:07d}" return f"+91-98{30000000 + index:08d}"[:14] def _expand_demo_clients(seed_clients: list[dict[str, Any]], target_count: int) -> list[dict[str, Any]]: if len(seed_clients) >= target_count: return seed_clients first_names = [ "Aarav", "Ishaan", "Kabir", "Reyansh", "Vihaan", "Anika", "Aanya", "Kiara", "Navya", "Rhea", "Omar", "Mariam", "Zayed", "Leila", "Farah", "Aditya", "Naina", "Rohan", "Tara", "Samaira", "Armaan", "Dev", "Saanvi", "Ayesha", ] last_names = [ "Mehta", "Kapoor", "Khanna", "Bhatia", "Sarin", "Al Maktoum", "Al Futtaim", "Al Habtoor", "Nair", "Menon", "Reddy", "Pillai", "Chopra", "Raheja", "Jindal", "Goenka", "Dalmia", "Merchant", "Saxena", "Bose", ] buyer_types = ["hni_end_user", "nri_investor", "family_office", "founder_buyer", "investor"] statuses = ["qualified", "site_visit_scheduled", "site_visited", "negotiation", "booking_initiated", "contacted"] stages = ["qualified", "proposal", "site_visit", "negotiation", "booking"] urgencies = ["medium", "high", "critical"] project_names = list(PROJECTS.keys()) expanded = list(seed_clients) for index in range(len(seed_clients), target_count): first = first_names[index % len(first_names)] last = last_names[(index * 3) % len(last_names)] project_name = project_names[index % len(project_names)] project = PROJECTS[project_name] city = project["location"]["city"] is_uae = city in {"Dubai", "Abu Dhabi"} country = "UAE" if is_uae else "India" buyer_type = buyer_types[index % len(buyer_types)] urgency = urgencies[index % len(urgencies)] high_value_usd = 1_150_000 + (index % 9) * 425_000 value = high_value_usd if is_uae else 82_000_000 + (index % 11) * 17_500_000 budget_label = f"${high_value_usd / 1_000_000:.1f}-${(high_value_usd + 950_000) / 1_000_000:.1f}M" if is_uae else f"{int(value / 10_000_000)}-{int(value / 10_000_000) + 4} Cr" configuration = project["unit_mix"][0]["bedrooms"] unit_label = "Royal penthouse stack" if value >= (4_000_000 if is_uae else 180_000_000) else "High-floor signature residence" full_name = f"{first} {last}" key = re.sub(r"[^a-z0-9]+", "-", full_name.lower()).strip("-") + f"-{index:02d}" expanded.append( { "key": key, "name": full_name, "email": f"{key}@demo.desineuron.in", "phone": _phone_for(index, country), "buyer_type": buyer_type, "city": city, "nationality": "Emirati" if is_uae and index % 3 == 0 else "Indian", "persona": [buyer_type, "tier_1_developer_pitch", "high_value_pipeline"], "budget": budget_label, "urgency": urgency, "status": statuses[index % len(statuses)], "project": project_name, "configuration": f"{configuration}BR Signature Residence", "unit": unit_label, "budget_min": int(value * 0.88), "budget_max": int(value * 1.22), "stage": stages[index % len(stages)], "value": value, "probability": min(92, 48 + (index * 7) % 43), "next_action": f"Prepare developer-grade commercial deck for {project_name} and schedule executive walkthrough.", "scores": ( round(0.68 + ((index * 7) % 28) / 100, 2), round(0.64 + ((index * 5) % 30) / 100, 2), round(0.62 + ((index * 11) % 35) / 100, 2), ), } ) return expanded DEMO_CLIENTS = _expand_demo_clients(DEMO_CLIENTS, DEMO_CLIENT_TARGET_COUNT) async def seed(conn: asyncpg.Connection, tenant_id: str, operator_user_id: str | None, dry_run: bool = False) -> dict[str, int]: now = datetime.now(timezone.utc) owner_user_ref = operator_user_id or f"{SEED_SOURCE}:operator" counts = { "people": 0, "leads": 0, "projects": 0, "properties": 0, "property_media": 0, "interests": 0, "opportunities": 0, "scores": 0, "interactions": 0, "edge_events": 0, "reminders": 0, "calendar_events": 0, "insight_recommendations": 0, "comms_threads": 0, "comms_messages": 0, "comms_call_logs": 0, "import_batches": 0, "import_proposals": 0, } if dry_run: return counts project_ids: dict[str, str] = {} for project_index, (name, project) in enumerate(PROJECTS.items()): project_id = _stable_uuid(tenant_id, f"project:{name}") project_ids[name] = project_id await conn.execute( """ INSERT INTO inventory_projects ( project_id, project_name, developer_name, city, micro_market, address, total_units, project_status, location_json, amenities_json, metadata_json ) VALUES ( $1::uuid, $2, $3, $4, $5, $6, $7, 'active', $8::jsonb, $9::jsonb, $10::jsonb ) ON CONFLICT (project_name) DO UPDATE SET developer_name = EXCLUDED.developer_name, micro_market = EXCLUDED.micro_market, address = EXCLUDED.address, total_units = EXCLUDED.total_units, project_status = EXCLUDED.project_status, location_json = EXCLUDED.location_json, amenities_json = EXCLUDED.amenities_json, metadata_json = inventory_projects.metadata_json || EXCLUDED.metadata_json, updated_at = NOW() """, project_id, name, project["developer"], project["location"]["city"], project["micro_market"], project["address"], sum(item["count"] for item in project["unit_mix"]), _json(project["location"]), _json(["concierge", "private club", "fitness studio", "visitor lounge", "ev charging"]), _json({"seed_source": SEED_SOURCE, "ipad_demo": True}), ) counts["projects"] += 1 property_id = _stable_uuid(tenant_id, f"inventory-property:{name}") await conn.execute( """ INSERT INTO inventory_properties ( property_id, tenant_id, source_id, project_name, developer_name, location, property_type, price_bands, unit_mix, amenities, status, validation_state, ingested_at, created_at, updated_at ) VALUES ( $1::uuid, $2, $3, $4, $5, $6::jsonb, $7, $8::jsonb, $9::jsonb, $10, 'active', $11::jsonb, NOW(), NOW(), NOW() ) ON CONFLICT (property_id) DO UPDATE SET project_name = EXCLUDED.project_name, developer_name = EXCLUDED.developer_name, location = EXCLUDED.location, property_type = EXCLUDED.property_type, price_bands = EXCLUDED.price_bands, unit_mix = EXCLUDED.unit_mix, amenities = EXCLUDED.amenities, status = 'active', validation_state = EXCLUDED.validation_state, updated_at = NOW() """, property_id, tenant_id, f"{SEED_SOURCE}:{name}", name, project["developer"], _json(project["location"]), project["property_type"], _json(project["price_bands"]), _json(project["unit_mix"]), ["concierge", "clubhouse", "security", "ev charging", "landscaped deck"], _json({"seed_source": SEED_SOURCE, "validated_for_ipad_demo": True}), ) counts["properties"] += 1 for media in _project_media_assets(name, project, project_index): media_asset_id = _stable_uuid(tenant_id, f"inventory-media:{media['key']}") await conn.execute( """ INSERT INTO inventory_media_assets ( media_asset_id, property_id, tenant_id, media_type, url, thumbnail_url, sort_order, metadata, uploaded_by, created_at ) VALUES ( $1::uuid, $2::uuid, $3, $4, $5, $6, $7, $8::jsonb, $9, NOW() ) ON CONFLICT (media_asset_id) DO UPDATE SET media_type = EXCLUDED.media_type, url = EXCLUDED.url, thumbnail_url = EXCLUDED.thumbnail_url, sort_order = EXCLUDED.sort_order, metadata = inventory_media_assets.metadata || EXCLUDED.metadata, uploaded_by = EXCLUDED.uploaded_by """, media_asset_id, property_id, tenant_id, media["media_type"], media["url"], media["thumbnail_url"], media["sort_order"], _json(media["metadata"]), owner_user_ref, ) counts["property_media"] += 1 for index, client in enumerate(DEMO_CLIENTS): person_id = _stable_uuid(tenant_id, f"person:{client['key']}") lead_id = _stable_uuid(tenant_id, f"lead:{client['key']}") opportunity_id = _stable_uuid(tenant_id, f"opportunity:{client['key']}") project_id = project_ids[client["project"]] await conn.execute( """ INSERT INTO crm_people ( person_id, tenant_id, full_name, primary_email, primary_phone, city, nationality, buyer_type, persona_labels, source_confidence, metadata_json, created_at, updated_at ) VALUES ( $1::uuid, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, 0.98, $10::jsonb, NOW() - ($11::int * INTERVAL '2 days'), NOW() ) ON CONFLICT (person_id) DO UPDATE SET full_name = EXCLUDED.full_name, primary_email = EXCLUDED.primary_email, primary_phone = EXCLUDED.primary_phone, city = EXCLUDED.city, nationality = EXCLUDED.nationality, buyer_type = EXCLUDED.buyer_type, persona_labels = EXCLUDED.persona_labels, source_confidence = EXCLUDED.source_confidence, metadata_json = crm_people.metadata_json || EXCLUDED.metadata_json, updated_at = NOW() """, person_id, tenant_id, client["name"], client["email"], client["phone"], client["city"], client["nationality"], client["buyer_type"], _json(client["persona"]), _json({"seed_source": SEED_SOURCE, "investor_demo": True, "source_note": "iPad production readiness seed"}), index, ) counts["people"] += 1 await conn.execute( """ INSERT INTO crm_leads ( lead_id, tenant_id, person_id, source_system, status, budget_band, urgency, financing_posture, timeline_to_decision, objections, motivations, assigned_user_id, metadata_json, created_at, updated_at ) VALUES ( $1::uuid, $2, $3::uuid, 'ipad_investor_demo', $4::crm_lead_status, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::uuid, $12::jsonb, NOW() - ($13::int * INTERVAL '2 days'), NOW() ) ON CONFLICT (lead_id) DO UPDATE SET status = EXCLUDED.status, budget_band = EXCLUDED.budget_band, urgency = EXCLUDED.urgency, financing_posture = EXCLUDED.financing_posture, timeline_to_decision = EXCLUDED.timeline_to_decision, objections = EXCLUDED.objections, motivations = EXCLUDED.motivations, assigned_user_id = EXCLUDED.assigned_user_id, metadata_json = crm_leads.metadata_json || EXCLUDED.metadata_json, updated_at = NOW() """, lead_id, tenant_id, person_id, client["status"], client["budget"], client["urgency"], "cash_and_structured_payment" if client["budget_min"] >= 100000000 else "bank_loan_preapproved", "30_days" if client["urgency"] in {"high", "critical"} else "60_to_90_days", _json(["needs_family_alignment"] if client["urgency"] != "critical" else ["price_protection", "privacy"]), _json(["upgrade_primary_home", "wealth_preservation", "status_address"]), operator_user_id, _json({"seed_source": SEED_SOURCE, "investor_demo": True}), index, ) counts["leads"] += 1 await conn.execute( """ INSERT INTO crm_property_interests ( interest_id, tenant_id, person_id, lead_id, project_id, project_name, unit_preference, configuration, budget_min, budget_max, priority, notes, created_at ) VALUES ( $1::uuid, $2, $3::uuid, $4::uuid, $5::uuid, $6, $7, $8, $9, $10, 1, $11, NOW() ) ON CONFLICT (interest_id) DO UPDATE SET project_name = EXCLUDED.project_name, unit_preference = EXCLUDED.unit_preference, configuration = EXCLUDED.configuration, budget_min = EXCLUDED.budget_min, budget_max = EXCLUDED.budget_max, notes = EXCLUDED.notes """, _stable_uuid(tenant_id, f"interest:{client['key']}"), tenant_id, person_id, lead_id, project_id, client["project"], client["unit"], client["configuration"], client["budget_min"], client["budget_max"], f"Seeded for iPad investor demo by {SEED_SOURCE}.", ) counts["interests"] += 1 await conn.execute( """ INSERT INTO crm_opportunities ( opportunity_id, tenant_id, lead_id, project_id, stage, value, probability, expected_close_date, next_action, notes, metadata_json, created_at, updated_at ) VALUES ( $1::uuid, $2, $3::uuid, $4::uuid, $5::crm_opportunity_stage, $6, $7, $8::date, $9, $10, $11::jsonb, NOW() - ($12::int * INTERVAL '1 day'), NOW() ) ON CONFLICT (opportunity_id) DO UPDATE SET project_id = EXCLUDED.project_id, stage = EXCLUDED.stage, value = EXCLUDED.value, probability = EXCLUDED.probability, expected_close_date = EXCLUDED.expected_close_date, next_action = EXCLUDED.next_action, notes = EXCLUDED.notes, metadata_json = crm_opportunities.metadata_json || EXCLUDED.metadata_json, updated_at = NOW() """, opportunity_id, tenant_id, lead_id, project_id, client["stage"], client["value"], client["probability"], (now + timedelta(days=21 + index * 4)).date().isoformat(), client["next_action"], "Investor-demo opportunity with realistic project, value, and next-step context.", _json({"seed_source": SEED_SOURCE, "investor_demo": True}), index, ) counts["opportunities"] += 1 for score_type, value in zip(("intent_score", "engagement_score", "urgency_score"), client["scores"]): await conn.execute( """ INSERT INTO intel_qd_scores ( qd_id, tenant_id, person_id, score_type, current_value, computed_at, evidence_refs_json, reasoning, metadata_json ) VALUES ( $1::uuid, $2, $3::uuid, $4, $5, NOW(), $6::jsonb, $7, $8::jsonb ) ON CONFLICT (person_id, score_type) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, current_value = EXCLUDED.current_value, computed_at = NOW(), evidence_refs_json = EXCLUDED.evidence_refs_json, reasoning = EXCLUDED.reasoning, metadata_json = intel_qd_scores.metadata_json || EXCLUDED.metadata_json """, _stable_uuid(tenant_id, f"qd:{client['key']}:{score_type}"), tenant_id, person_id, score_type, value, _json([f"seed:{client['key']}:interaction"]), f"{client['name']} shows {score_type.replace('_', ' ')} from recent budget, project, and follow-up signals.", _json({"seed_source": SEED_SOURCE, "investor_demo": True}), ) counts["scores"] += 1 interaction_templates = [ ("whatsapp", "message", now - timedelta(hours=6 + index), "Confirmed budget band and asked for a project comparison deck."), ("phone", "call", now - timedelta(days=1, hours=index), "Discussed decision timeline, family alignment, and next visit window."), ("site_visit", "visit", now - timedelta(days=3 + index), f"Reviewed {client['project']} and shortlisted {client['configuration']}."), ] for event_index, (channel, interaction_type, happened_at, summary) in enumerate(interaction_templates): interaction_id = _stable_uuid(tenant_id, f"interaction:{client['key']}:{event_index}") await conn.execute( """ INSERT INTO intel_interactions ( interaction_id, tenant_id, person_id, lead_id, channel, interaction_type, happened_at, summary, source_ref, metadata_json, created_at ) VALUES ( $1::uuid, $2, $3::uuid, $4::uuid, $5::intel_channel, $6, $7, $8, $9, $10::jsonb, NOW() ) ON CONFLICT (interaction_id) DO UPDATE SET happened_at = EXCLUDED.happened_at, summary = EXCLUDED.summary, metadata_json = intel_interactions.metadata_json || EXCLUDED.metadata_json """, interaction_id, tenant_id, person_id, lead_id, channel, interaction_type, happened_at, summary, f"{SEED_SOURCE}:{client['key']}:{event_index}", _json({"seed_source": SEED_SOURCE, "investor_demo": True}), ) counts["interactions"] += 1 edge_event_id = _stable_uuid(tenant_id, f"edge-event:{client['key']}") await conn.execute( """ INSERT INTO edge_communication_events ( event_id, tenant_id, lead_id, channel, direction, provider, capture_mode, consent_state, timestamp, duration_seconds, summary, raw_reference, provider_metadata, created_at, updated_at ) VALUES ( $1::uuid, $2, $3, 'whatsapp_message', 'inbound', 'operator_seed', 'operator_note', 'granted', $4, NULL, $5, $6, $7::jsonb, NOW(), NOW() ) ON CONFLICT (event_id) DO UPDATE SET timestamp = EXCLUDED.timestamp, summary = EXCLUDED.summary, provider_metadata = EXCLUDED.provider_metadata, updated_at = NOW() """, edge_event_id, tenant_id, lead_id, now - timedelta(minutes=25 + index * 12), f"{client['name']} asked the operator to proceed with the next step: {client['next_action']}", f"{SEED_SOURCE}:{client['key']}", _json({"seed_source": SEED_SOURCE, "investor_demo": True}), ) counts["edge_events"] += 1 reminder_id = _stable_uuid(tenant_id, f"reminder:{client['key']}") await conn.execute( """ INSERT INTO intel_reminders ( reminder_id, tenant_id, person_id, lead_id, opportunity_id, reminder_type, title, notes, due_at, status, assigned_to, created_by_type, priority, metadata_json, created_at ) VALUES ( $1::uuid, $2, $3::uuid, $4::uuid, $5::uuid, 'follow_up', $6, $7, $8, 'pending', $9::uuid, 'human', $10, $11::jsonb, NOW() ) ON CONFLICT (reminder_id) DO UPDATE SET title = EXCLUDED.title, notes = EXCLUDED.notes, due_at = EXCLUDED.due_at, status = 'pending', assigned_to = EXCLUDED.assigned_to, priority = EXCLUDED.priority, metadata_json = intel_reminders.metadata_json || EXCLUDED.metadata_json """, reminder_id, tenant_id, person_id, lead_id, opportunity_id, f"Follow up with {client['name']} on {client['project']}", client["next_action"], now + timedelta(hours=3 + index * 4), operator_user_id, "urgent" if client["urgency"] == "critical" else ("high" if client["urgency"] == "high" else "normal"), _json({"seed_source": SEED_SOURCE, "investor_demo": True}), ) counts["reminders"] += 1 calendar_id = _stable_uuid(tenant_id, f"calendar:{client['key']}") start_at = now + timedelta(days=1 + (index % 3), hours=2 + index) await conn.execute( """ INSERT INTO user_calendar_events ( calendar_event_id, tenant_id, owner_user_id, lead_id, source_event_id, title, description, start_at, end_at, all_day, status, reminder_minutes, created_by, is_nemoclaw_confirmed, location, metadata, created_at, updated_at ) VALUES ( $1::uuid, $2, $3, $4, $5::uuid, $6, $7, $8, $9, FALSE, 'confirmed', ARRAY[15, 60], 'user', TRUE, $10, $11::jsonb, NOW(), NOW() ) ON CONFLICT (calendar_event_id) DO UPDATE SET title = EXCLUDED.title, description = EXCLUDED.description, start_at = EXCLUDED.start_at, end_at = EXCLUDED.end_at, status = 'confirmed', reminder_minutes = EXCLUDED.reminder_minutes, location = EXCLUDED.location, metadata = EXCLUDED.metadata, updated_at = NOW() """, calendar_id, tenant_id, owner_user_ref, lead_id, edge_event_id, f"{client['name']} - {client['configuration']} review", client["next_action"], start_at, start_at + timedelta(minutes=45), client["project"], _json({"seed_source": SEED_SOURCE, "investor_demo": True}), ) counts["calendar_events"] += 1 recommendation_id = _stable_uuid(tenant_id, f"insight:{client['key']}") await conn.execute( """ INSERT INTO insight_recommendations ( recommendation_id, tenant_id, lead_id, source_event_id, recommendation_type, summary, suggested_action, target_system, status, confidence, created_at, updated_at ) VALUES ( $1::uuid, $2, $3, $4::uuid, $5, $6, $7, $8, 'pending', $9, NOW() - ($10::int * INTERVAL '8 minutes'), NOW() ) ON CONFLICT (recommendation_id) DO UPDATE SET source_event_id = EXCLUDED.source_event_id, recommendation_type = EXCLUDED.recommendation_type, summary = EXCLUDED.summary, suggested_action = EXCLUDED.suggested_action, target_system = EXCLUDED.target_system, status = 'pending', confidence = EXCLUDED.confidence, updated_at = NOW() """, recommendation_id, tenant_id, lead_id, edge_event_id, "schedule_meeting" if client["urgency"] in {"high", "critical"} else "send_property_info", f"{client['name']} is showing strong buying intent for {client['project']}.", client["next_action"], "calendar" if client["urgency"] in {"high", "critical"} else "whatsapp", min(0.98, float(client["scores"][0]) + 0.04), index, ) counts["insight_recommendations"] += 1 thread_id = _stable_uuid(tenant_id, f"comms-thread:{client['key']}") await conn.execute( """ INSERT INTO comms_threads ( thread_id, provider, external_thread_id, person_id, phone_e164, display_name, channel, status, assigned_user_id, last_message_at, unread_count, metadata_json, created_at, updated_at ) VALUES ( $1::uuid, 'waha', $2, $3::uuid, $4, $5, 'whatsapp', 'open', $6::uuid, $7, 1, $8::jsonb, NOW() - INTERVAL '3 days', NOW() ) ON CONFLICT (thread_id) DO UPDATE SET person_id = EXCLUDED.person_id, phone_e164 = EXCLUDED.phone_e164, display_name = EXCLUDED.display_name, assigned_user_id = EXCLUDED.assigned_user_id, last_message_at = EXCLUDED.last_message_at, unread_count = EXCLUDED.unread_count, metadata_json = comms_threads.metadata_json || EXCLUDED.metadata_json, updated_at = NOW() """, thread_id, f"{SEED_SOURCE}:{client['key']}", person_id, client["phone"], client["name"], operator_user_id, now - timedelta(minutes=18 + index * 9), _json({"seed_source": SEED_SOURCE, "investor_demo": True, "project": client["project"]}), ) counts["comms_threads"] += 1 message_bodies = [ ("inbound", f"Can you send the latest floor stack and payment plan for {client['project']}?"), ("outbound", f"Sharing the executive deck now. I also reserved a walkthrough slot for {client['configuration']}."), ("inbound", f"Please proceed. Budget is aligned around {client['budget']} if the view and handover schedule work."), ] for message_index, (direction, body) in enumerate(message_bodies): await conn.execute( """ INSERT INTO comms_messages ( message_id, thread_id, provider, external_message_id, direction, message_type, body, delivery_status, sent_at, delivered_at, raw_payload, created_at ) VALUES ( $1::uuid, $2::uuid, 'waha', $3, $4, 'text', $5, 'delivered', $6, $6, $7::jsonb, NOW() ) ON CONFLICT (message_id) DO UPDATE SET body = EXCLUDED.body, delivery_status = EXCLUDED.delivery_status, sent_at = EXCLUDED.sent_at, delivered_at = EXCLUDED.delivered_at, raw_payload = EXCLUDED.raw_payload """, _stable_uuid(tenant_id, f"comms-message:{client['key']}:{message_index}"), thread_id, f"{SEED_SOURCE}:{client['key']}:{message_index}", direction, body, now - timedelta(minutes=46 - message_index * 14 + index * 3), _json({"seed_source": SEED_SOURCE, "investor_demo": True}), ) counts["comms_messages"] += 1 call_id = _stable_uuid(tenant_id, f"comms-call:{client['key']}") await conn.execute( """ INSERT INTO comms_call_logs ( call_id, thread_id, person_id, provider, external_call_id, phone_e164, direction, status, started_at, ended_at, duration_seconds, recording_url, transcript_text, raw_payload, created_at ) VALUES ( $1::uuid, $2::uuid, $3::uuid, 'waha', $4, $5, 'outbound', 'completed', $6, $7, $8, $9, $10, $11::jsonb, NOW() ) ON CONFLICT (call_id) DO UPDATE SET thread_id = EXCLUDED.thread_id, person_id = EXCLUDED.person_id, status = EXCLUDED.status, started_at = EXCLUDED.started_at, ended_at = EXCLUDED.ended_at, duration_seconds = EXCLUDED.duration_seconds, recording_url = EXCLUDED.recording_url, transcript_text = EXCLUDED.transcript_text, raw_payload = EXCLUDED.raw_payload """, call_id, thread_id, person_id, f"{SEED_SOURCE}:call:{client['key']}", client["phone"], now - timedelta(hours=2 + index), now - timedelta(hours=2 + index) + timedelta(minutes=7, seconds=30), 450, f"s3://velocity-demo-media/{SEED_SOURCE}/calls/{client['key']}.m4a", f"Discussed {client['project']}, {client['configuration']}, budget {client['budget']}, and next action: {client['next_action']}", _json({"seed_source": SEED_SOURCE, "investor_demo": True}), ) counts["comms_call_logs"] += 1 batch_id = _stable_uuid(tenant_id, "workflow-import-batch:investor-demo") await conn.execute( """ INSERT INTO workflow_import_batches ( batch_id, tenant_id, source_system, uploaded_filename, mime_type, storage_ref, row_count, mapped_count, unresolved_count, canonical_count, uploaded_by, lifecycle, mapping_manifest, errors_json, metadata_json, created_at, updated_at ) VALUES ( $1::uuid, $2, 'sales_ops_csv', 'investor_demo_priority_clients.csv', 'text/csv', $3, 6, 5, 1, 0, $4::uuid, 'proposed', $5::jsonb, '[]'::jsonb, $6::jsonb, NOW() - INTERVAL '2 hours', NOW() ) ON CONFLICT (batch_id) DO UPDATE SET row_count = EXCLUDED.row_count, mapped_count = EXCLUDED.mapped_count, unresolved_count = EXCLUDED.unresolved_count, lifecycle = EXCLUDED.lifecycle, mapping_manifest = EXCLUDED.mapping_manifest, metadata_json = workflow_import_batches.metadata_json || EXCLUDED.metadata_json, updated_at = NOW() """, batch_id, tenant_id, f"s3://velocity-demo/{SEED_SOURCE}/investor_demo_priority_clients.csv", operator_user_id, _json({"mapped": {"Name": "full_name", "Phone": "primary_phone", "Budget": "budget_band"}}), _json({"seed_source": SEED_SOURCE, "investor_demo": True}), ) counts["import_batches"] += 1 for row_number, client in enumerate(DEMO_CLIENTS[:3], start=1): action_id = _stable_uuid(tenant_id, f"workflow-import-proposal:{client['key']}") payload = { "batch_id": batch_id, "row_number": row_number, "canonical_payload": { "full_name": client["name"], "primary_phone": client["phone"], "buyer_type": client["buyer_type"], "budget_band": client["budget"], "project_name": client["project"], }, "raw_row": { "Name": client["name"], "Phone": client["phone"], "Budget": client["budget"], "Project": client["project"], }, "unresolved_fields": [] if row_number < 3 else ["preferred_visit_time"], "missing_required": [], "confidence": 0.92 - (row_number * 0.04), "review_required": True, } await conn.execute( """ INSERT INTO workflow_actions ( action_id, tenant_id, action_type, target_domain, target_entity_ref, proposal_payload, reasoning_summary, evidence_refs, confidence, status, approval_required, created_by_agent, created_at, updated_at ) VALUES ( $1::uuid, $2, 'import_proposal', 'crm', $3, $4::jsonb, $5, $6::jsonb, $7, 'pending', TRUE, 'velocity_ipad_seed', NOW() - INTERVAL '90 minutes', NOW() ) ON CONFLICT (action_id) DO UPDATE SET proposal_payload = EXCLUDED.proposal_payload, reasoning_summary = EXCLUDED.reasoning_summary, evidence_refs = EXCLUDED.evidence_refs, confidence = EXCLUDED.confidence, status = 'pending', approval_required = TRUE, updated_at = NOW() """, action_id, tenant_id, client["email"], _json(payload), f"Mapped {client['name']} from realistic investor-demo CRM import.", _json([f"batch:{batch_id}", f"seed_source:{SEED_SOURCE}"]), payload["confidence"], ) counts["import_proposals"] += 1 return counts async def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--operator-email", default=os.getenv("VELOCITY_DEMO_OPERATOR_EMAIL", DEFAULT_OPERATOR_EMAIL)) parser.add_argument("--tenant-id", default=os.getenv("VELOCITY_DEMO_TENANT_ID")) parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() _load_env() if asyncpg is None: raise RuntimeError( "asyncpg is not installed. Install backend requirements first: " "python3 -m pip install -r backend/requirements.txt" ) if args.dry_run: try: db_kwargs = _db_kwargs() except RuntimeError as exc: print( json.dumps( { "status": "dry_run_without_db", "seed_source": SEED_SOURCE, "operator_email": args.operator_email, "database_note": str(exc), "expected_counts": _expected_counts(), }, indent=2, ) ) return else: db_kwargs = _db_kwargs() conn = await asyncpg.connect(**db_kwargs) try: tenant_id, operator_user_id = await _resolve_operator(conn, args.operator_email, args.tenant_id) counts = _expected_counts() if args.dry_run else await seed(conn, tenant_id, operator_user_id) print( json.dumps( { "status": "dry_run" if args.dry_run else "seeded", "seed_source": SEED_SOURCE, "tenant_id": tenant_id, "operator_email": args.operator_email, "operator_user_id": operator_user_id, "counts": counts, }, indent=2, ) ) finally: await conn.close() if __name__ == "__main__": asyncio.run(main())