import uuid import pytest from django.dispatch import receiver from django.test import override_settings from infrasynth.audit.models import SecurityEvent from infrasynth.billing.models import App, PaymentGateway, Plan from infrasynth.billing.services import BillingService from infrasynth.billing.signals import ( invoice_generated, subscription_cancelled, subscription_created, ) from infrasynth.notifications.models import ChannelConfig, NotificationDispatch, NotificationTemplate from infrasynth.notifications.services import NotificationService from infrasynth.shared.enums import ChannelType, MonetizationModel from infrasynth.webhooks.signals import outbound_delivery_succeeded pytestmark = pytest.mark.django_db @pytest.fixture def gateway(): return PaymentGateway.objects.create( slug="fake", display_name="Fake", gateway_class="tests.helpers.FakeGateway", is_active=True, ) @pytest.fixture def app(): return App.objects.create(slug="messenger", name="Messenger", monetization=MonetizationModel.SUBSCRIPTION) @pytest.fixture def plan(app, gateway): return Plan.objects.create( app=app, slug="pro", name="Pro", price_amount=4900, price_currency="USD", interval="monthly", gateway=gateway, external_id="price_123", ) @pytest.fixture def notification_template(): return NotificationTemplate.all_objects.create( slug="sub-created", name="Subscription Created", channel=ChannelType.EMAIL, subject_template="Your subscription is active", body_template="Hello, your {{ plan }} plan is now active.", is_html=False, ) @pytest.fixture def channel_config(tenant): return ChannelConfig.objects.create( tenant=tenant, slug="email", channel_type=ChannelType.EMAIL, display_name="Email Channel", config={}, is_active=True, ) class TestSubscriptionCreatedSignalIntegration: def test_subscription_created_signal_integration(self, tenant, plan, gateway): sent = [] receiver_fn = lambda **kw: sent.append(kw) # noqa: E731 subscription_created.connect(receiver_fn, weak=False) try: BillingService().create_subscription(plan, tenant, gateway, external_id="sub_int_1") assert len(sent) == 1 assert sent[0]["tenant_id"] == str(tenant.pk) assert sent[0]["plan_slug"] == "pro" assert sent[0]["app_slug"] == "messenger" assert sent[0]["gateway"] == "fake" assert sent[0]["external_id"] == "sub_int_1" assert sent[0]["signal"] is subscription_created finally: subscription_created.disconnect(receiver_fn) class TestBillingSignalTriggersNotificationDispatch: @override_settings( INFRASYNTH_NOTIFICATIONS={ "DISPATCH_BACKEND": "celery", "CHANNELS": { "email": { "primary": "infrasynth.notifications.channels.email_smtp.SMTPChannel", }, }, } ) def test_billing_signal_triggers_notification_dispatch( self, tenant, plan, gateway, notification_template, channel_config ): dispatch_created = [] @receiver(subscription_created, weak=False) def handle_sub_created(**kwargs): svc = NotificationService() d = svc.send( recipient="owner@example.com", subject="Subscription Active", body="Your subscription is active.", channel=ChannelType.EMAIL, template=notification_template, context={"plan": kwargs["plan_slug"]}, ) dispatch_created.append(d) try: BillingService().create_subscription(plan, tenant, gateway, external_id="sub_notif_1") assert len(dispatch_created) == 1 dispatch = dispatch_created[0] assert isinstance(dispatch, NotificationDispatch) assert dispatch.recipient == "owner@example.com" assert dispatch.channel == ChannelType.EMAIL assert dispatch.template == notification_template assert dispatch.status == NotificationDispatch.Status.PENDING finally: subscription_created.disconnect(handle_sub_created) class TestWebhookSignalTriggersAuditLog: def test_webhook_signal_triggers_audit_log(self, tenant, user): events_created = [] @receiver(outbound_delivery_succeeded, weak=False) def create_security_event(**kwargs): event = SecurityEvent.objects.create( tenant=tenant, event_type="webhook_delivery_succeeded", actor=user, metadata={ "delivery_id": kwargs["delivery_id"], "event_name": kwargs.get("event_name", ""), "status_code": kwargs.get("status_code", 0), }, request_id=str(uuid.uuid4()), ) events_created.append(event) try: outbound_delivery_succeeded.send( sender=None, delivery_id=42, event_name="subscription.created", status_code=200, ) assert len(events_created) == 1 event = SecurityEvent.objects.get(pk=events_created[0].pk) assert event.event_type == "webhook_delivery_succeeded" assert event.actor == user assert event.metadata["delivery_id"] == 42 assert event.metadata["event_name"] == "subscription.created" assert event.metadata["status_code"] == 200 finally: outbound_delivery_succeeded.disconnect(create_security_event) class TestSubscriptionCancelledSignal: def test_subscription_cancelled_signal(self, tenant, plan, gateway): subscription = BillingService().create_subscription(plan, tenant, gateway, external_id="sub_to_cancel") sent = [] receiver_fn = lambda **kw: sent.append(kw) # noqa: E731 subscription_cancelled.connect(receiver_fn, weak=False) try: BillingService().cancel_subscription(subscription) assert len(sent) == 1 assert sent[0]["tenant_id"] == str(tenant.pk) assert sent[0]["plan_slug"] == "pro" assert sent[0]["reason"] == "user_requested" assert sent[0]["signal"] is subscription_cancelled finally: subscription_cancelled.disconnect(receiver_fn) class TestInvoiceGeneratedSignal: def test_invoice_generated_signal(self, tenant, plan, gateway): subscription = BillingService().create_subscription(plan, tenant, gateway, external_id="sub_inv_1") sent = [] receiver_fn = lambda **kw: sent.append(kw) # noqa: E731 invoice_generated.connect(receiver_fn, weak=False) try: invoice = BillingService().generate_invoice(subscription) assert len(sent) == 1 assert sent[0]["invoice_id"] == invoice.id assert sent[0]["amount"] == 4900 assert sent[0]["signal"] is invoice_generated finally: invoice_generated.disconnect(receiver_fn)