"""Trial sweep: pause expired self-serve trials that never subscribed, email the payment link, and never touch paid or still-running trials.""" from __future__ import annotations from datetime import UTC, datetime, timedelta from sqlalchemy import select from app.models import Tenant from app.scheduler import pause_expired_trials async def test_pause_only_expired_unsubscribed_trials(db_session, monkeypatch): from app import mailer sent = [] async def fake(settings, to, subject, body, **k): sent.append(to) return True monkeypatch.setattr(mailer, "send_email", fake) past = datetime.now(UTC) - timedelta(hours=1) future = datetime.now(UTC) + timedelta(days=5) db_session.add_all([ Tenant(slug="t-expired", trial_ends_at=past, chatbot_enabled=True, support_email="e@x.com"), Tenant(slug="t-paid", trial_ends_at=past, chatbot_enabled=True, stripe_subscription_id="sub_123", support_email="p@x.com"), Tenant(slug="t-running", trial_ends_at=future, chatbot_enabled=True, support_email="r@x.com"), ]) await db_session.commit() n = await pause_expired_trials(db_session) assert n == 1 # only the expired, unsubscribed one db_session.expire_all() async def _get(slug): return (await db_session.execute(select(Tenant).where(Tenant.slug == slug))).scalar_one() expired = await _get("t-expired") assert expired.chatbot_enabled is False assert expired.disabled_reason == "trial" # kill-switch reason, cuts service everywhere assert (await _get("t-paid")).chatbot_enabled is True # subscribed -> kept assert (await _get("t-running")).chatbot_enabled is True # not expired yet assert "e@x.com" in sent # expired trial got the pay link assert "p@x.com" not in sent # paid one never emailed assert "r@x.com" not in sent async def test_pause_is_idempotent(db_session, monkeypatch): from app import mailer monkeypatch.setattr(mailer, "send_email", lambda *a, **k: _noop()) db_session.add(Tenant(slug="t-x", trial_ends_at=datetime.now(UTC) - timedelta(hours=1), chatbot_enabled=True, support_email="x@x.com")) await db_session.commit() assert await pause_expired_trials(db_session) == 1 # second sweep: already paused (chatbot_enabled=False) -> not reselected assert await pause_expired_trials(db_session) == 0 async def test_pause_excludes_shopify_installs(db_session, monkeypatch): """Shopify App Store installs are billed via the Shopify Billing API, never Stripe — the trial sweep must NEVER pause them or email the Stripe pay link (App Store policy 1.2.1). Direct/Stripe trials keep being swept as before.""" from app import mailer sent = [] async def fake(settings, to, subject, body, **k): sent.append(to) return True monkeypatch.setattr(mailer, "send_email", fake) past = datetime.now(UTC) - timedelta(hours=1) db_session.add_all([ Tenant(slug="t-sh-shop", trial_ends_at=past, chatbot_enabled=True, support_email="s1@x.com", shopify_shop="s1.myshopify.com", shopify_install_source="oauth"), Tenant(slug="t-sh-src", trial_ends_at=past, chatbot_enabled=True, support_email="s2@x.com", shopify_install_source="oauth"), Tenant(slug="t-direct", trial_ends_at=past, chatbot_enabled=True, support_email="d@x.com"), # direct/Stripe -> still swept ]) await db_session.commit() assert await pause_expired_trials(db_session) == 1 # only the direct one db_session.expire_all() async def _get(slug): return (await db_session.execute(select(Tenant).where(Tenant.slug == slug))).scalar_one() assert (await _get("t-sh-shop")).chatbot_enabled is True # Shopify -> untouched assert (await _get("t-sh-src")).chatbot_enabled is True assert (await _get("t-direct")).chatbot_enabled is False # direct -> paused assert "s1@x.com" not in sent and "s2@x.com" not in sent # no Stripe email to Shopify assert "d@x.com" in sent # direct still emailed async def _noop(): return True