| from __future__ import annotations |
|
|
| from datetime import UTC, datetime, timedelta |
|
|
| from sqlalchemy import select |
|
|
| from app import carts, mailer |
| from app.models import AbandonedCart, ChatSession, Tenant |
|
|
|
|
| async def _session(db, tenant_id, lang="es"): |
| s = ChatSession(tenant_id=tenant_id, lang=lang) |
| db.add(s) |
| await db.flush() |
| return s |
|
|
|
|
| async def test_capture_one_per_session_and_updates(db_session): |
| s = await _session(db_session, 1) |
| c1 = await carts.capture( |
| db_session, 1, session_id=s.id, checkout_url="https://x/cart/1:1", items_text="1 art." |
| ) |
| |
| c2 = await carts.capture( |
| db_session, 1, session_id=s.id, checkout_url="https://x/cart/2:1", items_text="2 art." |
| ) |
| assert c2.id == c1.id |
| assert c2.checkout_url.endswith("2:1") and c2.items_text == "2 art." |
| assert c2.email is None |
|
|
|
|
| async def test_capture_autofills_known_email(db_session): |
| s = await _session(db_session, 1) |
| c = await carts.capture( |
| db_session, 1, session_id=s.id, checkout_url="https://x/cart/9:1", |
| email="Buyer@X.com", name="Leo", |
| ) |
| assert c.email == "buyer@x.com" and c.name == "Leo" |
|
|
|
|
| async def test_attach_contact_requires_existing_cart(db_session): |
| s = await _session(db_session, 1) |
| |
| assert await carts.attach_contact(db_session, 1, session_id=s.id, email="a@b.com") is None |
| await carts.capture(db_session, 1, session_id=s.id, checkout_url="https://x/cart/3:1") |
| c = await carts.attach_contact(db_session, 1, session_id=s.id, email="a@b.com", name="Ann") |
| assert c is not None and c.email == "a@b.com" and c.name == "Ann" |
|
|
|
|
| async def test_capture_is_tenant_isolated(db_session): |
| sa = await _session(db_session, 1) |
| sb = await _session(db_session, 2) |
| a = await carts.capture(db_session, 1, session_id=sa.id, checkout_url="https://a/cart/1:1") |
| b = await carts.capture(db_session, 2, session_id=sb.id, checkout_url="https://b/cart/1:1") |
| assert a.id != b.id and a.tenant_id == 1 and b.tenant_id == 2 |
|
|
|
|
| def test_recovery_email_language(): |
| es_subject, es_body = carts._recovery_email( |
| AbandonedCart(name="María García", lang="es", checkout_url="https://x/cart/1:1"), "MiTienda" |
| ) |
| assert "MiTienda" in es_subject and "María" in es_body and "https://x/cart/1:1" in es_body |
| en_subject, en_body = carts._recovery_email( |
| AbandonedCart(name="John", lang="en", checkout_url="https://x/cart/1:1"), "MyShop" |
| ) |
| assert "saved" in en_subject.lower() and en_body.startswith("Hi John") |
|
|
|
|
| async def test_recover_due_carts_sends_and_skips(db_session, monkeypatch): |
| db_session.add(Tenant(slug="cr", name="CR", brand_name="CartShop", |
| allow_cart_recovery=True, support_email="shop@cr.com")) |
| db_session.add(Tenant(slug="off", name="OFF", allow_cart_recovery=False)) |
| await db_session.flush() |
| cr = (await db_session.execute( |
| select(Tenant).where(Tenant.slug == "cr"))).scalar_one() |
| off = (await db_session.execute( |
| select(Tenant).where(Tenant.slug == "off"))).scalar_one() |
| old = datetime.now(UTC) - timedelta(hours=5) |
| |
| db_session.add(AbandonedCart(tenant_id=cr.id, email="a@b.com", checkout_url="https://x/cart/1:1", |
| created_at=old)) |
| |
| db_session.add(AbandonedCart(tenant_id=cr.id, checkout_url="https://x/cart/2:1", created_at=old)) |
| |
| db_session.add(AbandonedCart(tenant_id=cr.id, email="c@d.com", checkout_url="https://x/cart/3:1", |
| created_at=datetime.now(UTC))) |
| |
| db_session.add(AbandonedCart(tenant_id=off.id, email="e@f.com", checkout_url="https://x/cart/4:1", |
| created_at=old)) |
| await db_session.flush() |
|
|
| sent: list[tuple] = [] |
|
|
| async def fake_send(settings, to, subject, body, **kw): |
| sent.append((to, subject, kw.get("reply_to"))) |
| return True |
|
|
| monkeypatch.setattr(mailer, "send_email", fake_send) |
| n = await carts.recover_due_carts(db_session, object(), now=datetime.now(UTC)) |
| assert n == 1 |
| assert sent == [("a@b.com", sent[0][1], "shop@cr.com")] |
|
|
|
|
| async def test_recover_due_skips_already_bought(db_session, monkeypatch): |
| import app.shopify.orders as orders_mod |
| import app.tenancy as tenancy_mod |
|
|
| db_session.add(Tenant(slug="cr2", name="CR2", brand_name="Shop2", allow_cart_recovery=True)) |
| await db_session.flush() |
| t = (await db_session.execute( |
| select(Tenant).where(Tenant.slug == "cr2"))).scalar_one() |
| old = datetime.now(UTC) - timedelta(hours=5) |
| cart = AbandonedCart(tenant_id=t.id, email="paid@x.com", checkout_url="https://x/cart/1:1", |
| created_at=old) |
| db_session.add(cart) |
| await db_session.flush() |
|
|
| monkeypatch.setattr(tenancy_mod, "tenant_shopify_client", lambda tenant: object()) |
|
|
| async def fake_orders(client, email): |
| |
| return [{"created_at": datetime.now(UTC).isoformat()}] |
|
|
| monkeypatch.setattr(orders_mod, "find_orders_by_email", fake_orders) |
| sent = [] |
|
|
| async def fake_send(*a, **k): |
| sent.append(a) |
| return True |
|
|
| monkeypatch.setattr(mailer, "send_email", fake_send) |
| n = await carts.recover_due_carts(db_session, object(), now=datetime.now(UTC)) |
| assert n == 0 and sent == [] |
| |
| |
| assert cart.recovered is True and cart.nudged_at is None |
|
|