Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ def cache_delete(key: str):
if client:
try:
client.delete(key)
except Exception:
pass
except Exception as e:
logger.warning(f"Redis delete error for {key}: {e}")

if key in _local_cache:
del _local_cache[key]
Expand Down
17 changes: 14 additions & 3 deletions mira_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ def _parse_timestamp(value: Any) -> datetime | None:
if dt.tzinfo is None:
dt = dt.replace(tzinfo=UTC)
return dt.astimezone(UTC)
except Exception:
except Exception as e:
struct_logger.warning(
"mira timestamp parse failed", value=value, error=str(e)
)
return None
return None

Expand All @@ -109,7 +112,12 @@ def _count_collection_by_status(collection_name: str, status_field: str = "statu
docs = db.collection(collection_name).stream()
statuses = [doc.to_dict().get(status_field, "UNKNOWN") for doc in docs]
return dict(Counter(statuses))
except Exception:
except Exception as e:
struct_logger.warning(
"mira collection status count failed",
collection=collection_name,
error=str(e),
)
return {"UNKNOWN": 0}


Expand Down Expand Up @@ -609,7 +617,10 @@ async def mira_firestore_collections(request: Request, limit: int = 1000) -> dic
try:
count = len(list(col.limit(limit).stream()))
result.append({"collection": col.id, "count": count})
except Exception:
except Exception as e:
struct_logger.warning(
"mira collection count failed", collection=col.id, error=str(e)
)
result.append({"collection": col.id, "count": None})
return {
"status": "healthy",
Expand Down
15 changes: 11 additions & 4 deletions seo_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import html
import json
import logging
import os
import re
import threading
Expand All @@ -44,6 +45,8 @@

router = APIRouter()

logger = logging.getLogger(__name__)

# ── Wiring (set by main.py at startup) ─────────────────────────────────────

_get_homes = None # callable -> list[dict]; the merged public inventory
Expand Down Expand Up @@ -197,8 +200,9 @@ def _safe_homes() -> list[dict]:
return []
try:
return _get_homes() or []
except Exception:
except Exception as e:
# SEO surface must never take the page down with it.
logger.warning(f"SEO _safe_homes: inventory fetch failed, serving empty: {e}")
return []


Expand Down Expand Up @@ -353,7 +357,8 @@ def _product_jsonld(home: dict, canonical_url: str) -> dict | None:
price = home.get("price_value")
try:
price = float(price) if price is not None else None
except (TypeError, ValueError):
except (TypeError, ValueError) as e:
logger.warning(f"SEO _product_jsonld: unparseable price_value {price!r}, omitting Product JSON-LD: {e}")
price = None
if not (price and price > 0):
return None
Expand Down Expand Up @@ -416,7 +421,8 @@ def _shell() -> str:
content = f.read()
_shell_cache = (mtime, content)
return content
except OSError:
except OSError as e:
logger.warning(f"SEO _shell: cannot read index shell {_index_html_path!r}, using minimal fallback: {e}")
return '<!doctype html><html><head><title></title></head><body><div id="root"></div></body></html>'


Expand Down Expand Up @@ -1047,7 +1053,8 @@ def render_spa_response(full_path: str) -> Response | None:
to its default file handling. Never raises."""
try:
return _render_spa_response(full_path)
except Exception:
except Exception as e:
logger.warning(f"SEO render_spa_response: rendering failed for {full_path!r}, falling through: {e}")
return None


Expand Down
48 changes: 48 additions & 0 deletions tests/test_contact_lead_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,51 @@ async def boom(_lead):
assert body["success"] is True
# ... but the dropped lead is now loud + alertable.
assert "lead_storage_failed" in body.get("warnings", [])


def test_contact_creates_lead_with_name_phone_and_source(monkeypatch):
"""A valid name+phone POST persists a Lead carrying those fields + source."""
client, main, *_ = create_client(monkeypatch)
before = len(main.lead_manager.leads)

body = _post(
client, name="Carol", phone="(281) 324-3020", email="carol@example.com"
).json()
assert body["success"] is True

# FakeLeadManager.create_lead appends the persisted Lead to .leads.
assert len(main.lead_manager.leads) == before + 1
created = main.lead_manager.leads[-1]
assert created.name == "Carol"
assert created.phone == "(281) 324-3020"
assert created.email == "carol@example.com"
assert created.source == "contact_form" # default source for the contact form


def test_contact_lead_carries_explicit_source(monkeypatch):
"""A caller-supplied `source` flows through to the persisted Lead."""
client, main, *_ = create_client(monkeypatch)
before = len(main.lead_manager.leads)

body = _post(
client, name="Dave", phone="2813243020", source="facebook_ad"
).json()
assert body["success"] is True

assert len(main.lead_manager.leads) == before + 1
created = main.lead_manager.leads[-1]
assert created.name == "Dave"
assert created.phone == "2813243020"
assert created.source == "facebook_ad"


def test_contact_invalid_phone_rejected_and_creates_no_lead(monkeypatch):
"""An invalid/short phone is rejected (success=false) and no Lead is stored."""
client, main, *_ = create_client(monkeypatch)
before = len(main.lead_manager.leads)

body = _post(client, name="Eve", phone="55512").json()
assert body["success"] is False
assert "error" in body
# Rejection happens before lead creation, so nothing is persisted.
assert len(main.lead_manager.leads) == before
108 changes: 63 additions & 45 deletions tests/test_crm.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,66 @@
import json
import os
import sys
"""Tests for tools.crm_tools.save_lead — lead capture validation + contract.

Replaces a legacy smoke *script* that called ``save_lead`` at module top level,
which (a) provided zero pytest coverage and (b) appended a fake lead to
``data/leads.json`` every time the suite was merely collected. These tests pin
the validation rules and success contract without writing to disk.
"""

import pytest

# Add project root to path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from tools import crm_tools

# Test 1: Valid Lead
print("Testing valid lead...")
result = crm_tools.save_lead(
user_name="John Doe",
phone_number="555-123-4567",
interest_notes="Looking for a 3 bedroom double wide.",
)
print(f"Result: {result}")

if result["success"]:
print("SUCCESS: Valid lead accepted.")
else:
print("FAILURE: Valid lead rejected.")

# Test 2: Invalid Phone
print("\nTesting invalid phone...")
result = crm_tools.save_lead(user_name="Invalid Phone", phone_number="123", interest_notes="test")
print(f"Result: {result}")

if not result["success"]:
print("SUCCESS: Invalid phone rejected.")
else:
print("FAILURE: Invalid phone accepted.")

# Test 3: Check local file (if writable)
try:
data_dir = os.path.join(os.path.dirname(__file__), "..", "data")
leads_file = os.path.join(data_dir, "leads.json")
if os.path.exists(leads_file):
with open(leads_file) as f:
leads = json.load(f)
last_lead = leads[-1]
if last_lead["name"] == "John Doe":
print("\nSUCCESS: Lead found in local JSON file.")
else:
print(f"\nFAILURE: Last lead was {last_lead['name']}, expected John Doe.")
else:
print("\nNOTE: leads.json not found (expected if data dir not writable/created).")
except Exception as e:
print(f"\nError checking file: {e}")

@pytest.fixture(autouse=True)
def _no_disk_writes(monkeypatch):
"""Keep save_lead's success path from appending to the repo's data/leads.json.

save_lead guards its file write behind ``os.access(..., os.W_OK)``; forcing
that False exercises the full structure-and-return logic while skipping the
side effect, so collecting/running tests never pollutes local lead data.
"""
monkeypatch.setattr(crm_tools.os, "access", lambda *a, **k: False)


def test_save_lead_valid_returns_success():
result = crm_tools.save_lead(
user_name="John Doe",
phone_number="555-123-4567",
interest_notes="Looking for a 3 bedroom double wide.",
)
assert result["success"] is True
# Confirmation echoes the customer's name and number back to the agent.
assert "John Doe" in result["message"]
assert "555-123-4567" in result["message"]


def test_save_lead_accepts_formatted_phone():
result = crm_tools.save_lead(
user_name="Jane Smith",
phone_number="(281) 555-0100",
interest_notes="Financing question",
)
assert result["success"] is True


def test_save_lead_rejects_missing_name():
result = crm_tools.save_lead(
user_name="", phone_number="555-123-4567", interest_notes="test"
)
assert result["success"] is False
assert "name" in result["message"].lower()


def test_save_lead_rejects_missing_phone():
result = crm_tools.save_lead(
user_name="No Phone", phone_number="", interest_notes="test"
)
assert result["success"] is False


def test_save_lead_rejects_short_phone():
result = crm_tools.save_lead(
user_name="Short Phone", phone_number="123", interest_notes="test"
)
assert result["success"] is False
assert "invalid" in result["message"].lower() or "10" in result["message"]
Loading
Loading