Case Study 21-2: Maya Researches Prospective Clients Before Every New Business Call

The Situation

Maya Reyes has been a freelance business consultant for three years. She's good at what she does — her client retention rate is high and referrals are steady. But she has a blind spot in her sales process.

Maya's business development approach has always been reactive. A prospect calls, they describe their problem, she listens and proposes a solution. The calls go well, but she often leaves them wishing she'd known more going in. Last month she took a call with a 40-person manufacturing company and spent the first 15 minutes asking questions she could have answered with five minutes of research. The prospect, who turned out to be a former VP at a Fortune 500, asked early on what Maya knew about their industry. Maya said "I'd love to learn more from you."

That answer would have been fine from a junior consultant. From a $175/hour independent advisor with three years of experience, it signaled that she hadn't prepared.

She decides to build a systematic pre-call research tool.


What Maya Builds

A script that takes a company name or ticker symbol and generates a briefing document she can review before a new business call. The briefing includes:

  1. Company overview — industry, size, what they do
  2. Recent news — anything significant in the last 30 days
  3. Financial context — for public companies, key metrics that shape their business pressures
  4. Weather/logistics note — if the company is in a city with current weather events, a quick note (useful for opening small talk and understanding operational context)

The Code

Client Briefing Generator

"""
client_briefing.py
==================
Generates a pre-call research briefing for prospective clients.
Maya Reyes Consulting — new business preparation tool.

Usage:
    python client_briefing.py --company "Acme Manufacturing" --ticker ACMF
    python client_briefing.py --company "Regional Hospital Group" --city "Cleveland"
    python client_briefing.py --help

Requirements:
    pip install requests python-dotenv

Environment variables:
    NEWS_API_KEY         — from newsapi.org (free tier: 100 req/day)
    ALPHA_VANTAGE_API_KEY — from alphavantage.co (free tier: 25 req/day)
"""

import os
import sys
import argparse
import logging
from datetime import datetime, timedelta
from pathlib import Path
from dotenv import load_dotenv
import requests

load_dotenv()
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s  %(levelname)-8s  %(message)s",
    datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Data fetching functions
# ---------------------------------------------------------------------------

def fetch_company_overview(ticker_symbol: str) -> dict:
    """
    Fetch company overview from Alpha Vantage.

    Returns company profile dict or empty dict if unavailable.
    """
    api_key = os.environ.get("ALPHA_VANTAGE_API_KEY")
    if not api_key:
        logger.info("ALPHA_VANTAGE_API_KEY not set — skipping financial data")
        return {}

    try:
        response = requests.get(
            "https://www.alphavantage.co/query",
            params={
                "function": "OVERVIEW",
                "symbol": ticker_symbol.upper(),
                "apikey": api_key,
            },
            timeout=20,
        )
        response.raise_for_status()
        data = response.json()

        # API rate limit message
        if "Note" in data or "Information" in data:
            msg = data.get("Note") or data.get("Information", "Rate limit")
            logger.warning(f"Alpha Vantage: {msg[:100]}")
            return {"_rate_limited": True}

        if not data or "Name" not in data:
            logger.info(f"No Alpha Vantage data for ticker: {ticker_symbol}")
            return {}

        return {
            "company_name": data.get("Name"),
            "ticker": ticker_symbol.upper(),
            "exchange": data.get("Exchange"),
            "sector": data.get("Sector"),
            "industry": data.get("Industry"),
            "country": data.get("Country"),
            "description": data.get("Description", ""),
            "full_time_employees": data.get("FullTimeEmployees"),
            "fiscal_year_end": data.get("FiscalYearEnd"),
            "market_cap": _format_large_number(data.get("MarketCapitalization", "0")),
            "revenue_ttm": _format_large_number(data.get("RevenueTTM", "0")),
            "profit_margin": data.get("ProfitMargin"),
            "operating_margin": data.get("OperatingMarginTTM"),
            "pe_ratio": data.get("PERatio"),
            "ev_to_ebitda": data.get("EVToEBITDA"),
            "debt_to_equity": data.get("DebtToEquityRatio"),
            "dividend_yield": data.get("DividendYield"),
            "52_week_high": data.get("52WeekHigh"),
            "52_week_low": data.get("52WeekLow"),
            "analyst_target_price": data.get("AnalystTargetPrice"),
        }

    except requests.exceptions.RequestException as e:
        logger.warning(f"Failed to fetch company data for {ticker_symbol}: {e}")
        return {}


