| 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 |
| |
| 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 |
|
|
|
|
| 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" |
|
|
|
|
| |
|
|
| 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") |
| 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" |
| 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" |
| assert captured["reply_to"] == "cliente@x.com" |
| assert captured["sender"] == "noreply@flexigobe.com" |
| assert "BZ" in captured["sender_name"] |
|
|
|
|
| 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", "") |
| await create_handoff(db_session, t.id, email="lead@x.com") |
| rows = (await db_session.execute(select(HandoffRequest))).scalars().all() |
| assert len(rows) == 1 |
|
|
|
|
| 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 |
|
|