diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO
index 555fbea3..196710ee 100644
--- a/.dev_session/SESSION_INFO
+++ b/.dev_session/SESSION_INFO
@@ -1 +1 @@
-{"task_id": "31188f42-8544-8074-bad3-d3e1b9b4051f", "token": "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", "readme_path": "connector-superoffice/README.md", "session_start_time": "2026-03-04T18:41:33.912605"}
\ No newline at end of file
+{"task_id": "31188f42-8544-8074-bad3-d3e1b9b4051f", "token": "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", "readme_path": "connector-superoffice/README.md", "session_start_time": "2026-03-05T06:02:30.481235"}
\ No newline at end of file
diff --git a/clear_zombies.py b/clear_zombies.py
new file mode 100644
index 00000000..cb3573ee
--- /dev/null
+++ b/clear_zombies.py
@@ -0,0 +1,31 @@
+import sqlite3
+from datetime import datetime, timedelta
+
+DB_PATH = "/app/connector_queue.db"
+
+def clear_all_zombies():
+ print("π§Ή Cleaning up Zombie Jobs (PROCESSING for too long)...")
+ # A job that is PROCESSING for more than 10 minutes is likely dead
+ threshold = (datetime.utcnow() - timedelta(minutes=10)).strftime('%Y-%m-%d %H:%M:%S')
+
+ with sqlite3.connect(DB_PATH) as conn:
+ cursor = conn.cursor()
+
+ # 1. Identify Zombies
+ cursor.execute("SELECT id, updated_at FROM jobs WHERE status = 'PROCESSING' AND updated_at < ?", (threshold,))
+ zombies = cursor.fetchall()
+
+ if not zombies:
+ print("β
No zombies found.")
+ return
+
+ print(f"π΅οΈ Found {len(zombies)} zombie jobs.")
+ for zid, updated in zombies:
+ print(f" - Zombie ID {zid} (Last active: {updated})")
+
+ # 2. Kill them
+ cursor.execute("UPDATE jobs SET status = 'FAILED', error_msg = 'Zombie cleared: Process timed out' WHERE status = 'PROCESSING' AND updated_at < ?", (threshold,))
+ print(f"β
Successfully cleared {cursor.rowcount} zombie(s).")
+
+if __name__ == "__main__":
+ clear_all_zombies()
diff --git a/company-explorer/Dockerfile b/company-explorer/Dockerfile
index ee4e17e9..0d48c3b8 100644
--- a/company-explorer/Dockerfile
+++ b/company-explorer/Dockerfile
@@ -7,18 +7,20 @@ COPY frontend/ ./
RUN grep "ROBOTICS EDITION" src/App.tsx || echo "Version string not found in App.tsx"
RUN npm run build
-# --- STAGE 2: Backend & Runtime ---
+# --- STAGE 2: Backend Builder ---
+FROM python:3.11-slim AS backend-builder
+WORKDIR /app
+RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
+COPY requirements.txt .
+RUN pip install --user --no-cache-dir -r requirements.txt
+
+# --- STAGE 3: Final Runtime ---
FROM python:3.11-slim
WORKDIR /app
-# System Dependencies
-RUN apt-get update && apt-get install -y \
- build-essential \
- && rm -rf /var/lib/apt/lists/*
-
-# Copy Requirements & Install
-COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
+# Copy only installed packages from backend-builder
+COPY --from=backend-builder /root/.local /root/.local
+ENV PATH=/root/.local/bin:$PATH
# Copy Built Frontend from Stage 1 (To a safe location outside /app)
COPY --from=frontend-builder /build/dist /frontend_static
diff --git a/connector-superoffice/Dockerfile b/connector-superoffice/Dockerfile
index 6dbcf3c7..bf427087 100644
--- a/connector-superoffice/Dockerfile
+++ b/connector-superoffice/Dockerfile
@@ -1,15 +1,26 @@
-FROM python:3.11-slim
+# --- STAGE 1: Builder ---
+FROM python:3.11-slim AS builder
WORKDIR /app
-# Install system dependencies
+# Install system dependencies needed for building C-extensions
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
-# Install dependencies
+# Install dependencies into a local directory
COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
+RUN pip install --user --no-cache-dir -r requirements.txt
+
+# --- STAGE 2: Final Runtime ---
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Copy only the installed packages from builder
+COPY --from=builder /root/.local /root/.local
+# Update PATH to include the user-installed packages
+ENV PATH=/root/.local/bin:$PATH
# Copy source code
COPY . .
diff --git a/connector-superoffice/config.py b/connector-superoffice/config.py
index df3e3ed2..62492fe0 100644
--- a/connector-superoffice/config.py
+++ b/connector-superoffice/config.py
@@ -48,6 +48,19 @@ class Settings:
self.UDF_LAST_UPDATE = os.getenv("UDF_LAST_UPDATE", "SuperOffice:85")
self.UDF_LAST_OUTREACH = os.getenv("UDF_LAST_OUTREACH", "SuperOffice:88")
+ # --- User Whitelist (Roboplanet Associates) ---
+ # Includes both Numerical IDs and Shortnames for robustness
+ self.ROBOPLANET_WHITELIST = {
+ # IDs
+ 485, 454, 487, 515, 469, 528, 512, 465, 486, 493, 468, 476, 455, 483,
+ 492, 523, 470, 457, 498, 491, 464, 525, 527, 496, 490, 497, 456, 479,
+ # Shortnames
+ "RAAH", "RIAK", "RABA", "RJBU", "RPDU", "RCGO", "RBHA", "RAHE", "RPHO",
+ "RSHO", "RMJO", "DKE", "RAKI", "RSKO", "RMKR", "RSLU", "REME", "RNSL",
+ "RAPF", "ROBO", "RBRU", "RSSC", "RBSC", "RASC", "RKAB", "RDSE", "RSSH",
+ "RJST", "JUTH", "RSWA", "RCWE", "RJZH", "EVZ"
+ }
+
# Global instance
settings = Settings()
\ No newline at end of file
diff --git a/connector-superoffice/queue_manager.py b/connector-superoffice/queue_manager.py
index 83703f18..b76836ad 100644
--- a/connector-superoffice/queue_manager.py
+++ b/connector-superoffice/queue_manager.py
@@ -16,6 +16,7 @@ class JobQueue:
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT,
payload TEXT,
+ entity_name TEXT,
status TEXT DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -26,8 +27,15 @@ class JobQueue:
# Migration for existing DBs
try:
conn.execute("ALTER TABLE jobs ADD COLUMN next_try_at TIMESTAMP")
- except sqlite3.OperationalError:
- pass
+ except sqlite3.OperationalError: pass
+
+ try:
+ conn.execute("ALTER TABLE jobs ADD COLUMN entity_name TEXT")
+ except sqlite3.OperationalError: pass
+
+ try:
+ conn.execute("ALTER TABLE jobs ADD COLUMN associate_name TEXT")
+ except sqlite3.OperationalError: pass
def add_job(self, event_type: str, payload: dict):
with sqlite3.connect(DB_PATH) as conn:
@@ -36,6 +44,19 @@ class JobQueue:
(event_type, json.dumps(payload), 'PENDING')
)
+ def update_entity_name(self, job_id, name, associate_name=None):
+ with sqlite3.connect(DB_PATH) as conn:
+ if associate_name:
+ conn.execute(
+ "UPDATE jobs SET entity_name = ?, associate_name = ?, updated_at = datetime('now') WHERE id = ?",
+ (str(name), str(associate_name), job_id)
+ )
+ else:
+ conn.execute(
+ "UPDATE jobs SET entity_name = ?, updated_at = datetime('now') WHERE id = ?",
+ (str(name), job_id)
+ )
+
def get_next_job(self):
"""
Atomically fetches the next pending job where next_try_at is reached.
@@ -127,7 +148,7 @@ class JobQueue:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
- SELECT id, event_type, status, created_at, updated_at, error_msg, payload
+ SELECT id, event_type, status, created_at, updated_at, error_msg, payload, entity_name, associate_name
FROM jobs
ORDER BY updated_at DESC, created_at DESC
LIMIT ?
@@ -189,7 +210,8 @@ class JobQueue:
"entity_id": entity_id,
"contact_id": c_id,
"person_id": p_id,
- "name": "Unknown",
+ "name": job.get('entity_name') or "Unknown",
+ "associate": job.get('associate_name') or "",
"last_event": job['event_type'],
"status": job['status'],
"created_at": job['created_at'],
@@ -224,19 +246,26 @@ class JobQueue:
target_run["duration"] = f"{seconds}s" if seconds < 60 else f"{seconds // 60}m {seconds % 60}s"
except: pass
- # Resolve Name
+ # Resolve Name & Associate (if not already set from a newer job in this cluster)
if target_run["name"] == "Unknown":
- name = payload.get('Name') or payload.get('crm_name') or payload.get('FullName') or payload.get('ContactName')
+ name = job.get('entity_name') or payload.get('Name') or payload.get('crm_name') or payload.get('FullName') or payload.get('ContactName')
if not name and payload.get('Firstname'):
name = f"{payload.get('Firstname')} {payload.get('Lastname', '')}".strip()
if name: target_run["name"] = name
+ if not target_run["associate"] and job.get('associate_name'):
+ target_run["associate"] = job['associate_name']
+
+ # Update Status based on the jobs in the run
+
# Update Status based on the jobs in the run
# Priority: FAILED > PROCESSING > COMPLETED > SKIPPED > PENDING
status_priority = {"FAILED": 4, "PROCESSING": 3, "COMPLETED": 2, "SKIPPED": 1, "PENDING": 0}
current_prio = status_priority.get(target_run["status"], -1)
new_prio = status_priority.get(job["status"], -1)
+ # CRITICAL: We only update the status if the new job has a HIGHER priority
+ # Example: If current is COMPLETED (2) and new is SKIPPED (1), we keep COMPLETED.
if new_prio > current_prio:
target_run["status"] = job["status"]
target_run["error_msg"] = job["error_msg"]
@@ -244,12 +273,16 @@ class JobQueue:
# Set visual phases based on status
if job["status"] == "COMPLETED":
target_run["phases"] = {"received": "completed", "enriching": "completed", "syncing": "completed", "completed": "completed"}
- elif job["status"] == "SKIPPED" and current_prio < 2: # Don't downgrade from COMPLETED
- target_run["phases"] = {"received": "completed", "enriching": "completed", "syncing": "completed", "completed": "completed"}
elif job["status"] == "FAILED":
target_run["phases"] = {"received": "completed", "enriching": "failed", "syncing": "pending", "completed": "pending"}
elif job["status"] == "PROCESSING":
target_run["phases"] = {"received": "completed", "enriching": "processing", "syncing": "pending", "completed": "pending"}
+ # Note: SKIPPED (1) and PENDING (0) will use the target_run's initial phases or keep previous ones.
+
+ # SPECIAL CASE: If we already have COMPLETED but a new job is SKIPPED, we might want to keep the error_msg empty
+ # to avoid showing "Skipped Echo" on a successful row.
+ if target_run["status"] == "COMPLETED" and job["status"] == "SKIPPED":
+ pass # Keep everything from the successful run
# Final cleanup
for r in runs:
diff --git a/connector-superoffice/superoffice_client.py b/connector-superoffice/superoffice_client.py
index 2ed19aa5..55235832 100644
--- a/connector-superoffice/superoffice_client.py
+++ b/connector-superoffice/superoffice_client.py
@@ -168,7 +168,9 @@ class SuperOfficeClient:
data = resp.json()
all_results.extend(data.get('value', []))
- next_page_url = data.get('next_page_url', None)
+
+ # Robust Pagination: Check both OData standard and legacy property
+ next_page_url = data.get('odata.nextLink') or data.get('next_page_url')
except requests.exceptions.HTTPError as e:
logger.error(f"β API Search Error for {query_string}: {e.response.text}")
diff --git a/connector-superoffice/tools/blind_check_associates.py b/connector-superoffice/tools/blind_check_associates.py
new file mode 100644
index 00000000..e80f82cf
--- /dev/null
+++ b/connector-superoffice/tools/blind_check_associates.py
@@ -0,0 +1,45 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def blind_check():
+ print("π΅οΈ Testing Manuel's Filter: contactAssociate/contactFullName eq 'RoboPlanet GmbH'")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # Manuel's filter logic with Count
+ endpoint = "Contact?$filter=contactAssociate/contactFullName eq 'RoboPlanet GmbH'&$top=0&$count=true"
+
+ print(f"π‘ Querying: {endpoint}")
+ try:
+ resp = client._get(endpoint)
+ count = resp.get('@odata.count')
+ print(f"\nπ― RESULT: Manuel's Filter found {count} accounts.")
+
+ if count == 17014:
+ print("β
PERFECT MATCH! Manuel's filter matches your UI count exactly.")
+ else:
+ print(f"βΉοΈ Delta to UI: {17014 - (count or 0)}")
+
+ except Exception as e:
+ print(f"β Manuel's filter failed: {e}")
+ # Try without spaces encoded
+ print("Trying with encoded spaces...")
+ try:
+ endpoint_enc = "Contact?$filter=contactAssociate/contactFullName eq 'RoboPlanet+GmbH'&$top=0&$count=true"
+ resp = client._get(endpoint_enc)
+ print(f"π― Encoded Result: {resp.get('@odata.count')}")
+ except:
+ pass
+
+if __name__ == "__main__":
+ blind_check()
diff --git a/connector-superoffice/tools/check_contact_associate.py b/connector-superoffice/tools/check_contact_associate.py
new file mode 100644
index 00000000..0583a0ae
--- /dev/null
+++ b/connector-superoffice/tools/check_contact_associate.py
@@ -0,0 +1,47 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def check_associate_details():
+ print("π Checking Associate Details in Contact Record...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # Use our known test company (if it still exists - oh wait, we deleted it!)
+ # We need to find ANY contact.
+
+ # Search for any contact
+ print("Searching for a contact...")
+ contacts = client.search("Contact?$top=1")
+
+ if contacts:
+ cid = contacts[0].get('contactId') or contacts[0].get('ContactId')
+ print(f"β
Found Contact ID: {cid}")
+
+ # Fetch Full Details
+ print("Fetching details...")
+ details = client.get_contact(cid)
+
+ assoc = details.get('Associate')
+ print("--- Associate Object ---")
+ print(json.dumps(assoc, indent=2))
+
+ if assoc and 'GroupIdx' in assoc:
+ print(f"β
SUCCESS: GroupIdx is available: {assoc['GroupIdx']}")
+ else:
+ print("β FAILURE: GroupIdx is MISSING in Contact details.")
+
+ else:
+ print("β No contacts found in system.")
+
+if __name__ == "__main__":
+ check_associate_details()
diff --git a/connector-superoffice/tools/check_filter_counts.py b/connector-superoffice/tools/check_filter_counts.py
new file mode 100644
index 00000000..40ccc537
--- /dev/null
+++ b/connector-superoffice/tools/check_filter_counts.py
@@ -0,0 +1,38 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def check_counts():
+ print("π Verifying Filter Logic via OData Search...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # Simplified OData Search
+ # We ask for top=1 but want the total count
+ endpoint = "Contact?$filter=name contains 'GmbH'&$top=1&$select=Associate"
+
+ print(f"π‘ Querying: {endpoint}")
+ try:
+ resp = client._get(endpoint)
+ print("--- RAW RESPONSE START ---")
+ print(json.dumps(resp, indent=2))
+ print("--- RAW RESPONSE END ---")
+
+ except Exception as e:
+ print(f"β Error: {e}")
+
+if __name__ == "__main__":
+ check_counts()
+
+
+if __name__ == "__main__":
+ check_counts()
diff --git a/connector-superoffice/tools/check_selection_members.py b/connector-superoffice/tools/check_selection_members.py
new file mode 100644
index 00000000..5c996277
--- /dev/null
+++ b/connector-superoffice/tools/check_selection_members.py
@@ -0,0 +1,52 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def check_selection():
+ selection_id = 10960
+ print(f"π Inspecting Selection {selection_id} (Alle_Contacts_Roboplanet)...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # 1. Get Selection Metadata
+ print("\nπ Fetching Selection Details...")
+ details = client._get(f"Selection/{selection_id}")
+ if details:
+ print(f" Name: {details.get('Name')}")
+ print(f" Description: {details.get('Description')}")
+ print(f" Type: {details.get('SelectionType')}") # e.g. Dynamic, Static
+
+ # 2. Fetch Members via direct Selection endpoint
+ print("\nπ₯ Fetching first 10 Members via direct Selection endpoint...")
+ # Direct endpoint for Contact members of a selection
+ endpoint = f"Selection/{selection_id}/ContactMembers?$top=10"
+
+ try:
+ members_resp = client._get(endpoint)
+ # OData usually returns a 'value' list
+ members = members_resp.get('value', []) if isinstance(members_resp, dict) else members_resp
+
+ if members and isinstance(members, list):
+ print(f"β
Found {len(members)} members in first page:")
+ for m in members:
+ # Structure might be flat or nested
+ name = m.get('Name') or m.get('name')
+ cid = m.get('ContactId') or m.get('contactId')
+ print(f" - {name} (ContactID: {cid})")
+ else:
+ print("β οΈ No members found or response format unexpected.")
+ print(f"DEBUG: {json.dumps(members_resp, indent=2)}")
+ except Exception as e:
+ print(f"β Direct Selection members query failed: {e}")
+
+if __name__ == "__main__":
+ check_selection()
diff --git a/connector-superoffice/tools/check_selections_and_associates.py b/connector-superoffice/tools/check_selections_and_associates.py
new file mode 100644
index 00000000..b511cff4
--- /dev/null
+++ b/connector-superoffice/tools/check_selections_and_associates.py
@@ -0,0 +1,62 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def run_discovery():
+ print("π Discovery: Searching for Selections and Associate Mapping...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # 1. Search for Selections
+ print("\nπ Searching for 'Roboplanet' Selections...")
+ # Selections can be found via Archive or direct endpoint
+ selections = client.search("Selection?$filter=name contains 'Roboplanet'")
+ if selections:
+ print(f"β
Found {len(selections)} matching selections:")
+ for sel in selections:
+ sid = sel.get('SelectionId') or sel.get('selectionId')
+ name = sel.get('Name') or sel.get('name')
+ print(f" - {name} (ID: {sid})")
+ else:
+ print("β οΈ No selections found with name 'Roboplanet'.")
+
+ # 2. Get Associate Mapping via Archive Provider
+ # This avoids the Associate/{id} 500 error
+ print("\nπ₯ Fetching Associate-to-Group mapping via Archive...")
+ # Provider 'associate' is standard
+ endpoint = "Archive/dynamic?provider=associate&columns=associateId,name,groupIdx"
+ try:
+ mapping_data = client._get(endpoint)
+ if mapping_data and isinstance(mapping_data, list):
+ print(f"β
Received {len(mapping_data)} associate records.")
+ robo_user_ids = []
+ for item in mapping_data:
+ aid = item.get("associateId")
+ name = item.get("name")
+ gid = item.get("groupIdx")
+
+ if gid == 52:
+ print(f" - [ROBO] {name} (ID: {aid}, Group: {gid})")
+ robo_user_ids.append(aid)
+ elif "Fottner" in str(name) or aid == 321:
+ print(f" - [EXCLUDE] {name} (ID: {aid}, Group: {gid})")
+
+ print(f"\nπ Identified {len(robo_user_ids)} Roboplanet Users.")
+ if robo_user_ids:
+ print(f"List of IDs: {robo_user_ids}")
+ else:
+ print("β Archive query returned no associate mapping.")
+ except Exception as e:
+ print(f"β Archive query failed: {e}")
+
+if __name__ == "__main__":
+ run_discovery()
diff --git a/connector-superoffice/tools/count_roboplanet_total.py b/connector-superoffice/tools/count_roboplanet_total.py
new file mode 100644
index 00000000..832b56ca
--- /dev/null
+++ b/connector-superoffice/tools/count_roboplanet_total.py
@@ -0,0 +1,69 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+from config import settings
+
+def verify_total_counts():
+ print("π Verifying Global Account Counts...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ whitelist = settings.ROBOPLANET_WHITELIST
+
+ # 1. Try to get MemberCount from the Selection 10960 directly
+ print("\nπ Checking Selection 10960 (Alle_Contacts_Roboplanet)...")
+ try:
+ sel_details = client._get("Selection/10960")
+ if sel_details:
+ # Note: MemberCount is often a property of the Selection entity
+ count = sel_details.get("MemberCount")
+ print(f" πΉ Web-Interface-equivalent Count (MemberCount): {count}")
+ except Exception as e:
+ print(f" β οΈ Could not fetch Selection count property: {e}")
+
+ # 2. Manual Aggregate Count via OData
+ # We construct a filter for all our IDs and Shortnames
+ # This might be too long for a URL, so we do it in smaller batches if needed
+ print("\nπ‘ Calculating Netto Count for Whitelist (IDs + Names)...")
+
+ # Divide whitelist into IDs and Names
+ ids = [x for x in whitelist if isinstance(x, int)]
+ names = [x for x in whitelist if isinstance(x, str)]
+
+ # Construct OData filter string
+ # example: (associateId eq 528 or associateId eq 485 or associateId eq 'RKAB')
+ id_filters = [f"associateId eq {i}" for i in ids]
+ name_filters = [f"associateId eq '{n}'" for n in names]
+ full_filter = " or ".join(id_filters + name_filters)
+
+ # We use $top=0 and $count=true to get JUST the number
+ endpoint = f"Contact?$filter={full_filter}&$top=0&$count=true"
+
+ try:
+ # Note: If the URL is too long (> 2000 chars), this might fail.
+ # But for ~60 entries it should be fine.
+ resp = client._get(endpoint)
+ total_api_count = resp.get("@odata.count")
+ print(f" π― API Calculated Count (Whitelist-Match): {total_api_count}")
+
+ if total_api_count is not None:
+ print(f"\nβ
PROOF: The API identifies {total_api_count} accounts for Roboplanet.")
+ print("π Bitte vergleiche diese Zahl mit der Selektion 'Alle_Contacts_Roboplanet' im SuperOffice Web-Interface.")
+ else:
+ print("β API did not return a count property.")
+
+ except Exception as e:
+ print(f"β OData Aggregation failed: {e}")
+ print(" The filter string might be too long for the API.")
+
+if __name__ == "__main__":
+ verify_total_counts()
diff --git a/connector-superoffice/tools/debug_names.py b/connector-superoffice/tools/debug_names.py
new file mode 100644
index 00000000..0358258b
--- /dev/null
+++ b/connector-superoffice/tools/debug_names.py
@@ -0,0 +1,35 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def debug_names():
+ print("π Debugging Associate Names...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ endpoint = "Contact?$orderby=contactId desc&$top=5&$select=name,Associate/Name"
+
+ print(f"π‘ Querying: {endpoint}")
+ contacts = client.search(endpoint)
+
+ if contacts:
+ for c in contacts:
+ cname = c.get('name')
+ assoc = c.get('Associate') or {}
+ aname = assoc.get('Name')
+ print(f" π’ Contact: {cname}")
+ print(f" π Associate Name: '{aname}'")
+ else:
+ print("β No contacts found.")
+
+if __name__ == "__main__":
+ debug_names()
diff --git a/connector-superoffice/tools/discover_associates.py b/connector-superoffice/tools/discover_associates.py
new file mode 100644
index 00000000..32339422
--- /dev/null
+++ b/connector-superoffice/tools/discover_associates.py
@@ -0,0 +1,66 @@
+import sys
+import os
+import json
+
+# Absolute path setup to avoid import errors
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def discover_associates_and_groups():
+ print("π Discovering Associates and Groups...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # 1. Fetch User Groups
+ print("\nπ₯ Fetching User Groups...")
+ groups = client._get("MDOList/usergroup")
+
+ robo_group_id = None
+
+ if groups:
+ for group in groups:
+ name = group.get('Name')
+ grp_id = group.get('Id')
+ print(f" - Group: {name} (ID: {grp_id})")
+ if "Roboplanet" in name:
+ robo_group_id = grp_id
+
+ if robo_group_id:
+ print(f"β
Identified Roboplanet Group ID: {robo_group_id}")
+ else:
+ print("β οΈ Could not auto-identify Roboplanet group. Check the list above.")
+
+ # 2. Check Candidate IDs directly
+ print("\nπ€ Checking specific Person IDs for Willi Fottner...")
+ candidates = [6, 182552]
+
+ for pid in candidates:
+ try:
+ p = client.get_person(pid)
+ if p:
+ fname = p.get('Firstname')
+ lname = p.get('Lastname')
+ is_assoc = p.get('IsAssociate')
+
+ print(f" π Person {pid}: {fname} {lname} (IsAssociate: {is_assoc})")
+
+ if is_assoc:
+ assoc_obj = p.get("Associate")
+ if assoc_obj:
+ assoc_id = assoc_obj.get("AssociateId")
+ grp = assoc_obj.get("GroupIdx")
+ print(f" β
IS ASSOCIATE! ID: {assoc_id}, Group: {grp}")
+ if "Fottner" in str(lname) or "Willi" in str(fname):
+ print(f" π― TARGET IDENTIFIED: Willi Fottner is Associate ID {assoc_id}")
+ except Exception as e:
+ print(f" β Error checking Person {pid}: {e}")
+
+ print("\n--- Done ---")
+
+if __name__ == "__main__":
+ discover_associates_and_groups()
diff --git a/connector-superoffice/tools/final_vertical_discovery.py b/connector-superoffice/tools/final_vertical_discovery.py
new file mode 100644
index 00000000..2c1575f8
--- /dev/null
+++ b/connector-superoffice/tools/final_vertical_discovery.py
@@ -0,0 +1,66 @@
+import sys
+import os
+import json
+
+# Absolute path to the connector-superoffice directory
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+
+# CRITICAL: Insert at 0 to shadow /app/config.py
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def discover_verticals():
+ print("π Starting Final Vertical Discovery (Production)...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # 1. Fetch Contact UDF Layout to find the List ID behind SuperOffice:83
+ print("π‘ Fetching Contact UDF Layout (Metadata)...")
+ layout = client._get("Contact/UdefLayout/Published")
+
+ list_id = None
+ if layout and 'Fields' in layout:
+ for field in layout['Fields']:
+ if field.get('ProgId') == 'SuperOffice:83':
+ print(f"β
Found SuperOffice:83: {field.get('Label')}")
+ list_id = field.get('ListId')
+ print(f"β
List ID: {list_id}")
+ break
+
+ if not list_id:
+ print("β Could not find Metadata for SuperOffice:83.")
+ return
+
+ # 2. Fetch the List Items for this List
+ print(f"π‘ Fetching List Items for List ID {list_id}...")
+ # List endpoint is typically List/ListId/Items
+ # Let's try to get all rows for this list
+ items = client._get(f"List/{list_id}/Items")
+
+ if items:
+ print(f"β
SUCCESS! Found {len(items)} items in the Vertical list.")
+ mapping = {}
+ for item in items:
+ name = item.get('Value') or item.get('Name')
+ item_id = item.get('Id')
+ mapping[name] = item_id
+ print(f" - {name}: {item_id}")
+
+ print("\nπ FINAL MAPPING JSON (Copy to .env VERTICAL_MAP_JSON):")
+ print(json.dumps(mapping))
+ else:
+ print(f"β Could not fetch items for List {list_id}. Trying MDO List...")
+ # Fallback to MDO List
+ mdo_items = client._get(f"MDOList/udlist{list_id}")
+ if mdo_items:
+ print("β
Success via MDO List.")
+ # ... process mdo items if needed ...
+ else:
+ print("β MDO List fallback failed too.")
+
+if __name__ == "__main__":
+ discover_verticals()
diff --git a/connector-superoffice/tools/find_latest_roboplanet_account.py b/connector-superoffice/tools/find_latest_roboplanet_account.py
new file mode 100644
index 00000000..a54b650c
--- /dev/null
+++ b/connector-superoffice/tools/find_latest_roboplanet_account.py
@@ -0,0 +1,41 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def find_latest_roboplanet():
+ print("π Searching for the latest Roboplanet (Group 52) Account...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # DIAGNOSTIC: Search for Account of Associate 528 (RCGO)
+ endpoint = "Contact?$filter=associateId eq 528&$orderby=contactId desc&$top=1&$select=contactId,name,Associate"
+
+ print(f"π‘ Diagnostic Query: {endpoint}")
+
+ try:
+ results = client.search(endpoint)
+
+ if results and len(results) > 0:
+ contact = results[0]
+ print("\nβ
FOUND ACCOUNT FOR RCGO (528):")
+ print(json.dumps(contact, indent=2))
+
+ # Check GroupIdx
+ # Usually flat like "Associate": {"GroupIdx": 52...}
+ else:
+ print("\nβ NO ACCOUNTS FOUND for RCGO (528).")
+
+ except Exception as e:
+ print(f"β Error: {e}")
+
+if __name__ == "__main__":
+ find_latest_roboplanet()
diff --git a/connector-superoffice/tools/find_missing_whitelist_ids.py b/connector-superoffice/tools/find_missing_whitelist_ids.py
new file mode 100644
index 00000000..3b84bdc8
--- /dev/null
+++ b/connector-superoffice/tools/find_missing_whitelist_ids.py
@@ -0,0 +1,60 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+from config import settings
+
+def find_missing():
+ print("π Scanning for Associate IDs not in Whitelist...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ whitelist = settings.ROBOPLANET_WHITELIST
+
+ # Fetch 500 contacts
+ limit = 500
+ endpoint = f"Contact?$orderby=contactId desc&$top={limit}&$select=associateId"
+
+ print(f"π‘ Scanning {limit} records...")
+ contacts = client.search(endpoint)
+
+ if contacts:
+ missing_ids = set()
+ match_count = 0
+ for c in contacts:
+ aid = c.get('associateId') or c.get('AssociateId')
+ if aid:
+ is_match = False
+ if str(aid).upper() in whitelist: is_match = True
+ try:
+ if int(aid) in whitelist: is_match = True
+ except: pass
+
+ if is_match:
+ match_count += 1
+ else:
+ missing_ids.add(aid)
+
+ print(f"\nπ Scan Results ({limit} records):")
+ print(f" - Total Matches (Roboplanet): {match_count}")
+ print(f" - Missing/Other IDs: {len(missing_ids)}")
+
+ if missing_ids:
+ print("\nβ
Found IDs NOT in whitelist:")
+ for mid in sorted(list(missing_ids), key=lambda x: str(x)):
+ print(f" - {mid}")
+
+ print("\nπ Bitte prΓΌfe, ob eine dieser IDs ebenfalls zu Roboplanet gehΓΆrt.")
+ else:
+ print("β No contacts found.")
+
+if __name__ == "__main__":
+ find_missing()
diff --git a/connector-superoffice/tools/inspect_group_users.py b/connector-superoffice/tools/inspect_group_users.py
new file mode 100644
index 00000000..a7948e5b
--- /dev/null
+++ b/connector-superoffice/tools/inspect_group_users.py
@@ -0,0 +1,98 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def inspect_group():
+ print("π Inspecting Group 52 (Roboplanet)...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # 1. Find Users in Group 52
+ print("\nπ₯ Finding Associates in Group 52...")
+ associates = client._get("MDOList/associate")
+
+ robo_associates = []
+ if associates:
+ for assoc in associates:
+ # Note: MDOList returns flat items.
+ # We might need to fetch details or check 'GroupIdx' if present in ExtraInfo
+ # Let's check keys first
+ # print(assoc.keys())
+
+ # The 'GroupIdx' is usually in 'ExtraInfo' or needs detail fetch
+ # But earlier discovery showed 'GroupIdx' directly? No, I inferred it.
+ # Let's fetch details for a few to be sure.
+ assoc_id = assoc.get('Id')
+
+ # Optimization: Only check first 50 to avoid spam, or check by Name if we know one
+ # Better: Use OData to filter associates by group?
+ # "Associate?$filter=groupIdx eq 52" -> Let's try this first!
+ pass
+
+ # Efficient OData Search for Associates in Group 52
+ users_in_group = client.search("Associate?$filter=groupIdx eq 52")
+
+ if users_in_group:
+ print(f"β
Found {len(users_in_group)} Associates in Group 52:")
+ for u in users_in_group:
+ uid = u.get('associateId') or u.get('AssociateId')
+ name = u.get('name') or u.get('Name') or u.get('fullName')
+ print(f" - {name} (ID: {uid})")
+ robo_associates.append(uid)
+ else:
+ print("β οΈ No Associates found in Group 52 via OData.")
+ print(" Trying manual scan of MDOList (slower)...")
+ # Fallback loop
+ if associates:
+ count = 0
+ for assoc in associates:
+ aid = assoc.get('Id')
+ det = client._get(f"Associate/{aid}")
+ if det and det.get('GroupIdx') == 52:
+ print(f" - {det.get('Name')} (ID: {aid}) [via Detail]")
+ robo_associates.append(aid)
+ count += 1
+ if count > 5:
+ print(" ... (stopping scan)")
+ break
+
+ if not robo_associates:
+ print("β CRITICAL: Group 52 seems empty! Filter logic will block everything.")
+ return
+
+ # 2. Check a Contact owned by one of these users
+ test_user_id = robo_associates[0]
+ print(f"\nπ’ Checking a Contact owned by User {test_user_id}...")
+
+ contacts = client.search(f"Contact?$filter=associateId eq {test_user_id}&$top=1&$select=ContactId,Name,Associate/GroupIdx")
+
+ if contacts:
+ c = contacts[0]
+ cid = c.get('contactId') or c.get('ContactId')
+ cname = c.get('name') or c.get('Name')
+ # Check nested Associate GroupIdx if returned, or fetch detail
+ print(f" found: {cname} (ID: {cid})")
+
+ # Double Check with full Get
+ full_c = client.get_contact(cid)
+ assoc_grp = full_c.get('Associate', {}).get('GroupIdx')
+ print(f" π Contact Associate GroupIdx: {assoc_grp}")
+
+ if assoc_grp == 52:
+ print("β
VERIFIED: Filter logic 'GroupIdx == 52' will work.")
+ else:
+ print(f"β MISMATCH: Contact GroupIdx is {assoc_grp}, expected 52.")
+ else:
+ print("β οΈ User has no contacts. Cannot verify contact group mapping.")
+
+if __name__ == "__main__":
+ inspect_group()
diff --git a/connector-superoffice/tools/precise_count_verification.py b/connector-superoffice/tools/precise_count_verification.py
new file mode 100644
index 00000000..cdc487b5
--- /dev/null
+++ b/connector-superoffice/tools/precise_count_verification.py
@@ -0,0 +1,73 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+from config import settings
+
+def run_precise_check():
+ print("π Precise Count Verification: API vs. Whitelist...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ whitelist = settings.ROBOPLANET_WHITELIST
+ ids_in_whitelist = [x for x in whitelist if isinstance(x, int)]
+
+ # 1. Individual Counts for our Whitelist IDs
+ print(f"\nπ’ Counting accounts for the {len(ids_in_whitelist)} IDs in whitelist...")
+ total_whitelist_count = 0
+ for aid in ids_in_whitelist:
+ endpoint = f"Contact?$filter=associateId eq {aid}&$top=0&$count=true"
+ try:
+ resp = client._get(endpoint)
+ count = resp.get('@odata.count') or 0
+ if count > 0:
+ # print(f" - ID {aid}: {count}")
+ total_whitelist_count += count
+ except:
+ pass
+
+ print(f"β
Total accounts owned by Whitelist IDs: {total_whitelist_count}")
+
+ # 2. Check for "Strangers" in the Selection 10960
+ # We want to find who else is in that selection
+ print(f"\nπ΅οΈ Looking for Owners in Selection 10960 who are NOT in our whitelist...")
+
+ # We use Archive/dynamic to group members by AssociateId
+ # This is the most efficient way to see all owners in the selection
+ endpoint = "Archive/dynamic?provider=selectionmember&columns=contact/associateId,contact/associate/name&criteria=selectionId=10960&$top=1000"
+
+ try:
+ members = client._get(endpoint)
+ if members and isinstance(members, list):
+ owners_in_selection = {}
+ for m in members:
+ aid = m.get("contact/associateId")
+ aname = m.get("contact/associate/name")
+ if aid:
+ owners_in_selection[aid] = aname
+
+ print(f"Found {len(owners_in_selection)} distinct owners in the first 1000 members of selection.")
+ for aid, name in owners_in_selection.items():
+ if aid not in whitelist and name not in whitelist:
+ print(f" β οΈ OWNER NOT IN WHITELIST: {name} (ID: {aid})")
+ else:
+ print("β οΈ Could not group selection members by owner via API.")
+
+ except Exception as e:
+ print(f"β οΈ Archive grouping failed: {e}")
+
+ print(f"\nπ Target from UI: 17014")
+ print(f"π Whitelist sum: {total_whitelist_count}")
+ delta = 17014 - total_whitelist_count
+ print(f"π Delta: {delta}")
+
+if __name__ == "__main__":
+ run_precise_check()
diff --git a/connector-superoffice/tools/test_selection_membership.py b/connector-superoffice/tools/test_selection_membership.py
new file mode 100644
index 00000000..9fbbdfb1
--- /dev/null
+++ b/connector-superoffice/tools/test_selection_membership.py
@@ -0,0 +1,41 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+
+def test_membership(contact_id: int):
+ selection_id = 10960
+ print(f"π Testing if Contact {contact_id} is member of Selection {selection_id}...")
+ client = SuperOfficeClient()
+
+ # Efficient Membership Check
+ # GET Selection/{id}/MemberStatus/Contact/{contactId}
+ endpoint = f"Selection/{selection_id}/MemberStatus/Contact/{contact_id}"
+
+ print(f"π‘ Querying: {endpoint}")
+ try:
+ resp = client._get(endpoint)
+ print(f"β
Response: {json.dumps(resp, indent=2)}")
+
+ # Result format is usually a string: "Member", "NotMember", "Excluded"
+ if resp == "Member":
+ print("π― YES: Contact is a member.")
+ else:
+ print("βοΈ NO: Contact is NOT a member.")
+
+ except Exception as e:
+ print(f"β Membership check failed: {e}")
+
+if __name__ == "__main__":
+ # Test with Tanja Ullmann (171188) which we identified as Roboplanet
+ test_membership(171188)
+
+ # Test with Wackler parent (ID 3)
+ print("\n--- Control Test ---")
+ test_membership(3)
diff --git a/connector-superoffice/tools/verify_latest_roboplanet.py b/connector-superoffice/tools/verify_latest_roboplanet.py
new file mode 100644
index 00000000..6ab396e8
--- /dev/null
+++ b/connector-superoffice/tools/verify_latest_roboplanet.py
@@ -0,0 +1,80 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+from config import settings
+
+def find_latest_match():
+ print("π Searching for the youngest account assigned to a Roboplanet user...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ whitelist = settings.ROBOPLANET_WHITELIST
+ print(f"π Whitelist contains {len(whitelist)} entries (IDs + Names).")
+
+ # 1. Fetch more contacts to find a match
+ limit = 1000
+ endpoint = f"Contact?$orderby=contactId desc&$top={limit}&$select=contactId,name,associateId"
+
+ print(f"π‘ Fetching latest {limit} contacts (this may take a few seconds)...")
+ try:
+ contacts = client.search(endpoint)
+ if not contacts:
+ print("β No contacts returned from API.")
+ return
+
+ print(f"β
Received {len(contacts)} contacts. Checking against whitelist...")
+
+ found = False
+ for i, c in enumerate(contacts):
+ if i > 0 and i % 100 == 0:
+ print(f" ... checked {i} records ...")
+
+ cid = c.get('contactId') or c.get('ContactId')
+ cname = c.get('name') or c.get('Name')
+
+ # Extract associate identifier (might be ID or Name)
+ raw_aid = c.get('associateId') or c.get('AssociateId')
+
+ is_match = False
+ if raw_aid:
+ # 1. Try as String (Name)
+ val_str = str(raw_aid).upper().strip()
+ if val_str in whitelist:
+ is_match = True
+ else:
+ # 2. Try as Int (ID)
+ try:
+ if int(raw_aid) in whitelist:
+ is_match = True
+ except (ValueError, TypeError):
+ pass
+
+ if is_match:
+ print("\nπ― FOUND YOUNGEST ROBOPLANET ACCOUNT:")
+ print(f" - Company Name: {cname}")
+ print(f" - Contact ID: {cid}")
+ print(f" - Responsible Identifier: {raw_aid}")
+ print(f" - Link: https://online3.superoffice.com/Cust26720/default.aspx?contact?contact_id={cid}")
+ found = True
+ break
+
+ if not found:
+ print(f"\nβ οΈ No match found in the last {limit} contacts.")
+ print(" This confirms that recent activity is from non-whitelist users.")
+
+ except Exception as e:
+ print(f"β Error: {e}")
+ import traceback
+ traceback.print_exc()
+
+if __name__ == "__main__":
+ find_latest_match()
diff --git a/connector-superoffice/tools/verify_selection_members_directly.py b/connector-superoffice/tools/verify_selection_members_directly.py
new file mode 100644
index 00000000..d30abfaf
--- /dev/null
+++ b/connector-superoffice/tools/verify_selection_members_directly.py
@@ -0,0 +1,67 @@
+import sys
+import os
+import json
+
+# Absolute path setup
+current_dir = os.path.dirname(os.path.abspath(__file__))
+connector_dir = os.path.abspath(os.path.join(current_dir, '..'))
+sys.path.insert(0, connector_dir)
+
+from superoffice_client import SuperOfficeClient
+from config import settings
+
+def verify():
+ selection_id = 10960
+ print(f"π Verifying members of Selection {selection_id}...")
+ client = SuperOfficeClient()
+ if not client.access_token:
+ print("β Auth failed.")
+ return
+
+ # Use the Selection/ID/ContactMembers endpoint which is part of the REST API
+ # We ask for a few members and their Associate info
+ endpoint = f"Selection/{selection_id}/ContactMembers?$top=50&$select=ContactId,Name,AssociateId"
+
+ print(f"π‘ Querying: {endpoint}")
+ try:
+ resp = client._get(endpoint)
+ # OData returns 'value'
+ members = resp.get('value', [])
+
+ if not members:
+ print("β οΈ No members found via REST. Trying alternative Archive call...")
+ # If REST fails, we might have to use a different approach
+ return
+
+ print(f"β
Found {len(members)} members. Inspecting owners...")
+
+ whitelist = settings.ROBOPLANET_WHITELIST
+ owners_found = {}
+
+ for m in members:
+ cid = m.get('ContactId')
+ cname = m.get('Name')
+ # The AssociateId might be named differently in the response
+ aid = m.get('AssociateId')
+
+ if aid:
+ is_robo = aid in whitelist or str(aid).upper() in whitelist
+ status = "β
ROBO" if is_robo else "β STRANGER"
+ owners_found[aid] = (status, aid)
+ # print(f" - Contact {cid} ({cname}): Owner {aid} [{status}]")
+
+ print("\nπ Summary of Owners in Selection:")
+ for aid, (status, val) in owners_found.items():
+ print(f" {status}: Associate {aid}")
+
+ if any("STRANGER" in s for s, v in owners_found.values()):
+ print("\nβ οΈ ALERT: Found owners in the selection who are NOT in our whitelist.")
+ print("This explains the delta. Please check if these IDs should be added.")
+ else:
+ print("\nβ
All sampled members belong to whitelist users.")
+
+ except Exception as e:
+ print(f"β Error: {e}")
+
+if __name__ == "__main__":
+ verify()
diff --git a/connector-superoffice/webhook_app.py b/connector-superoffice/webhook_app.py
index 6e51cb5c..1684e4d7 100644
--- a/connector-superoffice/webhook_app.py
+++ b/connector-superoffice/webhook_app.py
@@ -147,6 +147,7 @@ def dashboard():
Account / Person
+ Responsible
ID
Process Progress
Duration
@@ -156,7 +157,7 @@ def dashboard():