def _format_large_number(value_str: str) -> str:
    """Format large numeric strings into readable form (e.g., 2800000000 -> $2.8B)."""
    try:
        value = float(value_str)
        if value >= 1_000_000_000:
            return f"${value / 1_000_000_000:.1f}B"
        elif value >= 1_000_000:
            return f"${value / 1_000_000:.1f}M"
        elif value >= 1_000:
            return f"${value / 1_000:.1f}K"
        else:
            return f"${value:,.0f}"
    except (ValueError, TypeError):
        return value_str or "N/A"


def fetch_company_news(
    company_name: str,
    ticker_symbol: str = None,
    days_back: int = 30,
    max_articles: int = 10,
) -> list[dict]:
    """
    Fetch recent news about a company.

    Combines company name search and ticker search, deduplicates by URL.

    Args:
        company_name: Company name for news search
        ticker_symbol: Optional ticker for additional search
        days_back: How many days of news to retrieve
        max_articles: Maximum number of articles to return

    Returns:
        List of article dicts sorted by recency
    """
    api_key = os.environ.get("NEWS_API_KEY")
    if not api_key:
        logger.info("NEWS_API_KEY not set — skipping news fetch")
        return []

    from_date = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%d")

    # Build search queries
    queries = [f'"{company_name}"']
    if ticker_symbol:
        queries.append(f'"{ticker_symbol}" company earnings')

    seen_urls = set()
    all_articles = []

    for query in queries:
        try:
            response = requests.get(
                "https://newsapi.org/v2/everything",
                headers={"X-Api-Key": api_key},
                params={
                    "q": query,
                    "from": from_date,
                    "language": "en",
                    "sortBy": "relevancy",
                    "pageSize": max_articles,
                },
                timeout=20,
            )

            if response.status_code == 401:
                logger.error("Invalid NEWS_API_KEY")
                return []

            if response.status_code == 426:
                logger.warning(
                    "NewsAPI free tier restriction: Cannot search articles older than 30 days. "
                    "Upgrade to paid plan for extended history."
                )
                # Try with a shorter date range
                response = requests.get(
                    "https://newsapi.org/v2/everything",
                    headers={"X-Api-Key": api_key},
                    params={
                        "q": query,
                        "language": "en",
                        "sortBy": "relevancy",
                        "pageSize": max_articles,
                    },
                    timeout=20,
                )

            response.raise_for_status()
            data = response.json()

            if data.get("status") != "ok":
                logger.warning(f"NewsAPI: {data.get('message', 'Unknown error')}")
                continue

            for article in data.get("articles", []):
                url = article.get("url", "")
                title = article.get("title", "")

                # Skip removed articles
                if title == "[Removed]" or not title:
                    continue

                if url and url not in seen_urls:
                    seen_urls.add(url)
                    all_articles.append({
                        "title": title,
                        "source": article.get("source", {}).get("name", ""),
                        "published_date": article.get("publishedAt", "")[:10],
                        "description": (article.get("description") or "")[:300],
                        "url": url,
                    })

        except requests.exceptions.RequestException as e:
            logger.warning(f"News fetch failed for query '{query}': {e}")
            continue

    # Sort by date, most recent first, limit to max_articles
    all_articles.sort(key=lambda a: a["published_date"], reverse=True)
    return all_articles[:max_articles]


