From 4a2cfc5756cfddbc4d904617ae224189d8be96d0 Mon Sep 17 00:00:00 2001 From: Floke Date: Sun, 1 Mar 2026 18:43:47 +0000 Subject: [PATCH] [2ff88f42] feat: Integrated Miller-Heiman strategic context into Marketing Matrix --- company-explorer/backend/database.py | 1 + .../backend/scripts/generate_matrix.py | 4 + .../backend/scripts/sync_notion_industries.py | 3 + devtools/add_strategy_column.py | 24 +++ devtools/push_mh_to_notion.py | 188 ++++++++++++++++++ 5 files changed, 220 insertions(+) create mode 100644 devtools/add_strategy_column.py create mode 100644 devtools/push_mh_to_notion.py diff --git a/company-explorer/backend/database.py b/company-explorer/backend/database.py index 00cd1cc6..6f53466d 100644 --- a/company-explorer/backend/database.py +++ b/company-explorer/backend/database.py @@ -140,6 +140,7 @@ class Industry(Base): notes = Column(Text, nullable=True) priority = Column(String, nullable=True) # Replaces old status concept ("Freigegeben") ops_focus_secondary = Column(Boolean, default=False) + strategy_briefing = Column(Text, nullable=True) # NEW: Strategic context (Miller Heiman) # NEW SCHEMA FIELDS (from MIGRATION_PLAN) metric_type = Column(String, nullable=True) # Unit_Count, Area_in, Area_out diff --git a/company-explorer/backend/scripts/generate_matrix.py b/company-explorer/backend/scripts/generate_matrix.py index da5928cc..8a1aca8a 100644 --- a/company-explorer/backend/scripts/generate_matrix.py +++ b/company-explorer/backend/scripts/generate_matrix.py @@ -121,8 +121,12 @@ SPEZIFISCHE HERAUSFORDERUNGEN (PAIN POINTS) DER ROLLE: SPEZIFISCHE NUTZEN (GAINS) DER ROLLE: {chr(10).join(['- ' + str(g) for g in persona_gains])} +HINTERGRUNDWISSEN & STRATEGIE (Miller Heiman): +{industry.strategy_briefing or 'Kein spezifisches Briefing verfügbar.'} + --- DEINE AUFGABE --- Deine Texte müssen "voll ins Zentrum" der Rolle treffen. Vermeide oberflächliche Floskeln. Nutze die Details zur Denkweise, den KPIs und den Überzeugungsargumenten, um eine tiefgreifende Relevanz zu erzeugen. +Nutze das Strategie-Briefing, um typische Einwände vorwegzunehmen oder "Red Flags" zu vermeiden. 1. **Subject:** Formuliere eine kurze Betreffzeile (max. 6 Wörter). Richte sie **direkt an einem der persönlichen Pain Points** des Ansprechpartners oder dem zentralen Branchen-Pain. Sei scharfsinnig, nicht werblich. diff --git a/company-explorer/backend/scripts/sync_notion_industries.py b/company-explorer/backend/scripts/sync_notion_industries.py index 0d29705b..ff75942a 100644 --- a/company-explorer/backend/scripts/sync_notion_industries.py +++ b/company-explorer/backend/scripts/sync_notion_industries.py @@ -156,6 +156,9 @@ def sync_industries(token, session): # New Field: Ops Focus Secondary (Checkbox) industry.ops_focus_secondary = props.get("Ops Focus: Secondary", {}).get("checkbox", False) + + # New Field: Strategy Briefing (Miller Heiman) + industry.strategy_briefing = extract_rich_text(props.get("Strategy Briefing")) # Relation: Primary Product Category relation = props.get("Primary Product Category", {}).get("relation", []) diff --git a/devtools/add_strategy_column.py b/devtools/add_strategy_column.py new file mode 100644 index 00000000..9922361a --- /dev/null +++ b/devtools/add_strategy_column.py @@ -0,0 +1,24 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" + +def add_column(): + try: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + print(f"Adding column 'strategy_briefing' to 'industries' table in {DB_PATH}...") + cursor.execute("ALTER TABLE industries ADD COLUMN strategy_briefing TEXT;") + conn.commit() + print("Success.") + except sqlite3.OperationalError as e: + if "duplicate column name" in str(e): + print("Column 'strategy_briefing' already exists. Skipping.") + else: + print(f"Error: {e}") + except Exception as e: + print(f"Error: {e}") + finally: + if conn: conn.close() + +if __name__ == "__main__": + add_column() diff --git a/devtools/push_mh_to_notion.py b/devtools/push_mh_to_notion.py new file mode 100644 index 00000000..ae8fa418 --- /dev/null +++ b/devtools/push_mh_to_notion.py @@ -0,0 +1,188 @@ +import sys +import os +import csv +import requests +import json +import logging +from collections import defaultdict + +# Setup Logger +logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') +logger = logging.getLogger("NotionPusher") + +# Config +CSV_PATH = "./docs/miller_heiman_augmented.csv" +NOTION_TOKEN_FILE = "/app/notion_token.txt" +INDUSTRIES_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +def load_notion_token(): + try: + with open(NOTION_TOKEN_FILE, "r") as f: + return f.read().strip() + except: + logger.error("Token file not found.") + sys.exit(1) + +def add_strategy_property(token): + """Try to add the 'Strategy Briefing' property to the database schema.""" + url = f"https://api.notion.com/v1/databases/{INDUSTRIES_DB_ID}" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + + # First, get current schema to check if it exists + resp = requests.get(url, headers=headers) + if resp.status_code != 200: + logger.error(f"Failed to fetch DB schema: {resp.text}") + return False + + current_properties = resp.json().get("properties", {}) + if "Strategy Briefing" in current_properties: + logger.info("Property 'Strategy Briefing' already exists.") + return True + + logger.info("Property 'Strategy Briefing' missing. Attempting to create...") + + payload = { + "properties": { + "Strategy Briefing": { + "rich_text": {} + } + } + } + + update_resp = requests.patch(url, headers=headers, json=payload) + if update_resp.status_code == 200: + logger.info("Successfully added 'Strategy Briefing' property.") + return True + else: + logger.error(f"Failed to add property: {update_resp.text}") + return False + +def get_pages_map(token): + """Returns a map of Vertical Name -> Page ID""" + url = f"https://api.notion.com/v1/databases/{INDUSTRIES_DB_ID}/query" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + + mapping = {} + has_more = True + next_cursor = None + + while has_more: + payload = {} + if next_cursor: + payload["start_cursor"] = next_cursor + + resp = requests.post(url, headers=headers, json=payload) + if resp.status_code != 200: + break + + data = resp.json() + for page in data.get("results", []): + props = page.get("properties", {}) + name_parts = props.get("Vertical", {}).get("title", []) + if name_parts: + name = "".join([t.get("plain_text", "") for t in name_parts]) + mapping[name] = page["id"] + + has_more = data.get("has_more", False) + next_cursor = data.get("next_cursor") + + return mapping + +def update_page(token, page_id, pains, gains, strategy): + url = f"https://api.notion.com/v1/pages/{page_id}" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + + # Construct Payload + props = {} + + # Only update if content exists (safety) + if pains: + props["Pains"] = {"rich_text": [{"text": {"content": pains[:2000]}}]} # Limit to 2000 chars to be safe + if gains: + props["Gains"] = {"rich_text": [{"text": {"content": gains[:2000]}}]} + if strategy: + props["Strategy Briefing"] = {"rich_text": [{"text": {"content": strategy[:2000]}}]} + + if not props: + return + + payload = {"properties": props} + resp = requests.patch(url, headers=headers, json=payload) + + if resp.status_code != 200: + logger.error(f"Failed to update page {page_id}: {resp.text}") + else: + logger.info(f"Updated page {page_id}") + +def main(): + token = load_notion_token() + + # 1. Ensure Schema + if not add_strategy_property(token): + logger.warning("Could not add 'Strategy Briefing' column. Please add it manually in Notion as a 'Text' property.") + # We continue, maybe it failed because of permissions but column exists? + # Actually if it failed, the update later will fail too if key is missing. + # But let's try. + + # 2. Map Notion Verticals + logger.info("Mapping existing Notion pages...") + notion_map = get_pages_map(token) + logger.info(f"Found {len(notion_map)} verticals in Notion.") + + # 3. Read CSV + logger.info(f"Reading CSV: {CSV_PATH}") + csv_data = {} + + with open(CSV_PATH, "r", encoding="utf-8-sig") as f: + reader = csv.DictReader(f, delimiter=";") + for row in reader: + vertical = row.get("Vertical") + if not vertical: + continue + + # Aggregate Strategy + parts = [] + if row.get("MH Coach Hypothesis"): + parts.append(f"🧠 MH Coach: {row.get('MH Coach Hypothesis')}") + if row.get("MH Early Red Flags"): + parts.append(f"🚩 Red Flags: {row.get('MH Early Red Flags')}") + if row.get("MH Adjustments / Ergänzungen"): + parts.append(f"🔧 Adjustments: {row.get('MH Adjustments / Ergänzungen')}") + + strategy = "\n\n".join(parts) + + # Store (last row wins if duplicates, but they should be identical for vertical data) + csv_data[vertical] = { + "pains": row.get("Pains (clean)", ""), + "gains": row.get("Gains (clean)", ""), + "strategy": strategy + } + + # 4. Push Updates + logger.info("Starting Batch Update...") + count = 0 + for vertical, data in csv_data.items(): + if vertical in notion_map: + page_id = notion_map[vertical] + logger.info(f"Updating '{vertical}'...") + update_page(token, page_id, data["pains"], data["gains"], data["strategy"]) + count += 1 + else: + logger.warning(f"Skipping '{vertical}' (Not found in Notion)") + + logger.info(f"Finished. Updated {count} verticals.") + +if __name__ == "__main__": + main()