from __future__ import annotations from sqlalchemy import select from app.handoff import create_handoff from app.models import ChatSession, HandoffRequest, Tenant from app.tools import escalate_tool from app.tools.registry import ToolContext AUTH = {"Authorization": "Bearer test-admin-token"} async def _ctx(db, channel="web"): t = Tenant(slug="t", brand_name="T") db.add(t) await db.flush() s = ChatSession(shop="x", tenant_id=t.id) db.add(s) await db.flush() return ToolContext(db=db, session=s, tenant_id=t.id, channel=channel), t async def test_escalate_web_without_email_shows_form(db_session): ctx, _t = await _ctx(db_session, channel="web") out = await escalate_tool.run({}, ctx) assert out["status"] == "show_form" assert ctx.handoff is True # widget will render the form # nothing stored yet (the form submit will store it) n = (await db_session.execute(select(HandoffRequest))).scalars().all() assert n == [] async def test_escalate_whatsapp_without_email_asks_for_it(db_session): ctx, _t = await _ctx(db_session, channel="whatsapp") out = await escalate_tool.run({}, ctx) assert out["status"] == "need_info" assert ctx.handoff is False # no form on WhatsApp async def test_escalate_with_details_stores_request(db_session): ctx, t = await _ctx(db_session, channel="whatsapp") out = await escalate_tool.run( {"email": "cliente@x.com", "name": "Ana", "question": "garantía"}, ctx ) assert out["status"] == "received" rows = (await db_session.execute(select(HandoffRequest))).scalars().all() assert len(rows) == 1 assert rows[0].email == "cliente@x.com" assert rows[0].name == "Ana" assert rows[0].message == "garantía" assert rows[0].channel == "whatsapp" assert rows[0].tenant_id == t.id async def test_create_handoff_isolated_per_tenant(db_session): a = Tenant(slug="a") b = Tenant(slug="b") db_session.add_all([a, b]) await db_session.flush() await create_handoff(db_session, a.id, email="a@x.com", channel="web") await create_handoff(db_session, b.id, email="b@x.com", channel="web") a_rows = (await db_session.execute( select(HandoffRequest).where(HandoffRequest.tenant_id == a.id) )).scalars().all() assert len(a_rows) == 1 and a_rows[0].email == "a@x.com" # --- HTTP: the widget form submit + admin listing ----------------------- async def test_handoff_endpoint_stores_and_admin_lists(app_client, db_session): _app, client = app_client await client.post("/admin/tenants", headers=AUTH, json={"slug": "tienda1"}) r = await client.post( "/handoff?t=tienda1", json={"name": "Víctor", "email": "v@x.com", "message": "quiero info"}, ) assert r.status_code == 200 assert r.json()["status"] == "received" listed = await client.get("/admin/tenants/tienda1/handoffs", headers=AUTH) data = listed.json() assert len(data) == 1 assert data[0]["email"] == "v@x.com" assert data[0]["channel"] == "web" assert data[0]["status"] == "new" hid = data[0]["id"] done = await client.post(f"/admin/tenants/tienda1/handoffs/{hid}/done", headers=AUTH) assert done.status_code == 200 again = (await client.get("/admin/tenants/tienda1/handoffs", headers=AUTH)).json() assert again[0]["status"] == "done" async def test_handoff_admin_requires_auth(app_client): _app, client = app_client assert (await client.get("/admin/tenants/x/handoffs")).status_code == 401 async def test_handoff_emails_the_tenants_own_inbox(db_session, monkeypatch): """The lead is emailed to THAT tenant's support_email (never another's).""" from app.config import get_settings a = Tenant(slug="empresa-a", brand_name="A", support_email="a@empresa-a.com") b = Tenant(slug="empresa-b", brand_name="B", support_email="b@empresa-b.com") db_session.add_all([a, b]) await db_session.flush() sent = [] async def fake_send(to, subject, body): sent.append({"to": to, "subject": subject, "body": body}) return True s = get_settings() monkeypatch.setattr(s, "smtp_host", "smtp.test") # pretend SMTP is configured import app.mailer as mailer monkeypatch.setattr(mailer, "default_sender", lambda _s: fake_send) await create_handoff(db_session, a.id, name="Ana", email="lead@x.com", message="hola") assert len(sent) == 1 assert sent[0]["to"] == "a@empresa-a.com" # NOT b@empresa-b.com assert "lead@x.com" in sent[0]["body"] async def test_handoff_uses_brevo_api_when_configured(db_session, monkeypatch): """When Brevo is configured, the lead emails the tenant's OWN inbox via the Brevo HTTP API, with reply-to set to the customer.""" import app.mailer as mailer from app.config import get_settings t = Tenant(slug="bz", brand_name="BZ", support_email="dueno@bz.com") db_session.add(t) await db_session.flush() s = get_settings() monkeypatch.setattr(s, "brevo_api_key", "xkeys-test") monkeypatch.setattr(s, "brevo_sender", "noreply@flexigobe.com") monkeypatch.setattr(s, "smtp_host", "") captured = {} async def fake_send(self, to, subject, body, *, reply_to=None, sender_name=None, html=None): captured.update(to=to, reply_to=reply_to, sender=self.sender_email, sender_name=sender_name) return True monkeypatch.setattr(mailer.BrevoMailer, "send", fake_send) await create_handoff(db_session, t.id, email="cliente@x.com", message="hola") assert captured["to"] == "dueno@bz.com" # tenant's own inbox, not anyone else's assert captured["reply_to"] == "cliente@x.com" # merchant replies straight to the lead assert captured["sender"] == "noreply@flexigobe.com" assert "BZ" in captured["sender_name"] # the From name is the store's own brand async def test_handoff_no_smtp_still_stores(db_session, monkeypatch): from sqlalchemy import select from app.config import get_settings t = Tenant(slug="nosmtp", support_email="x@x.com") db_session.add(t) await db_session.flush() monkeypatch.setattr(get_settings(), "smtp_host", "") # no SMTP await create_handoff(db_session, t.id, email="lead@x.com") rows = (await db_session.execute(select(HandoffRequest))).scalars().all() assert len(rows) == 1 # stored even without email delivery def test_widget_js_has_handoff_form(): from pathlib import Path js = (Path(__file__).resolve().parent.parent / "app" / "static" / "widget.js").read_text() assert "renderHandoffForm" in js assert "/handoff" in js assert "d.handoff" in js # triggered by the response flag