def fetch_city_weather(city_name: str, latitude: float, longitude: float) -> dict:
    """
    Fetch current weather for a city using Open-Meteo.
    Free, no API key required.

    Returns:
        Dict with temperature, wind, and conditions. Empty dict if fetch fails.
    """
    try:
        response = requests.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": latitude,
                "longitude": longitude,
                "current_weather": True,
                "timezone": "auto",
            },
            timeout=10,
        )
        response.raise_for_status()
        data = response.json()

        current = data.get("current_weather", {})
        temp_c = current.get("temperature")
        temp_f = (temp_c * 9 / 5 + 32) if temp_c is not None else None
        wind_kmh = current.get("windspeed")
        weather_code = current.get("weathercode", 0)

        # WMO weather code interpretation
        weather_descriptions = {
            0: "Clear sky",
            1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
            45: "Foggy", 48: "Icy fog",
            51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
            61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
            71: "Slight snowfall", 73: "Moderate snowfall", 75: "Heavy snowfall",
            80: "Slight showers", 81: "Moderate showers", 82: "Violent showers",
            95: "Thunderstorm", 99: "Thunderstorm with hail",
        }

        severe_codes = {65, 75, 82, 95, 99}
        is_notable = weather_code in severe_codes

        return {
            "city": city_name,
            "temperature_c": temp_c,
            "temperature_f": round(temp_f, 1) if temp_f else None,
            "wind_speed_kmh": wind_kmh,
            "description": weather_descriptions.get(weather_code, f"Code {weather_code}"),
            "is_notable_weather": is_notable,
        }

    except requests.exceptions.RequestException as e:
        logger.debug(f"Weather fetch failed for {city_name}: {e}")
        return {}


# ---------------------------------------------------------------------------
# Common city coordinates
# ---------------------------------------------------------------------------

CITY_COORDINATES = {
    "new york": (40.7128, -74.0060),
    "new york city": (40.7128, -74.0060),
    "nyc": (40.7128, -74.0060),
    "los angeles": (34.0522, -118.2437),
    "chicago": (41.8781, -87.6298),
    "houston": (29.7604, -95.3698),
    "phoenix": (33.4484, -112.0740),
    "philadelphia": (39.9526, -75.1652),
    "san antonio": (29.4241, -98.4936),
    "san diego": (32.7157, -117.1611),
    "dallas": (32.7767, -96.7970),
    "san jose": (37.3382, -121.8863),
    "austin": (30.2672, -97.7431),
    "san francisco": (37.7749, -122.4194),
    "seattle": (47.6062, -122.3321),
    "denver": (39.7392, -104.9903),
    "boston": (42.3601, -71.0589),
    "atlanta": (33.7490, -84.3880),
    "miami": (25.7617, -80.1918),
    "minneapolis": (44.9778, -93.2650),
    "portland": (45.5231, -122.6765),
    "cleveland": (41.4993, -81.6944),
    "detroit": (42.3314, -83.0458),
    "nashville": (36.1627, -86.7816),
    "memphis": (35.1495, -90.0490),
    "charlotte": (35.2271, -80.8431),
    "raleigh": (35.7796, -78.6382),
    "pittsburgh": (40.4406, -79.9959),
    "cincinnati": (39.1031, -84.5120),
    "kansas city": (39.0997, -94.5786),
    "st. louis": (38.6270, -90.1994),
    "st louis": (38.6270, -90.1994),
    "indianapolis": (39.7684, -86.1581),
    "columbus": (39.9612, -82.9988),
    "milwaukee": (43.0389, -87.9065),
    "toronto": (43.6532, -79.3832),
    "london": (51.5074, -0.1278),
}

def get_city_coordinates(city_name: str) -> tuple[float, float] | None:
    """Look up coordinates for a common city name."""
    return CITY_COORDINATES.get(city_name.lower())


# ---------------------------------------------------------------------------
# Report generation
# ---------------------------------------------------------------------------

