from datetime import datetime, timezone import pytest from httpx import AsyncClient from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.section import ContentSection from app.models.member import AdminMessage, Booking, Member from app.models.settings import SiteSettings from app.services.notifications import run_automatic_notifications from app.auth.password import hash_password pytestmark = pytest.mark.asyncio async def _create_member( db: AsyncSession, email: str, *, claimed: bool = False, member_status: str | None = None, ) -> Member: member = Member( email=email, first_name="Jane", last_name="Doe", phone="021 000 0000", is_claimed=claimed, is_active=True, notifications_enabled=True, member_status=member_status or ("active" if claimed else "invited"), hashed_password=hash_password("Password1!") if claimed else None, onboarding_data={"dog_name": "Buddy", "breed": "Labrador"}, ) db.add(member) await db.commit() await db.refresh(member) return member async def test_activation_sends_member_message(client: AsyncClient, admin_token: str, db_session: AsyncSession): member = await _create_member(db_session, "notify-active@example.com", claimed=True, member_status="pending_review") member.onboarding_completed_at = datetime.now(timezone.utc) member.contract_signed_at = datetime.now(timezone.utc) member.contract_signer_name = "Jane Doe" await db_session.commit() response = await client.post( f"/api/v1/admin/members/{member.id}/activate", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 result = await db_session.execute(select(AdminMessage).where(AdminMessage.member_id == member.id)) messages = result.scalars().all() assert len(messages) == 1 assert messages[0].subject == "Your Goodwalk members account is now active" async def test_activation_respects_member_notification_toggle(client: AsyncClient, admin_token: str, db_session: AsyncSession): member = await _create_member(db_session, "notify-muted@example.com", claimed=True, member_status="pending_review") member.onboarding_completed_at = datetime.now(timezone.utc) member.contract_signed_at = datetime.now(timezone.utc) member.contract_signer_name = "Jane Doe" member.notifications_enabled = False await db_session.commit() response = await client.post( f"/api/v1/admin/members/{member.id}/activate", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 result = await db_session.execute(select(AdminMessage).where(AdminMessage.member_id == member.id)) assert result.scalars().first() is None async def test_global_notification_toggle_suppresses_activation_message(client: AsyncClient, admin_token: str, db_session: AsyncSession): member = await _create_member(db_session, "notify-global-off@example.com", claimed=True, member_status="pending_review") member.onboarding_completed_at = datetime.now(timezone.utc) member.contract_signed_at = datetime.now(timezone.utc) member.contract_signer_name = "Jane Doe" db_session.add( SiteSettings( site_name="Goodwalk", automatic_member_notifications_enabled=False, nz_public_holiday_notifications_enabled=True, invoice_reminder_notifications_enabled=True, invoice_day_of_week=1, ) ) await db_session.commit() response = await client.post( f"/api/v1/admin/members/{member.id}/activate", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 result = await db_session.execute(select(AdminMessage).where(AdminMessage.member_id == member.id)) assert result.scalars().first() is None async def test_booking_confirmation_sends_member_message(client: AsyncClient, admin_token: str, db_session: AsyncSession): member = await _create_member(db_session, "bookingnotify@example.com", claimed=True, member_status="active") booking = Booking( member_id=member.id, service_type="pack_walk", status="pending", requested_date=datetime(2026, 4, 7, 9, 0, tzinfo=timezone.utc), ) db_session.add(booking) await db_session.commit() await db_session.refresh(booking) response = await client.put( f"/api/v1/admin/bookings/{booking.id}", headers={"Authorization": f"Bearer {admin_token}"}, json={"status": "confirmed"}, ) assert response.status_code == 200 result = await db_session.execute(select(AdminMessage).where(AdminMessage.member_id == member.id)) messages = result.scalars().all() assert len(messages) == 1 assert "confirmed" in messages[0].subject.lower() async def test_activation_uses_custom_template_content(client: AsyncClient, admin_token: str, db_session: AsyncSession): member = await _create_member(db_session, "custom-active@example.com", claimed=True, member_status="pending_review") member.onboarding_completed_at = datetime.now(timezone.utc) member.contract_signed_at = datetime.now(timezone.utc) member.contract_signer_name = "Jane Doe" db_session.add( ContentSection( key="notifications.automaticMessages", data={ "templates": { "member_activated": { "subject": "Welcome live, {{member_first_name}}", "body": "{{member_first_name}}, your members area is unlocked.", } } }, ) ) await db_session.commit() response = await client.post( f"/api/v1/admin/members/{member.id}/activate", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 result = await db_session.execute(select(AdminMessage).where(AdminMessage.member_id == member.id)) message = result.scalars().one() assert message.subject == "Welcome live, Jane" assert message.body == "Jane, your members area is unlocked." async def test_automatic_notifications_send_and_dedupe(db_session: AsyncSession): enabled_member = await _create_member(db_session, "holidaymember@example.com", claimed=True, member_status="active") muted_member = await _create_member(db_session, "holidaymuted@example.com", claimed=True, member_status="active") muted_member.notifications_enabled = False db_session.add( SiteSettings( site_name="Goodwalk", automatic_member_notifications_enabled=True, nz_public_holiday_notifications_enabled=True, invoice_reminder_notifications_enabled=False, invoice_day_of_week=1, ) ) await db_session.commit() summary = await run_automatic_notifications(db_session, now=datetime(2026, 7, 10, 0, 30, tzinfo=timezone.utc)) await db_session.commit() assert summary.public_holiday_messages_sent == 1 result = await db_session.execute(select(AdminMessage).where(AdminMessage.member_id == enabled_member.id)) first_run_messages = result.scalars().all() assert len(first_run_messages) == 1 assert "public holiday" in first_run_messages[0].subject.lower() second_summary = await run_automatic_notifications(db_session, now=datetime(2026, 7, 10, 10, 0, tzinfo=timezone.utc)) await db_session.commit() assert second_summary.public_holiday_messages_sent == 0 async def test_public_holiday_automation_uses_custom_template(db_session: AsyncSession): member = await _create_member(db_session, "holiday-template@example.com", claimed=True, member_status="active") db_session.add( SiteSettings( site_name="Goodwalk", automatic_member_notifications_enabled=True, nz_public_holiday_notifications_enabled=True, invoice_reminder_notifications_enabled=False, invoice_day_of_week=1, ) ) db_session.add( ContentSection( key="notifications.publicHolidays", data={ "subject": "Heads up for {{holiday_name}}", "body": "{{member_first_name}}, service may change on {{holiday_name}}.", }, ) ) await db_session.commit() summary = await run_automatic_notifications(db_session, now=datetime(2026, 7, 10, 0, 30, tzinfo=timezone.utc)) await db_session.commit() assert summary.public_holiday_messages_sent == 1 result = await db_session.execute(select(AdminMessage).where(AdminMessage.member_id == member.id)) message = result.scalars().one() assert message.subject == "Heads up for Matariki" assert message.body == "Jane, service may change on Matariki." async def test_invoice_automation_uses_custom_template(db_session: AsyncSession): member = await _create_member(db_session, "invoice-template@example.com", claimed=True, member_status="active") db_session.add( SiteSettings( site_name="Goodwalk", automatic_member_notifications_enabled=True, nz_public_holiday_notifications_enabled=False, invoice_reminder_notifications_enabled=True, invoice_day_of_week=1, ) ) db_session.add( ContentSection( key="notifications.invoiceReminders", data={ "subject": "Invoices go out on {{weekday_label}}", "body": "Preview date: {{invoice_date_label}}.", }, ) ) await db_session.commit() summary = await run_automatic_notifications(db_session, now=datetime(2026, 4, 7, 1, 0, tzinfo=timezone.utc)) await db_session.commit() assert summary.invoice_reminders_sent == 1 result = await db_session.execute(select(AdminMessage).where(AdminMessage.member_id == member.id)) message = result.scalars().one() assert message.subject == "Invoices go out on Tuesday" assert message.body == "Preview date: Tuesday 7 April." async def test_global_automatic_notification_toggle_suppresses_automation(db_session: AsyncSession): await _create_member(db_session, "invoice@example.com", claimed=True, member_status="active") db_session.add( SiteSettings( site_name="Goodwalk", automatic_member_notifications_enabled=False, nz_public_holiday_notifications_enabled=True, invoice_reminder_notifications_enabled=True, invoice_day_of_week=1, ) ) await db_session.commit() summary = await run_automatic_notifications(db_session, now=datetime(2026, 4, 7, 1, 0, tzinfo=timezone.utc)) await db_session.commit() assert summary.automatic_member_notifications_enabled is False assert summary.invoice_reminders_sent == 0