diff --git a/conftest.py b/conftest.py index c244ac24..f7dbc972 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,4 @@ +from datetime import datetime import pytest import os import responses @@ -8,9 +9,11 @@ from model_bakery import baker from stripe_payments.models import Invoice, Seller +from stripe_payments.tests.mock_connector import MockConnector User = get_user_model() + @pytest.fixture def configured_user(): user = User.objects.create_user( @@ -23,6 +26,20 @@ def configured_user(): yield user +@pytest.fixture +def configured_stripe_user(): + user = User.objects.create_user( + username='stripe_customer', + first_name="Test", + last_name="User", + email='stripetest@test.com', + password='test' + ) + user.userprofile.stripe_customer_id = "cus-1" + user.userprofile.save() + yield user + + @pytest.fixture def superuser(): yield User.objects.create_superuser( @@ -69,34 +86,60 @@ def invoice(configured_user): ) -@pytest.fixture -def get_mock_payment_intent(): - def payment_intent(webhook_event_type=None, **params): - defaults = { - "id": "mock-intent-id", - "amount": 1000, - "description": "", - "status": "succeeded", - "metadata": {}, - "currency": "gbp", - "client_secret": "secret", - "charges": Mock(data=[{"billing_details": {"email": "stripe-payer@test.com"}}]) - } - options = {**defaults, **params} - if webhook_event_type == "payment_intent.payment_failed": - options["last_payment_error"] = {'error': 'an error'} - return Mock(**options) - return payment_intent +def get_mock_payment_intent(webhook_event_type=None, **params): + defaults = { + "id": "mock-intent-id", + "amount": 1000, + "description": "", + "status": "succeeded", + "metadata": {}, + "currency": "gbp", + "client_secret": "secret", + "charges": Mock(data=[{"billing_details": {"email": "stripe-payer@test.com"}}]) + } + options = {**defaults, **params} + if webhook_event_type == "payment_intent.payment_failed": + options["last_payment_error"] = {'error': 'an error'} + return Mock(**options) + + +class MockSubscription: + def __init__(self, **init_dict): + for k, v in init_dict.items(): + setattr(self, k, v) + + def __getitem__(self, item): + return getattr(self, item) + + +def get_mock_subscription(webhook_event_type, **params): + defaults = { + "id": "id", + "status": "active", + "items": Mock(data=[Mock(price=Mock(id="price_1234"))]), # matches the id returned by the MockStripeConnector + "customer": "cus-1", + "start_date": datetime(2024, 6, 25).timestamp(), + "metadata": {}, + } + options = {**defaults, **params} + return MockSubscription(**options) @pytest.fixture -def get_mock_webhook_event(seller, get_mock_payment_intent): +def get_mock_webhook_event(seller): def mock_webhook_event(**params): webhook_event_type = params.pop("webhook_event_type", "payment_intent.succeeded") seller_id = params.pop("seller_id", seller.stripe_user_id) + if webhook_event_type in ["payment_intent.succeeded", "payment_intent.payment_failed"]: + object = get_mock_payment_intent(webhook_event_type, **params) + elif webhook_event_type == "customer.subscription.created": + object = get_mock_subscription(webhook_event_type, **params) + else: + object = Mock(**params) mock_event = Mock( account=seller_id, - data=Mock(object=get_mock_payment_intent(webhook_event_type, **params)), type=webhook_event_type + data=Mock(object=object), + type=webhook_event_type, ) return mock_event return mock_webhook_event diff --git a/stripe_payments/emails.py b/stripe_payments/emails.py index 22bf60a3..c3229892 100644 --- a/stripe_payments/emails.py +++ b/stripe_payments/emails.py @@ -57,6 +57,7 @@ def send_processed_refund_emails(invoice, event_object): user, user_membership = _get_user_from_membership(event_object) else: user = _get_user_from_invoice(invoice) + user_membership = None ctx = { 'host': f"https://{Site.objects.get_current().domain}", 'user': user, diff --git a/stripe_payments/templates/stripe_payments/email/subscription_created.html b/stripe_payments/templates/stripe_payments/email/subscription_created.html index 54ff7b14..fbccd8e6 100644 --- a/stripe_payments/templates/stripe_payments/email/subscription_created.html +++ b/stripe_payments/templates/stripe_payments/email/subscription_created.html @@ -3,6 +3,6 @@ {% block messagecontent %}

Thank you for setting up your new membership!

Your membership: {{ user_membership.membership.name }}

-

Start date: {{ user_membership.start_date|date 'd M Y' }}

+

Start date: {{ user_membership.start_date|date:'d M Y' }}

View your membership details here

{% endblock %} \ No newline at end of file diff --git a/stripe_payments/templates/stripe_payments/email/subscription_created.txt b/stripe_payments/templates/stripe_payments/email/subscription_created.txt index 0b7ee69a..8a582d9a 100644 --- a/stripe_payments/templates/stripe_payments/email/subscription_created.txt +++ b/stripe_payments/templates/stripe_payments/email/subscription_created.txt @@ -4,7 +4,7 @@ Thank you for setting up your new membership! Your membership: {{ user_membership.membership.name }} -Start date: {{ user_membership.start_date|date 'd M Y' }} +Start date: {{ user_membership.start_date|date:'d M Y' }} View your membership details: https://{{ domain }}/memberships/ diff --git a/stripe_payments/tests/test_admin.py b/stripe_payments/tests/test_admin.py index 57c09e30..48c21228 100644 --- a/stripe_payments/tests/test_admin.py +++ b/stripe_payments/tests/test_admin.py @@ -8,6 +8,7 @@ from model_bakery import baker from stripe_payments.models import Invoice, StripePaymentIntent +from conftest import get_mock_payment_intent from ..admin import StripePaymentIntentAdmin, InvoiceAdmin @@ -28,7 +29,7 @@ def test_invoice_display_no_payment_intent_or_items(): assert invoice_admin.pi(invoice) == "" -def test_invoice_display_payment_intent(get_mock_payment_intent): +def test_invoice_display_payment_intent(): invoice = baker.make( Invoice, invoice_id="foo123", username="test@test.com", amount=10 ) @@ -40,7 +41,7 @@ def test_invoice_display_payment_intent(get_mock_payment_intent): assert invoice_admin.pi(invoice) == f'mock-intent-id' -def test_invoice_display_items(get_mock_payment_intent): +def test_invoice_display_items(): invoice = baker.make( Invoice, invoice_id="foo123", username="test@test.com", amount=10 ) @@ -52,7 +53,7 @@ def test_invoice_display_items(get_mock_payment_intent): assert invoice_admin.items(invoice) == f"" -def test_payment_intent_admin_display(get_mock_payment_intent, block_gift_voucher): +def test_payment_intent_admin_display(block_gift_voucher): invoice = baker.make( Invoice, invoice_id="foo123", username="test@test.com", amount=10 ) diff --git a/stripe_payments/tests/test_models.py b/stripe_payments/tests/test_models.py index bc30ec90..34d5a3ef 100644 --- a/stripe_payments/tests/test_models.py +++ b/stripe_payments/tests/test_models.py @@ -7,6 +7,7 @@ from model_bakery import baker +from conftest import get_mock_payment_intent from booking.models import Booking, Block, TicketBooking, Ticket from ..models import Invoice, Seller, StripePaymentIntent @@ -141,7 +142,7 @@ def test_seller_str(): assert str(seller) == "testuser@test.com" -def test_invoice_payment_intent_ids(get_mock_payment_intent): +def test_invoice_payment_intent_ids(): invoice = baker.make(Invoice, invoice_id="foo123") stripe_pi, _ = StripePaymentIntent.update_or_create_payment_intent_instance( get_mock_payment_intent(), invoice @@ -149,7 +150,7 @@ def test_invoice_payment_intent_ids(get_mock_payment_intent): assert invoice.payment_intent_ids == "mock-intent-id" -def test_create_stripe_payment_intent_instance_from_pi(get_mock_payment_intent): +def test_create_stripe_payment_intent_instance_from_pi(): payment_intent = get_mock_payment_intent() invoice = baker.make(Invoice, invoice_id="foo123") assert not StripePaymentIntent.objects.exists() @@ -167,7 +168,7 @@ def test_create_stripe_payment_intent_instance_from_pi(get_mock_payment_intent): assert pi.seller == seller -def test_stripe_payment_intent_str(get_mock_payment_intent): +def test_stripe_payment_intent_str(): payment_intent = get_mock_payment_intent() invoice = baker.make(Invoice, invoice_id="foo123", username="user@test.com") pi, _ = StripePaymentIntent.update_or_create_payment_intent_instance(payment_intent, invoice) diff --git a/stripe_payments/tests/test_stripe_views.py b/stripe_payments/tests/test_stripe_views.py index fddb8bb5..e56e33db 100644 --- a/stripe_payments/tests/test_stripe_views.py +++ b/stripe_payments/tests/test_stripe_views.py @@ -1,5 +1,5 @@ +from datetime import datetime from unittest.mock import patch, Mock -import json import pytest from django.conf import settings @@ -10,7 +10,8 @@ import stripe from model_bakery import baker -from booking.models import Block, Booking, TicketBooking, Ticket +from conftest import get_mock_payment_intent +from booking.models import Block, Booking, TicketBooking, Ticket, Membership from ..models import Invoice, Seller, StripePaymentIntent from .mock_connector import MockConnector @@ -42,7 +43,7 @@ def test_return_with_unknown_payment_intent(mock_payment_intent_retrieve, client @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") def test_return_with_no_matching_invoice( - mock_payment_intent, get_mock_payment_intent, client + mock_payment_intent, client ): mock_payment_intent.retrieve.return_value = get_mock_payment_intent() resp = client.get(complete_url) @@ -58,7 +59,7 @@ def test_return_with_no_matching_invoice( @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") def test_return_with_matching_invoice_and_block( - mock_payment_intent, get_mock_payment_intent, client, configured_user + mock_payment_intent, client, configured_user ): assert StripePaymentIntent.objects.exists() is False invoice = baker.make( @@ -91,7 +92,7 @@ def test_return_with_matching_invoice_and_block( @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") def test_return_with_matching_invoice_and_booking( - mock_payment_intent, get_mock_payment_intent, client, configured_user + mock_payment_intent, client, configured_user ): assert StripePaymentIntent.objects.exists() is False invoice = baker.make( @@ -124,7 +125,7 @@ def test_return_with_matching_invoice_and_booking( @patch("stripe_payments.views.stripe.PaymentIntent") def test_return_with_matching_invoice_and_ticket_booking( - mock_payment_intent, get_mock_payment_intent, client, configured_user, seller + mock_payment_intent, client, configured_user, seller ): assert StripePaymentIntent.objects.exists() is False invoice = baker.make( @@ -160,7 +161,7 @@ def test_return_with_matching_invoice_and_ticket_booking( @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") -def test_return_with_matching_invoice_and_gift_voucher(mock_payment_intent, get_mock_payment_intent, client, configured_user, block_gift_voucher): +def test_return_with_matching_invoice_and_gift_voucher(mock_payment_intent, client, configured_user, block_gift_voucher): assert StripePaymentIntent.objects.exists() is False invoice = baker.make( Invoice, invoice_id="foo", amount=10, @@ -199,7 +200,7 @@ def test_return_with_matching_invoice_and_gift_voucher(mock_payment_intent, get_ @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") def test_return_with_matching_invoice_and_gift_voucher_anon_user( - mock_payment_intent, get_mock_payment_intent, client, block_gift_voucher + mock_payment_intent, client, block_gift_voucher ): assert StripePaymentIntent.objects.exists() is False invoice = baker.make( @@ -241,7 +242,7 @@ def test_return_with_matching_invoice_and_gift_voucher_anon_user( @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") -def test_return_with_invalid_invoice(mock_payment_intent, get_mock_payment_intent, client, configured_user): +def test_return_with_invalid_invoice(mock_payment_intent, client, configured_user): invoice = baker.make( Invoice, invoice_id="", amount=10, username=configured_user.email, stripe_payment_intent_id="mock-intent-id" @@ -264,7 +265,7 @@ def test_return_with_invalid_invoice(mock_payment_intent, get_mock_payment_inten @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") -def test_return_with_matching_invoice_multiple_bookingss(mock_payment_intent, get_mock_payment_intent, client, configured_user): +def test_return_with_matching_invoice_multiple_bookingss(mock_payment_intent, client, configured_user): invoice = baker.make( Invoice, invoice_id="foo", amount=10, username=configured_user.email, stripe_payment_intent_id="mock-intent-id" @@ -292,7 +293,7 @@ def test_return_with_matching_invoice_multiple_bookingss(mock_payment_intent, ge @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") -def test_return_with_matching_invoice_invalid_amount(mock_payment_intent, get_mock_payment_intent, client, configured_user): +def test_return_with_matching_invoice_invalid_amount(mock_payment_intent, client, configured_user): invoice = baker.make( Invoice, invoice_id="foo", username=configured_user.email, amount=50, stripe_payment_intent_id="mock-intent-id" @@ -311,7 +312,7 @@ def test_return_with_matching_invoice_invalid_amount(mock_payment_intent, get_mo @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") -def test_return_with_matching_invoice_invalid_signature(mock_payment_intent, get_mock_payment_intent, client, configured_user): +def test_return_with_matching_invoice_invalid_signature(mock_payment_intent, client, configured_user): invoice = baker.make( Invoice, invoice_id="foo", username=configured_user.email, amount=50, stripe_payment_intent_id="mock-intent-id" @@ -330,7 +331,7 @@ def test_return_with_matching_invoice_invalid_signature(mock_payment_intent, get @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") -def test_return_with_matching_invoice_block_already_processed(mock_payment_intent, get_mock_payment_intent, client, configured_user): +def test_return_with_matching_invoice_block_already_processed(mock_payment_intent, client, configured_user): invoice = baker.make( Invoice, invoice_id="foo", amount=10, username=configured_user.email, stripe_payment_intent_id="mock-intent-id", @@ -353,7 +354,7 @@ def test_return_with_matching_invoice_block_already_processed(mock_payment_inten @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") -def test_return_with_failed_payment_intent(mock_payment_intent, get_mock_payment_intent, client, configured_user): +def test_return_with_failed_payment_intent(mock_payment_intent, client, configured_user): invoice = baker.make( Invoice, invoice_id="foo", username=configured_user.email, amount=50, stripe_payment_intent_id="mock-intent-id" @@ -372,7 +373,7 @@ def test_return_with_failed_payment_intent(mock_payment_intent, get_mock_payment @pytest.mark.usefixtures("seller", "send_all_studio_emails") @patch("stripe_payments.views.stripe.PaymentIntent") -def test_return_with_processing_payment_intent(mock_payment_intent, get_mock_payment_intent, client, configured_user): +def test_return_with_processing_payment_intent(mock_payment_intent, client, configured_user): invoice = baker.make( Invoice, invoice_id="foo", username=configured_user.email, amount=50, stripe_payment_intent_id="mock-intent-id" @@ -673,16 +674,24 @@ def test_webhook_deauthorized_account( # Memberships +@patch("booking.models.membership_models.StripeConnector", MockConnector) @patch("stripe_payments.views.stripe.Webhook") def test_webhook_subscription_created( - mock_webhook, get_mock_webhook_event, client + mock_webhook, get_mock_webhook_event, client, configured_stripe_user ): + membership = baker.make(Membership, name="membership1") + assert not membership.user_memberships.exists() mock_webhook.construct_event.return_value = get_mock_webhook_event( - webhook_event_type="customer.subscription.created", metadata={} + webhook_event_type="customer.subscription.created", + start_date = datetime(2024, 6, 25).timestamp() ) resp = client.post(webhook_url, data={}, HTTP_STRIPE_SIGNATURE="foo") assert resp.status_code == 200 + # email sent to user assert len(mail.outbox) == 1 + # membership created, with start date as first of next month + assert membership.user_memberships.count() == 1 + assert membership.user_memberships.first().start_date.date() == datetime(2024, 7, 1).date() @patch("stripe_payments.views.stripe.Webhook")