import uuid from datetime import timedelta from unittest.mock import Mock, patch import pytest from django.urls import reverse from django.utils import timezone from rest_framework.test import APIClient from apps.accounts.models import User, UserRole from apps.bookings.models import Booking, BookingStatus from apps.payments.models import Payment, PaymentProvider, PaymentStatus from apps.salons.models import Salon, Service, StaffProfile @pytest.fixture def booking_entities(): owner = User.objects.create_user( email="owner@example.com", password="pass", role=UserRole.MANAGER, phone_number="+966500000011", ) customer = User.objects.create_user( email="customer@example.com", password="pass", phone_number="+966500000012", ) staff_user = User.objects.create_user( email="staff@example.com", password="pass", role=UserRole.STAFF, phone_number="+966500000013", ) salon = Salon.objects.create( owner=owner, name="Main Salon", description="", address="123 King Rd", city="Riyadh", phone_number="0512345678", ) service = Service.objects.create( salon=salon, name="Haircut", description="", duration_minutes=60, price_amount=120, currency="SAR", ) staff = StaffProfile.objects.create(user=staff_user, salon=salon) start_time = timezone.now() + timedelta(days=1) end_time = start_time + timedelta(minutes=60) booking = Booking.objects.create( salon=salon, customer=customer, service=service, staff=staff, start_time=start_time, end_time=end_time, status=BookingStatus.PENDING, price_amount=service.price_amount, currency=service.currency, notes="", ) return customer, booking def _mock_gateway_response(payment_id="pay_test", status="initiated"): response = Mock() response.status_code = 201 response.json.return_value = { "id": payment_id, "status": status, "source": {"transaction_url": "https://moyasar.example/tx"}, } response.content = b"{}" return response @pytest.mark.django_db @patch("apps.payments.services.gateway.requests.post") def test_create_payment_idempotency_returns_existing(mock_post, booking_entities, monkeypatch): customer, booking = booking_entities client = APIClient() client.force_authenticate(user=customer) monkeypatch.setenv("MOYASAR_SECRET_KEY", "sk_test") monkeypatch.setenv("MOYASAR_PUBLISHABLE_KEY", "pk_test") mock_post.return_value = _mock_gateway_response() request_id = str(uuid.uuid4()) payload = { "booking_id": booking.id, "provider": PaymentProvider.MOYASAR, "idempotency_key": request_id, "source": {"type": "stcpay", "mobile": "0500000000"}, } response = client.post(reverse("payment-list"), payload, content_type="application/json") assert response.status_code == 201 response_repeat = client.post(reverse("payment-list"), payload, content_type="application/json") assert response_repeat.status_code == 200 assert Payment.objects.count() == 1 @pytest.mark.django_db def test_rejects_creditcard_source(booking_entities): customer, booking = booking_entities client = APIClient() client.force_authenticate(user=customer) payload = { "booking_id": booking.id, "provider": PaymentProvider.MOYASAR, "idempotency_key": str(uuid.uuid4()), "source": {"type": "creditcard", "number": "4111111111111111"}, } response = client.post(reverse("payment-list"), payload, content_type="application/json") assert response.status_code == 400 assert "source" in response.json() @pytest.mark.django_db def test_webhook_paid_updates_status(booking_entities, monkeypatch): _, booking = booking_entities monkeypatch.setenv("MOYASAR_WEBHOOK_SECRET", "secret") payment = Payment.objects.create( booking=booking, provider=PaymentProvider.MOYASAR, status=PaymentStatus.INITIATED, amount=booking.price_amount, currency=booking.currency, external_id="pay_webhook", metadata={}, ) payload = { "type": "payment_paid", "secret_token": "secret", "data": {"id": "pay_webhook"}, } client = APIClient() response = client.post(reverse("payment-webhook"), payload, content_type="application/json") assert response.status_code == 200 payment.refresh_from_db() assert payment.status == PaymentStatus.PAID assert payment.paid_at is not None @pytest.mark.django_db def test_webhook_invalid_secret_is_rejected(booking_entities, monkeypatch): _, booking = booking_entities monkeypatch.setenv("MOYASAR_WEBHOOK_SECRET", "secret") Payment.objects.create( booking=booking, provider=PaymentProvider.MOYASAR, status=PaymentStatus.INITIATED, amount=booking.price_amount, currency=booking.currency, external_id="pay_webhook", metadata={}, ) payload = { "type": "payment_paid", "secret_token": "wrong", "data": {"id": "pay_webhook"}, } client = APIClient() response = client.post(reverse("payment-webhook"), payload, content_type="application/json") assert response.status_code == 401