def generate_client_briefing(
    company_name: str,
    company_data: dict,
    news_articles: list[dict],
    weather_data: dict,
    output_dir: Path,
) -> Path:
    """
    Generate a pre-call client briefing document.

    Args:
        company_name: The company being researched
        company_data: Dict from fetch_company_overview()
        news_articles: List from fetch_company_news()
        weather_data: Dict from fetch_city_weather() (may be empty)
        output_dir: Directory to save the briefing file

    Returns:
        Path to the generated briefing file
    """
    now = datetime.now()
    date_str = now.strftime("%Y-%m-%d")
    time_str = now.strftime("%H:%M")
    safe_company_name = "".join(
        c if c.isalnum() or c in " -_" else "_" for c in company_name
    ).strip().replace(" ", "_")
    output_path = output_dir / f"briefing_{safe_company_name}_{date_str}.txt"

    with open(output_path, "w", encoding="utf-8") as f:

        f.write(f"PRE-CALL RESEARCH BRIEFING\n")
        f.write(f"Maya Reyes Consulting\n")
        f.write(f"Prepared: {date_str} at {time_str}\n")
        f.write("=" * 65 + "\n\n")

        # Company overview
        f.write(f"COMPANY: {company_name.upper()}\n")
        f.write("-" * 45 + "\n")

        if company_data and "_rate_limited" not in company_data:
            f.write(f"  Name:         {company_data.get('company_name', company_name)}\n")
            if company_data.get("ticker"):
                f.write(f"  Ticker:       {company_data['ticker']} ({company_data.get('exchange', 'N/A')})\n")
            f.write(f"  Sector:       {company_data.get('sector', 'N/A')}\n")
            f.write(f"  Industry:     {company_data.get('industry', 'N/A')}\n")
            f.write(f"  Country:      {company_data.get('country', 'N/A')}\n")
            employees = company_data.get("full_time_employees", "N/A")
            if employees and employees != "N/A":
                f.write(f"  Employees:    {int(employees):,} full-time\n")
            f.write(f"  Market Cap:   {company_data.get('market_cap', 'N/A')}\n")
            f.write(f"  Revenue (TTM):{company_data.get('revenue_ttm', 'N/A')}\n")
            f.write(f"  Profit Margin:{company_data.get('profit_margin', 'N/A')}\n")
            f.write(f"  P/E Ratio:    {company_data.get('pe_ratio', 'N/A')}\n")

            if company_data.get("description"):
                f.write(f"\n  BUSINESS DESCRIPTION:\n")
                description = company_data["description"]
                # Word-wrap at 62 chars
                words = description.split()
                line = "  "
                for word in words:
                    if len(line) + len(word) + 1 > 63:
                        f.write(line + "\n")
                        line = "  " + word + " "
                    else:
                        line += word + " "
                if line.strip():
                    f.write(line + "\n")

        elif company_data.get("_rate_limited"):
            f.write("  [Alpha Vantage API rate limit reached — try again in 1 minute]\n")
        else:
            f.write(f"  No financial data available for {company_name}.\n")
            f.write("  (Set ALPHA_VANTAGE_API_KEY and provide a ticker symbol for financial data)\n")

        # Key financial ratios for call prep
        if company_data and not company_data.get("_rate_limited"):
            f.write(f"\n  KEY METRICS FOR CONVERSATION:\n")
            margin = company_data.get("profit_margin")
            if margin:
                try:
                    margin_pct = float(margin) * 100
                    if margin_pct < 5:
                        margin_note = "TIGHT margins — price sensitivity likely high"
                    elif margin_pct < 15:
                        margin_note = "Moderate margins"
                    else:
                        margin_note = "Healthy margins — value over cost"
                    f.write(f"  - Profit margin: {margin_pct:.1f}% ({margin_note})\n")
                except (ValueError, TypeError):
                    pass

            debt_eq = company_data.get("debt_to_equity")
            if debt_eq:
                try:
                    de_val = float(debt_eq)
                    if de_val > 2.0:
                        de_note = "HIGH leverage — watch for cost-cutting initiatives"
                    elif de_val > 0.5:
                        de_note = "Moderate leverage"
                    else:
                        de_note = "Low leverage — likely in investment mode"
                    f.write(f"  - Debt/Equity: {de_val:.2f} ({de_note})\n")
                except (ValueError, TypeError):
                    pass

        # Weather (if available)
        if weather_data:
            f.write(f"\nLOCAL WEATHER\n")
            f.write("-" * 45 + "\n")
            f.write(
                f"  {weather_data.get('city', 'N/A')}: "
                f"{weather_data.get('temperature_f', 'N/A')}°F / "
                f"{weather_data.get('temperature_c', 'N/A')}°C  —  "
                f"{weather_data.get('description', 'N/A')}\n"
            )
            if weather_data.get("is_notable_weather"):
                f.write(
                    f"  NOTE: Significant weather event. "
                    f"Opening small talk opportunity / operational context.\n"
                )

        # Recent news
        f.write(f"\nRECENT NEWS ({len(news_articles)} articles, last 30 days)\n")
        f.write("-" * 45 + "\n")
        if news_articles:
            for i, article in enumerate(news_articles, 1):
                f.write(f"\n  {i}. [{article['published_date']}] {article['source']}\n")
                f.write(f"     {article['title']}\n")
                if article.get("description"):
                    desc = article["description"][:160]
                    f.write(f"     {desc}{'...' if len(article['description']) > 160 else ''}\n")
                f.write(f"     {article['url']}\n")
        else:
            f.write(
                "  No recent news found.\n"
                "  (Set NEWS_API_KEY to enable news monitoring)\n"
            )

        # Pre-call notes section (blank — for Maya to fill in)
        f.write(f"\nPRE-CALL NOTES\n")
        f.write("-" * 45 + "\n")
        f.write("  Referral source:     ____________________________\n")
        f.write("  Contact name/title:  ____________________________\n")
        f.write("  Primary need (heard): ____________________________\n")
        f.write("  My relevant work:    ____________________________\n")
        f.write("  Opening question:    ____________________________\n\n")

        f.write("  QUESTIONS TO ASK:\n")
        f.write("  1. \n")
        f.write("  2. \n")
        f.write("  3. \n\n")

        f.write("  RED FLAGS TO WATCH FOR:\n")
        f.write("  - \n\n")

        f.write("=" * 65 + "\n")
        f.write(
            "Data sources: Alpha Vantage (financials), NewsAPI (news), "
            "Open-Meteo (weather)\n"
        )
        f.write(f"Generated by Maya Reyes Consulting client briefing tool\n")

    return output_path


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Maya Reyes Consulting — Pre-call client briefing generator",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python client_briefing.py --company "General Mills" --ticker GIS --city "Minneapolis"
  python client_briefing.py --company "Regional Hospital Group" --city "Cleveland"
  python client_briefing.py --company "Tech Startup Inc" --ticker TECH
        """,
    )
    parser.add_argument("--company", required=True, help="Company name (required)")
    parser.add_argument("--ticker", default=None, help="Stock ticker symbol (optional)")
    parser.add_argument(
        "--city",
        default=None,
        help="Company headquarters city for weather context (optional)",
    )
    parser.add_argument(
        "--days",
        type=int,
        default=30,
        help="Days of news history to retrieve (default: 30)",
    )
    parser.add_argument(
        "--output-dir",
        default="briefings",
        help="Directory to save briefing files (default: briefings/)",
    )
    args = parser.parse_args()

    output_dir = Path(args.output_dir)
    output_dir.mkdir(exist_ok=True)

    print(f"\nMaya Reyes Consulting — Client Briefing Generator")
    print(f"Company: {args.company}")
    if args.ticker:
        print(f"Ticker:  {args.ticker}")
    print()

    # Fetch company financial data
    company_data = {}
    if args.ticker:
        print("Fetching company financials...")
        company_data = fetch_company_overview(args.ticker)

    # Fetch news
    print("Fetching recent news...")
    news_articles = fetch_company_news(
        company_name=args.company,
        ticker_symbol=args.ticker,
        days_back=args.days,
        max_articles=10,
    )

    # Fetch weather if city provided
    weather_data = {}
    if args.city:
        print(f"Fetching weather for {args.city}...")
        coords = get_city_coordinates(args.city)
        if coords:
            weather_data = fetch_city_weather(args.city, coords[0], coords[1])
        else:
            print(
                f"  City '{args.city}' not in coordinates database. "
                f"Skipping weather."
            )

    # Generate briefing
    print("Generating briefing document...")
    output_path = generate_client_briefing(
        company_name=args.company,
        company_data=company_data,
        news_articles=news_articles,
        weather_data=weather_data,
        output_dir=output_dir,
    )

    print(f"\nBriefing saved to: {output_path}")

    # Quick summary
    print("\nQuick Summary:")
    if company_data and not company_data.get("_rate_limited"):
        print(f"  Company: {company_data.get('company_name', args.company)}")
        print(f"  Industry: {company_data.get('industry', 'N/A')}")
        print(f"  Employees: {company_data.get('full_time_employees', 'N/A')}")
        print(f"  Revenue: {company_data.get('revenue_ttm', 'N/A')}")
    print(f"  News articles found: {len(news_articles)}")
    if weather_data:
        print(
            f"  Weather in {weather_data.get('city')}: "
            f"{weather_data.get('temperature_f')}°F, "
            f"{weather_data.get('description')}"
        )
    print(f"\nReview your briefing before the call: {output_path}")


if __name__ == "__main__":
    main()

The Result

Maya runs the tool before her next three new business calls. The difference is immediate.

For the first call — a 200-person regional grocery chain — her briefing showed their profit margin was under 3%, which told her they were deeply cost-conscious. She led with ROI and payback period rather than capability and features. The call went better than any she'd had in months.

For the second call — a 45-person manufacturing company — three of the ten recent news articles mentioned a factory acquisition. She opened by congratulating them on the expansion and asked how the integration was going. The prospect lit up. They were exactly the kind of problem — culture gaps, misaligned processes, metrics that didn't translate across facilities — that Maya specializes in.

For the third call — a private equity-backed services company — she found nothing relevant in the financial data (private company, no ticker) but the news showed a portfolio management change at the PE firm two months earlier. She noted it and mentioned she'd followed the transition announcement in her opening. The prospect thanked her for doing her homework.

Three calls, three conversations that felt more like partnerships than pitches. Maya saves the tool to her consulting_tools directory and adds it to her standard pre-call checklist.


What Maya Learned

Preparation is leverage. Every hour spent in research before a call saves two hours of awkward discovery conversation during it. The briefing tool makes that preparation systematic rather than ad hoc.

Free APIs are sufficient for most business intelligence needs. News, weather, and basic financials are all available without a paid subscription. The value isn't in expensive data — it's in synthesizing what's already publicly available.

The tool handles the recall problem. Maya knew she should research prospects. She just didn't always do it consistently because it required navigating multiple websites, taking notes, and organizing them somewhere. A script that outputs a formatted document she can print removes all friction.

Graceful degradation matters. The script works even when API keys aren't set or data isn't available — it just notes what's missing rather than crashing. That means she can use it on any machine, even if she hasn't configured every API key.


Extension Ideas

Maya's tool is already useful. But she's noted a few enhancements worth building:

  1. LinkedIn integration — If the prospect has a LinkedIn company page, scrape their recent updates (though LinkedIn's ToS requires care here)
  2. CRM integration — If the company is already in Maya's client database, pull the history automatically
  3. Industry benchmarks — Add industry-average margins and PE ratios so she can see at a glance whether this company is outperforming or underperforming its peers
  4. Email-ready format — Generate a version formatted for pasting into an email to a referrer, asking for specific context to prepare for the call

These extensions are left as exercises — all are achievable with tools from chapters you've already studied.


Discussion Questions

  1. The generate_client_briefing() function writes a text file rather than a PDF or Word document. What are the tradeoffs? Under what circumstances would a different format be better?

  2. The briefing includes an analysis of profit margin and debt/equity ratios with plain-English interpretations. What are the risks of this kind of automated interpretation? When might the interpretive logic be wrong?

  3. Maya's tool uses the company name for news search with exact-match quotes ("Maple Office Supplies Ltd"). What are the failure modes of this approach? How would you improve the query to catch more relevant articles?

  4. The weather feature uses a hardcoded dictionary of city coordinates. What's a more scalable approach? Which API could you use to convert any city name to coordinates automatically?