From 2d3ce597822328cf15e32ed8fd992c49b38439e4 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Mon, 18 Dec 2023 11:38:32 -0800 Subject: [PATCH 01/22] upgrade and add dj stripe --- .github/workflows/pythonpackage.yml | 4 ++-- requirements.txt | 5 ++++- setup.cfg | 5 +++++ testproject/settings.py | 2 ++ testproject/testapp/models.py | 6 ++++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index caab0b0..f9eba92 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.9.x, 3.10.x, 3.11.x] - django-version: ['<4', '>=4'] + python-version: [3.10.x, 3.11.x, 3.12.x] + django-version: ['<4', '>=4', '>=5'] steps: - uses: actions/checkout@v2 diff --git a/requirements.txt b/requirements.txt index cd09a32..12f4f5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ twine==4.0.2 # django stuff -Django==4.2.2 +Django==5.0 djangorestframework==3.14.0 pytz==2023.3 @@ -15,3 +15,6 @@ factory-boy==3.2.1 pytest==7.3.2 pytest-django==4.5.2 flake8==6.0.0 + +# payment processing +djstripe==2.8.3 diff --git a/setup.cfg b/setup.cfg index 8a82a91..8c825cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Software Development long_description = file: README.md long_description_content_type = text/markdown @@ -60,3 +61,7 @@ license_files = python_requires = >= 3.6 packages = find: zip_safe: False + +[options.extras_require] +stripe = + djstripe>=2.8.3 diff --git a/testproject/settings.py b/testproject/settings.py index ce23d56..ca7cbab 100644 --- a/testproject/settings.py +++ b/testproject/settings.py @@ -40,6 +40,8 @@ "ckc", "testapp", + + "djstripe", ) STATIC_URL = "/static/" diff --git a/testproject/testapp/models.py b/testproject/testapp/models.py index b4459fe..30dc577 100644 --- a/testproject/testapp/models.py +++ b/testproject/testapp/models.py @@ -55,3 +55,9 @@ class SnapshottedModelMissingOverride(JsonSnapshotModel, models.Model): # No _create_json_snapshot here! This is for testing purposes, to confirm we raise # an assertion when this method is missing pass + + +# ---------------------------------------------------------------------------- +# For testing DJStripe +# ---------------------------------------------------------------------------- +class From a11cdbdd6a1dd318e61a6439b612c752f25b39d5 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Tue, 19 Dec 2023 11:22:32 -0800 Subject: [PATCH 02/22] dj stripe payment methods --- .env_sample | 2 + README.md | 2 +- ckc/serializers.py | 49 ++++++++++++++++++++ ckc/views.py | 15 ++++++ requirements.txt | 2 +- testproject/settings.py | 17 +++++++ testproject/testapp/models.py | 6 --- testproject/urls.py | 2 + tests/integration/test_payment_processing.py | 25 ++++++++++ 9 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 .env_sample create mode 100644 ckc/views.py create mode 100644 tests/integration/test_payment_processing.py diff --git a/.env_sample b/.env_sample new file mode 100644 index 0000000..c4cd784 --- /dev/null +++ b/.env_sample @@ -0,0 +1,2 @@ +STRIPE_PUBLIC_KEY= +STRIPE_PRIVATE_KEY= diff --git a/README.md b/README.md index e5df743..66a7bb3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ INSTALLED_APPS = ( ## tests ```bash -$ docker build -t django-ckc . && docker run django-ckc pytest +$ docker build -t django-ckc . && docker run --env-file .env django-ckc pytest ``` ## what's in this diff --git a/ckc/serializers.py b/ckc/serializers.py index d00dde0..abe467b 100644 --- a/ckc/serializers.py +++ b/ckc/serializers.py @@ -1,3 +1,8 @@ +import stripe +from djstripe.models import PaymentMethod, Customer + +from rest_framework import serializers + class DefaultCreatedByMixin: """This will automatically set `YourModel.created_by` to `request.user`. To override which attribute the user is written to, add a `user_field` to your classes Meta information @@ -25,3 +30,47 @@ def create(self, validated_data): 'and overwrote context?') validated_data[user_field] = self.context['request'].user return super().create(validated_data) + + +class PaymentMethodSerializer(serializers.ModelSerializer): + token = serializers.CharField(write_only=True) + + class Meta: + model = PaymentMethod + fields = ( + 'token', + 'id', + 'type', + + # 'customer', + # 'stripe_id', + # 'card_brand', + # 'card_last4', + # 'card_exp_month', + # 'card_exp_year', + # 'is_default', + # 'created', + # 'modified', + ) + read_only_fields = ( + 'id', + 'type', + # 'customer', + # 'stripe_id', + # 'card_brand', + # 'card_last4', + # 'card_exp_month', + # 'card_exp_year', + # 'is_default', + # 'created', + # 'modified', + ) + + def create(self, validated_data): + customer, created = Customer.get_or_create(subscriber=self.context['request'].user) + try: + payment_method = customer.add_payment_method(validated_data['token']) + except (stripe.error.InvalidRequestError) as e: + raise serializers.ValidationError(e) + + return payment_method diff --git a/ckc/views.py b/ckc/views.py new file mode 100644 index 0000000..a348c6b --- /dev/null +++ b/ckc/views.py @@ -0,0 +1,15 @@ +from djstripe.models import PaymentMethod +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from ckc.serializers import PaymentMethodSerializer + + +class PaymentMethodViewSet(viewsets.ModelViewSet): + queryset = PaymentMethod.objects.all() + serializer_class = PaymentMethodSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + qs = PaymentMethod.objects.filter(customer__subscriber=self.request.user) + return qs diff --git a/requirements.txt b/requirements.txt index 12f4f5e..12b84e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,4 @@ pytest-django==4.5.2 flake8==6.0.0 # payment processing -djstripe==2.8.3 +dj-stripe==2.8.3 diff --git a/testproject/settings.py b/testproject/settings.py index ca7cbab..1ea6e33 100644 --- a/testproject/settings.py +++ b/testproject/settings.py @@ -1,5 +1,6 @@ import os +import stripe DEBUG = True @@ -54,3 +55,19 @@ "APP_DIRS": True } ] + +# ============================================================================= +# Stripe +# ============================================================================= +STRIPE_PUBLIC_KEY = os.environ.get('STRIPE_PUBLIC_KEY') +STRIPE_PRIVATE_KEY = os.environ.get('STRIPE_PRIVATE_KEY') +DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id" +stripe.api_key = STRIPE_PRIVATE_KEY + +# ============================================================================= +# DJStripe +# ============================================================================= +STRIPE_LIVE_SECRET_KEY = STRIPE_PRIVATE_KEY +STRIPE_TEST_SECRET_KEY = STRIPE_PRIVATE_KEY +DJSTRIPE_USE_NATIVE_JSONFIELD = True +STRIPE_LIVE_MODE = False # Change to True in production diff --git a/testproject/testapp/models.py b/testproject/testapp/models.py index 30dc577..b4459fe 100644 --- a/testproject/testapp/models.py +++ b/testproject/testapp/models.py @@ -55,9 +55,3 @@ class SnapshottedModelMissingOverride(JsonSnapshotModel, models.Model): # No _create_json_snapshot here! This is for testing purposes, to confirm we raise # an assertion when this method is missing pass - - -# ---------------------------------------------------------------------------- -# For testing DJStripe -# ---------------------------------------------------------------------------- -class diff --git a/testproject/urls.py b/testproject/urls.py index 5fcc7ea..644739e 100644 --- a/testproject/urls.py +++ b/testproject/urls.py @@ -1,6 +1,7 @@ from django.urls import path from rest_framework import routers +from ckc.views import PaymentMethodViewSet from testapp.views import TestExceptionsViewSet from testapp.viewsets import TestModelWithACreatorViewSet, TestModelWithADifferentNamedCreatorViewSet, BModelViewSet @@ -9,6 +10,7 @@ router.register(r'creators', TestModelWithACreatorViewSet) router.register(r'creators-alternative', TestModelWithADifferentNamedCreatorViewSet) router.register(r'bmodel', BModelViewSet) +router.register(r'payment-methods', PaymentMethodViewSet, basename='payment-methods') urlpatterns = router.urls + [ path('test-exceptions/', TestExceptionsViewSet.as_view(), name='test-exceptions'), diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py new file mode 100644 index 0000000..2aaf355 --- /dev/null +++ b/tests/integration/test_payment_processing.py @@ -0,0 +1,25 @@ +from django.urls import reverse +from djstripe.models import PaymentMethod, Customer +from rest_framework.test import APITestCase + +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class TestExceptions(APITestCase): + def setUp(self): + self.user = User.objects.create_user(username="test", password="test") + self.client.force_authenticate(user=self.user) + return super().setUp() + + def test_payment_method(self): + # simulate card being created on the frontend + url = reverse('payment-methods-list') + payload = {"token": "pm_card_visa"} + resp = self.client.post(url, data=payload, format='json') + + # assert payment method and customer creation + assert resp.status_code == 201 + assert PaymentMethod.objects.count() == 1 + assert Customer.objects.count() == 1 From a0bf39cb777372633f36e478b44dc1c2de3cf225 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Wed, 20 Dec 2023 20:53:01 -0800 Subject: [PATCH 03/22] checkout session util function --- ckc/management/utils/__init__.py | 0 ckc/management/utils/payments.py | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 ckc/management/utils/__init__.py create mode 100644 ckc/management/utils/payments.py diff --git a/ckc/management/utils/__init__.py b/ckc/management/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckc/management/utils/payments.py b/ckc/management/utils/payments.py new file mode 100644 index 0000000..f7b000f --- /dev/null +++ b/ckc/management/utils/payments.py @@ -0,0 +1,55 @@ +import stripe +from djstripe.models import Customer + + +def create_checkout_session(user, success_url, cancel_url, line_items, metadata=None, payment_method_types=None): + """ + create and return a stripe checkout session + + @param user: the user to associate the session with + @param success_url: the url to redirect to after a successful payment + @param cancel_url: the url to redirect to after a cancelled payment + @param line_items: a list of line items to add to the session + @param metadata: optional metadata to add to the session + @param payment_method_types: optional payment method types to accept. defaults to ["card"] + + + metadata = {}, + success_url = "https://example.com/success", + cancel_url = "https://example.com/cancel", + line_items = [{ + "quantity": 1, + "price_data": { + "currency": "usd", + "unit_amount": 2000, + "product_data": { + "name": "Sample Product Name", + "images": ["https://i.imgur.com/EHyR2nP.png"], + "description": "Sample Description", + }, + }, + }] + + @returns stripe.checkout.Session + """ + if not metadata: + metadata = {} + if not payment_method_types: + payment_method_types = ["card"] + + customer, created = Customer.get_or_create(subscriber=user) + session = stripe.checkout.Session.create( + payment_method_types=payment_method_types, + customer=customer.id, + payment_intent_data={ + "setup_future_usage": "off_session", + # so that the metadata gets copied to the associated Payment Intent and Charge Objects + "metadata": metadata + }, + line_items=line_items, + mode="payment", + success_url=success_url, + cancel_url=cancel_url, + metadata=metadata, + ) + return session From cbabfcfc1d6c92962d51a4756a7e603f201ae951 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Wed, 20 Dec 2023 21:32:48 -0800 Subject: [PATCH 04/22] keep pluggin on payment intents --- ckc/management/utils/payments.py | 62 ++++++++++++++++++++ ckc/serializers.py | 4 +- tests/integration/test_payment_processing.py | 55 ++++++++++++++++- 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/ckc/management/utils/payments.py b/ckc/management/utils/payments.py index f7b000f..0cde14c 100644 --- a/ckc/management/utils/payments.py +++ b/ckc/management/utils/payments.py @@ -1,6 +1,10 @@ +import json + import stripe from djstripe.models import Customer +from django.conf import settings + def create_checkout_session(user, success_url, cancel_url, line_items, metadata=None, payment_method_types=None): """ @@ -53,3 +57,61 @@ def create_checkout_session(user, success_url, cancel_url, line_items, metadata= metadata=metadata, ) return session + + +def create_payment_intent(payment_method_id, customer_id, amount, currency="usd", confirmation_method="automatic"): + """ + create and return a stripe payment intent + @param payment_method_id: the id of the payment method to use + @param amount: the amount to charge + @param currency: the currency to charge in. defaults to "usd" + @param confirmation_method: the confirmation method to use. choices are "manual" and "automatic". defaults to "automatic" + if set to manual, you must call confirm_payment_intent to confirm the payment intent + @returns stripe.PaymentIntent + """ + if not payment_method_id: + raise ValueError("payment_method_id must be set") + + intent = None + try: + # Create the PaymentIntent + intent = stripe.PaymentIntent.create( + customer=customer_id, + payment_method=payment_method_id, + amount=amount, + currency=currency, + confirmation_method=confirmation_method, + confirm=confirmation_method == "automatic", + api_key=settings.STRIPE_PRIVATE_KEY, + ) + except stripe.error.CardError: + pass + return intent + + +def confirm_payment_intent(payment_intent_id): + """ + confirm a stripe payment intent + @param payment_intent_id: the id of the payment intent to confirm + @returns a tuple of (data, status_code) + """ + intent = stripe.PaymentIntent.confirm( + payment_intent_id, + api_key=settings.STRIPE_PRIVATE_KEY, + ) + + if intent.status == "requires_action" and intent.next_action.type == "use_stripe_sdk": + # Tell the client to handle the action + return_data = json.dumps({ + "requires_action": True, + "payment_intent_client_secret": intent.client_secret + }), 200 + pass + elif intent.status == "succeeded": + # The payment did not need any additional actions and completed! + # Handle post-payment fulfillment + return_data = json.dumps({"success": True}), 200 + else: + # Invalid status + return_data = json.dumps({"error": "Invalid PaymentIntent status"}), 500 + return return_data diff --git a/ckc/serializers.py b/ckc/serializers.py index abe467b..b88510a 100644 --- a/ckc/serializers.py +++ b/ckc/serializers.py @@ -38,7 +38,7 @@ class PaymentMethodSerializer(serializers.ModelSerializer): class Meta: model = PaymentMethod fields = ( - 'token', + 'pm_id', 'id', 'type', @@ -69,7 +69,7 @@ class Meta: def create(self, validated_data): customer, created = Customer.get_or_create(subscriber=self.context['request'].user) try: - payment_method = customer.add_payment_method(validated_data['token']) + payment_method = customer.add_payment_method(validated_data['pm_id']) except (stripe.error.InvalidRequestError) as e: raise serializers.ValidationError(e) diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 2aaf355..596c251 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -1,9 +1,12 @@ +import stripe from django.urls import reverse from djstripe.models import PaymentMethod, Customer from rest_framework.test import APITestCase from django.contrib.auth import get_user_model +from ckc.management.utils.payments import create_checkout_session, create_payment_intent, confirm_payment_intent + User = get_user_model() @@ -16,10 +19,60 @@ def setUp(self): def test_payment_method(self): # simulate card being created on the frontend url = reverse('payment-methods-list') - payload = {"token": "pm_card_visa"} + payload = {"pm_id": "pm_card_visa"} resp = self.client.post(url, data=payload, format='json') # assert payment method and customer creation assert resp.status_code == 201 assert PaymentMethod.objects.count() == 1 assert Customer.objects.count() == 1 + + def test_create_checkout_session(self): + session = create_checkout_session( + self.user, + 'https://example.com/success', + 'https://example.com/cancel', + [{ + "quantity": 1, + "price_data": { + "currency": "usd", + "unit_amount": 2000, + "product_data": { + "name": "Sample Product Name", + "images": ["https://i.imgur.com/EHyR2nP.png"], + "description": "Sample Description", + }, + }, + }], + metadata={ + "test": "metadata" + }, + ) + assert session is not None + + def test_payment_intents(self): + # assume we already have a stripe customer with a payment method attatched + customer, created = Customer.get_or_create(subscriber=self.user) + payment_method = customer.add_payment_method("pm_card_visa") + customer.add_payment_method(payment_method.id) + + # manual payment intent confirmation + + # create a payment intent + intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="manual") + + # assert payment intent creation + assert intent is not None + assert intent.status == "requires_confirmation" + + # assert payment intent confirmation + response_data, status_code = confirm_payment_intent(intent.id) + assert status_code == 200 + assert response_data.get('success', False) + + # automatic payment intent confirmation + intent = create_payment_intent(payment_method.id, 2000, confirmation_method="automatic") + assert intent is not None + assert intent.status == "succeeded" + + From 431de8584acef762b36fc29e4d7021014eb575de Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 28 Dec 2023 08:55:52 -0800 Subject: [PATCH 05/22] fix payment intents --- ckc/management/utils/payments.py | 7 ++++++- tests/integration/test_payment_processing.py | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ckc/management/utils/payments.py b/ckc/management/utils/payments.py index 0cde14c..e0155bd 100644 --- a/ckc/management/utils/payments.py +++ b/ckc/management/utils/payments.py @@ -80,9 +80,14 @@ def create_payment_intent(payment_method_id, customer_id, amount, currency="usd" payment_method=payment_method_id, amount=amount, currency=currency, - confirmation_method=confirmation_method, + # confirmation_method=confirmation_method, confirm=confirmation_method == "automatic", api_key=settings.STRIPE_PRIVATE_KEY, + automatic_payment_methods={ + "enabled": True, + "allow_redirects": 'never' + }, + ) except stripe.error.CardError: pass diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 596c251..184f191 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -1,3 +1,5 @@ +import json + import stripe from django.urls import reverse from djstripe.models import PaymentMethod, Customer @@ -67,11 +69,12 @@ def test_payment_intents(self): # assert payment intent confirmation response_data, status_code = confirm_payment_intent(intent.id) + assert status_code == 200 - assert response_data.get('success', False) + assert json.loads(response_data).get('success', False) # automatic payment intent confirmation - intent = create_payment_intent(payment_method.id, 2000, confirmation_method="automatic") + intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="automatic") assert intent is not None assert intent.status == "succeeded" From e0afde3ef619226e534cccd51bddabe6cb7c894f Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 28 Dec 2023 08:57:06 -0800 Subject: [PATCH 06/22] rename --- ckc/{management => }/utils/__init__.py | 0 ckc/{management => }/utils/payments.py | 0 tests/integration/test_payment_processing.py | 3 +-- 3 files changed, 1 insertion(+), 2 deletions(-) rename ckc/{management => }/utils/__init__.py (100%) rename ckc/{management => }/utils/payments.py (100%) diff --git a/ckc/management/utils/__init__.py b/ckc/utils/__init__.py similarity index 100% rename from ckc/management/utils/__init__.py rename to ckc/utils/__init__.py diff --git a/ckc/management/utils/payments.py b/ckc/utils/payments.py similarity index 100% rename from ckc/management/utils/payments.py rename to ckc/utils/payments.py diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 184f191..549cb6e 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -1,13 +1,12 @@ import json -import stripe from django.urls import reverse from djstripe.models import PaymentMethod, Customer from rest_framework.test import APITestCase from django.contrib.auth import get_user_model -from ckc.management.utils.payments import create_checkout_session, create_payment_intent, confirm_payment_intent +from ckc.utils.payments import create_checkout_session, create_payment_intent, confirm_payment_intent User = get_user_model() From c35fb15f650cea7d09e6433c6fc2139e4bda011c Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 28 Dec 2023 10:23:38 -0800 Subject: [PATCH 07/22] subscriptions --- ckc/serializers.py | 36 +++++++++++++++++- ckc/views.py | 18 +++++++-- testproject/urls.py | 4 +- tests/integration/test_payment_processing.py | 36 +++++++++++++++++- tests/integration/utils.py | 40 ++++++++++++++++++++ 5 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 tests/integration/utils.py diff --git a/ckc/serializers.py b/ckc/serializers.py index b88510a..3e61f36 100644 --- a/ckc/serializers.py +++ b/ckc/serializers.py @@ -1,5 +1,5 @@ import stripe -from djstripe.models import PaymentMethod, Customer +from djstripe.models import PaymentMethod, Customer, Price, Product from rest_framework import serializers @@ -74,3 +74,37 @@ def create(self, validated_data): raise serializers.ValidationError(e) return payment_method + + +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = ( + 'id', + 'name', + 'description', + 'type', + ) + read_only_fields = ( + 'id', + 'name', + 'description', + 'type', + ) + + +class PriceSerializer(serializers.ModelSerializer): + class Meta: + model = Price + fields = ( + 'id', + 'unit_amount', + 'currency', + 'recurring', + ) + read_only_fields = ( + 'id', + 'unit_amount', + 'currency', + 'recurring', + ) diff --git a/ckc/views.py b/ckc/views.py index a348c6b..d044308 100644 --- a/ckc/views.py +++ b/ckc/views.py @@ -1,8 +1,8 @@ -from djstripe.models import PaymentMethod -from rest_framework import viewsets -from rest_framework.permissions import IsAuthenticated +from djstripe.models import PaymentMethod, Price, Plan +from rest_framework import viewsets, mixins +from rest_framework.permissions import IsAuthenticated, AllowAny -from ckc.serializers import PaymentMethodSerializer +from ckc.serializers import PaymentMethodSerializer, PriceSerializer class PaymentMethodViewSet(viewsets.ModelViewSet): @@ -13,3 +13,13 @@ class PaymentMethodViewSet(viewsets.ModelViewSet): def get_queryset(self): qs = PaymentMethod.objects.filter(customer__subscriber=self.request.user) return qs + + +class PriceViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin): + queryset = Price.objects.all() + serializer_class = PriceSerializer + permission_classes = [AllowAny] + + def get_queryset(self): + qs = Price.objects.all() + return qs diff --git a/testproject/urls.py b/testproject/urls.py index 644739e..d324a0a 100644 --- a/testproject/urls.py +++ b/testproject/urls.py @@ -1,7 +1,7 @@ from django.urls import path from rest_framework import routers -from ckc.views import PaymentMethodViewSet +from ckc.views import PaymentMethodViewSet, PriceViewSet from testapp.views import TestExceptionsViewSet from testapp.viewsets import TestModelWithACreatorViewSet, TestModelWithADifferentNamedCreatorViewSet, BModelViewSet @@ -11,6 +11,8 @@ router.register(r'creators-alternative', TestModelWithADifferentNamedCreatorViewSet) router.register(r'bmodel', BModelViewSet) router.register(r'payment-methods', PaymentMethodViewSet, basename='payment-methods') +# router.register(r'subscription-plans', SubscriptionPlanViewSet, basename='subscription-plans') +router.register(r'prices', PriceViewSet, basename='prices') urlpatterns = router.urls + [ path('test-exceptions/', TestExceptionsViewSet.as_view(), name='test-exceptions'), diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 549cb6e..3fcabd8 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -1,17 +1,20 @@ import json +import stripe from django.urls import reverse -from djstripe.models import PaymentMethod, Customer +from djstripe.models import PaymentMethod, Customer, Price, Product +# from djstripe.core import Price from rest_framework.test import APITestCase from django.contrib.auth import get_user_model from ckc.utils.payments import create_checkout_session, create_payment_intent, confirm_payment_intent +from tests.integration.utils import create_subscription_plan User = get_user_model() -class TestExceptions(APITestCase): +class TestPaymentProcessing(APITestCase): def setUp(self): self.user = User.objects.create_user(username="test", password="test") self.client.force_authenticate(user=self.user) @@ -77,4 +80,33 @@ def test_payment_intents(self): assert intent is not None assert intent.status == "succeeded" + def test_subscriptions(self): + # create the subscription plan through dj stripe price object + price = create_subscription_plan(2000, "month", product_name="Sample Product Name: 0", currency="usd") + assert price is not None + assert price.id is not None + customer, created = Customer.get_or_create(subscriber=self.user) + customer.add_payment_method("pm_card_visa") + # subscribe the customer to the plan + subscription = customer.subscribe(price=price.id) + + stripe_sub = stripe.Subscription.retrieve(subscription.id) + assert stripe_sub is not None + assert stripe_sub.status == "active" + assert stripe_sub.customer == customer.id + + # cancel the subscription + subscription.cancel() + stripe_sub = stripe.Subscription.retrieve(subscription.id) + assert stripe_sub is not None + assert stripe_sub.status == "canceled" + + def test_subscription_plan_list(self): + for i in range(3): + create_subscription_plan(2000 + i, "month", product_name=f"Sample Product Name: {i}", currency="usd") + + url = reverse('prices-list') + resp = self.client.get(url) + assert resp.status_code == 200 + assert len(resp.data) == 3 diff --git a/tests/integration/utils.py b/tests/integration/utils.py new file mode 100644 index 0000000..a5b9f45 --- /dev/null +++ b/tests/integration/utils.py @@ -0,0 +1,40 @@ +import stripe +from djstripe.models import Product, Price, Plan + + +def create_subscription_plan(amount, interval, interval_count=1, currency="usd", product_name="Sample Product Name"): + # product, created = Product.get_or_create( + # name=product_name, + # description="Sample Description", + # type="service", + # ) + stripe_product = stripe.Product.create( + name=product_name, + description="Sample Description", + ) + product = Product.sync_from_stripe_data(stripe_product) + + price = Price.create( + unit_amount=amount, + currency=currency, + recurring={ + "interval": interval, + "interval_count": interval_count, + }, + product=product, + active=True, + ) + from pprint import pprint + pprint(price) + + # print(price) + # print(created) + # plan, created = Plan.objects.get_or_create( + # active=True, + # amount=amount, + # interval=interval, + # interval_count=interval_count, + # product=product, + # currency=currency, + # ) + return price From 493f18718f3426f53b4abf94859a3cc573553106 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Fri, 29 Dec 2023 12:43:51 -0800 Subject: [PATCH 08/22] add views to subscribe --- ckc/serializers.py | 83 ------------------ ckc/stripe/__init__.py | 0 ckc/stripe/serializers.py | 90 ++++++++++++++++++++ ckc/stripe/utils/__init__.py | 0 ckc/{ => stripe}/utils/payments.py | 0 ckc/stripe/utils/subscriptions.py | 39 +++++++++ ckc/stripe/views.py | 53 ++++++++++++ ckc/views.py | 25 ------ testproject/urls.py | 3 +- tests/integration/test_payment_processing.py | 29 +++++-- tests/integration/utils.py | 40 --------- 11 files changed, 205 insertions(+), 157 deletions(-) create mode 100644 ckc/stripe/__init__.py create mode 100644 ckc/stripe/serializers.py create mode 100644 ckc/stripe/utils/__init__.py rename ckc/{ => stripe}/utils/payments.py (100%) create mode 100644 ckc/stripe/utils/subscriptions.py create mode 100644 ckc/stripe/views.py delete mode 100644 ckc/views.py delete mode 100644 tests/integration/utils.py diff --git a/ckc/serializers.py b/ckc/serializers.py index 3e61f36..d00dde0 100644 --- a/ckc/serializers.py +++ b/ckc/serializers.py @@ -1,8 +1,3 @@ -import stripe -from djstripe.models import PaymentMethod, Customer, Price, Product - -from rest_framework import serializers - class DefaultCreatedByMixin: """This will automatically set `YourModel.created_by` to `request.user`. To override which attribute the user is written to, add a `user_field` to your classes Meta information @@ -30,81 +25,3 @@ def create(self, validated_data): 'and overwrote context?') validated_data[user_field] = self.context['request'].user return super().create(validated_data) - - -class PaymentMethodSerializer(serializers.ModelSerializer): - token = serializers.CharField(write_only=True) - - class Meta: - model = PaymentMethod - fields = ( - 'pm_id', - 'id', - 'type', - - # 'customer', - # 'stripe_id', - # 'card_brand', - # 'card_last4', - # 'card_exp_month', - # 'card_exp_year', - # 'is_default', - # 'created', - # 'modified', - ) - read_only_fields = ( - 'id', - 'type', - # 'customer', - # 'stripe_id', - # 'card_brand', - # 'card_last4', - # 'card_exp_month', - # 'card_exp_year', - # 'is_default', - # 'created', - # 'modified', - ) - - def create(self, validated_data): - customer, created = Customer.get_or_create(subscriber=self.context['request'].user) - try: - payment_method = customer.add_payment_method(validated_data['pm_id']) - except (stripe.error.InvalidRequestError) as e: - raise serializers.ValidationError(e) - - return payment_method - - -class ProductSerializer(serializers.ModelSerializer): - class Meta: - model = Product - fields = ( - 'id', - 'name', - 'description', - 'type', - ) - read_only_fields = ( - 'id', - 'name', - 'description', - 'type', - ) - - -class PriceSerializer(serializers.ModelSerializer): - class Meta: - model = Price - fields = ( - 'id', - 'unit_amount', - 'currency', - 'recurring', - ) - read_only_fields = ( - 'id', - 'unit_amount', - 'currency', - 'recurring', - ) diff --git a/ckc/stripe/__init__.py b/ckc/stripe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckc/stripe/serializers.py b/ckc/stripe/serializers.py new file mode 100644 index 0000000..d54de1e --- /dev/null +++ b/ckc/stripe/serializers.py @@ -0,0 +1,90 @@ +import stripe +from djstripe.models import PaymentMethod, Customer, Price, Product + +from rest_framework import serializers + +class PaymentMethodSerializer(serializers.ModelSerializer): + token = serializers.CharField(write_only=True) + + class Meta: + model = PaymentMethod + fields = ( + 'pm_id', + 'id', + 'type', + + # 'customer', + # 'stripe_id', + # 'card_brand', + # 'card_last4', + # 'card_exp_month', + # 'card_exp_year', + # 'is_default', + # 'created', + # 'modified', + ) + read_only_fields = ( + 'id', + 'type', + # 'customer', + # 'stripe_id', + # 'card_brand', + # 'card_last4', + # 'card_exp_month', + # 'card_exp_year', + # 'is_default', + # 'created', + # 'modified', + ) + + def create(self, validated_data): + customer, created = Customer.get_or_create(subscriber=self.context['request'].user) + try: + payment_method = customer.add_payment_method(validated_data['pm_id']) + except (stripe.error.InvalidRequestError) as e: + raise serializers.ValidationError(e) + + return payment_method + + +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = ( + 'id', + 'name', + 'description', + 'type', + ) + read_only_fields = ( + 'id', + 'name', + 'description', + 'type', + ) + + +class PriceSerializer(serializers.ModelSerializer): + class Meta: + model = Price + fields = ( + 'id', + 'unit_amount', + 'currency', + 'recurring', + ) + read_only_fields = ( + 'id', + 'unit_amount', + 'currency', + 'recurring', + ) + + +class SubscribeSerializer(serializers.Serializer): + price_id = serializers.CharField() + + class Meta: + fields = ( + 'price_id' + ) diff --git a/ckc/stripe/utils/__init__.py b/ckc/stripe/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckc/utils/payments.py b/ckc/stripe/utils/payments.py similarity index 100% rename from ckc/utils/payments.py rename to ckc/stripe/utils/payments.py diff --git a/ckc/stripe/utils/subscriptions.py b/ckc/stripe/utils/subscriptions.py new file mode 100644 index 0000000..7345cba --- /dev/null +++ b/ckc/stripe/utils/subscriptions.py @@ -0,0 +1,39 @@ +import stripe +from djstripe.models import Price, Product + + +def create_price(amount, interval, interval_count=1, currency="usd", product_name="Sample Product Name", **kwargs): + """ + create and return a stripe price object + @param amount: the amount to charge + @param interval: the interval to charge at + @param interval_count: the number of intervals to charge at + @param currency: the currency to charge in + @param product_name: the name of the product to create + @param kwargs: additional arguments to pass to the stripe.Product.create method + @returns stripe.Price + + """ + stripe_product = stripe.Product.create( + name=product_name, + description="Sample Description", + ) + product = Product.sync_from_stripe_data(stripe_product) + recurring = kwargs.pop("recurring", {}) + recurring.update({ + "interval": interval, + "interval_count": interval_count, + }) + price = Price.create( + unit_amount=amount, + currency=currency, + recurring={ + "interval": interval, + "interval_count": interval_count, + }, + product=product, + active=True, + **kwargs + ) + + return price diff --git a/ckc/stripe/views.py b/ckc/stripe/views.py new file mode 100644 index 0000000..bbc6c71 --- /dev/null +++ b/ckc/stripe/views.py @@ -0,0 +1,53 @@ +from djstripe.models import PaymentMethod, Price, Plan, Customer +from rest_framework import viewsets, mixins +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.response import Response + +from ckc.stripe.serializers import PaymentMethodSerializer, PriceSerializer, SubscribeSerializer + + +class PaymentMethodViewSet(viewsets.ModelViewSet): + serializer_class = PaymentMethodSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + qs = PaymentMethod.objects.filter(customer__subscriber=self.request.user) + return qs + + +class PriceViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin): + serializer_class = PriceSerializer + permission_classes = [AllowAny] + + def get_queryset(self): + qs = Price.objects.all() + return qs + + +class SubscribeViewSet(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + def get_serialzer_class(self): + if self.action == 'subscribe': + return SubscribeSerializer + + @action(methods=['post'], detail=False) + def subscribe(self, request): + # get stripe customer + customer, created = Customer.get_or_create(subscriber=request.user) + if customer.subscription: + return Response(status=400, data={'error': 'already subscribed'}) + + serializer = SubscribeSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + customer.subscribe(price=serializer.data['price_id']) + return Response(status=204) + + @action(methods=['post'], detail=False) + def cancel(self, request): + # get stripe customer + customer, created = Customer.get_or_create(subscriber=request.user) + customer.subscription.cancel() + return Response(status=204) diff --git a/ckc/views.py b/ckc/views.py deleted file mode 100644 index d044308..0000000 --- a/ckc/views.py +++ /dev/null @@ -1,25 +0,0 @@ -from djstripe.models import PaymentMethod, Price, Plan -from rest_framework import viewsets, mixins -from rest_framework.permissions import IsAuthenticated, AllowAny - -from ckc.serializers import PaymentMethodSerializer, PriceSerializer - - -class PaymentMethodViewSet(viewsets.ModelViewSet): - queryset = PaymentMethod.objects.all() - serializer_class = PaymentMethodSerializer - permission_classes = [IsAuthenticated] - - def get_queryset(self): - qs = PaymentMethod.objects.filter(customer__subscriber=self.request.user) - return qs - - -class PriceViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin): - queryset = Price.objects.all() - serializer_class = PriceSerializer - permission_classes = [AllowAny] - - def get_queryset(self): - qs = Price.objects.all() - return qs diff --git a/testproject/urls.py b/testproject/urls.py index d324a0a..7529e5c 100644 --- a/testproject/urls.py +++ b/testproject/urls.py @@ -1,7 +1,7 @@ from django.urls import path from rest_framework import routers -from ckc.views import PaymentMethodViewSet, PriceViewSet +from ckc.stripe.views import PaymentMethodViewSet, PriceViewSet, SubscribeViewSet from testapp.views import TestExceptionsViewSet from testapp.viewsets import TestModelWithACreatorViewSet, TestModelWithADifferentNamedCreatorViewSet, BModelViewSet @@ -13,6 +13,7 @@ router.register(r'payment-methods', PaymentMethodViewSet, basename='payment-methods') # router.register(r'subscription-plans', SubscriptionPlanViewSet, basename='subscription-plans') router.register(r'prices', PriceViewSet, basename='prices') +router.register(r'subscriptions', SubscribeViewSet, basename='subscriptions') urlpatterns = router.urls + [ path('test-exceptions/', TestExceptionsViewSet.as_view(), name='test-exceptions'), diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 3fcabd8..092e261 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -2,14 +2,15 @@ import stripe from django.urls import reverse -from djstripe.models import PaymentMethod, Customer, Price, Product +from djstripe.models import PaymentMethod, Customer +from djstripe.sync import sync_subscriber # from djstripe.core import Price from rest_framework.test import APITestCase from django.contrib.auth import get_user_model -from ckc.utils.payments import create_checkout_session, create_payment_intent, confirm_payment_intent -from tests.integration.utils import create_subscription_plan +from ckc.stripe.utils.payments import create_checkout_session, create_payment_intent, confirm_payment_intent +from ckc.stripe.utils.subscriptions import create_price User = get_user_model() @@ -82,14 +83,21 @@ def test_payment_intents(self): def test_subscriptions(self): # create the subscription plan through dj stripe price object - price = create_subscription_plan(2000, "month", product_name="Sample Product Name: 0", currency="usd") + price = create_price(2000, "month", product_name="Sample Product Name: 0", currency="usd") assert price is not None assert price.id is not None customer, created = Customer.get_or_create(subscriber=self.user) customer.add_payment_method("pm_card_visa") # subscribe the customer to the plan - subscription = customer.subscribe(price=price.id) + url = reverse('subscriptions-subscribe') + payload = {"price_id": price.id} + resp = self.client.post(url, data=payload, format='json') + assert resp.status_code == 204 + + customer, created = Customer.get_or_create(subscriber=self.user) + subscription = customer.subscription + assert subscription stripe_sub = stripe.Subscription.retrieve(subscription.id) assert stripe_sub is not None @@ -97,14 +105,19 @@ def test_subscriptions(self): assert stripe_sub.customer == customer.id # cancel the subscription - subscription.cancel() - stripe_sub = stripe.Subscription.retrieve(subscription.id) + url = reverse('subscriptions-cancel') + resp = self.client.post(url, format='json') + assert resp.status_code == 204 + customer = sync_subscriber(self.user) + subscription = customer.subscription + assert not subscription + stripe_sub = stripe.Subscription.retrieve(stripe_sub.id) assert stripe_sub is not None assert stripe_sub.status == "canceled" def test_subscription_plan_list(self): for i in range(3): - create_subscription_plan(2000 + i, "month", product_name=f"Sample Product Name: {i}", currency="usd") + create_price(2000 + i, "month", product_name=f"Sample Product Name: {i}", currency="usd") url = reverse('prices-list') resp = self.client.get(url) diff --git a/tests/integration/utils.py b/tests/integration/utils.py deleted file mode 100644 index a5b9f45..0000000 --- a/tests/integration/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -import stripe -from djstripe.models import Product, Price, Plan - - -def create_subscription_plan(amount, interval, interval_count=1, currency="usd", product_name="Sample Product Name"): - # product, created = Product.get_or_create( - # name=product_name, - # description="Sample Description", - # type="service", - # ) - stripe_product = stripe.Product.create( - name=product_name, - description="Sample Description", - ) - product = Product.sync_from_stripe_data(stripe_product) - - price = Price.create( - unit_amount=amount, - currency=currency, - recurring={ - "interval": interval, - "interval_count": interval_count, - }, - product=product, - active=True, - ) - from pprint import pprint - pprint(price) - - # print(price) - # print(created) - # plan, created = Plan.objects.get_or_create( - # active=True, - # amount=amount, - # interval=interval, - # interval_count=interval_count, - # product=product, - # currency=currency, - # ) - return price From 076cb28018b26cc846692ff752db8fbd65417c04 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Fri, 29 Dec 2023 12:50:37 -0800 Subject: [PATCH 09/22] bug fix --- ckc/stripe/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckc/stripe/serializers.py b/ckc/stripe/serializers.py index d54de1e..81f17a2 100644 --- a/ckc/stripe/serializers.py +++ b/ckc/stripe/serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers class PaymentMethodSerializer(serializers.ModelSerializer): - token = serializers.CharField(write_only=True) + pm_id = serializers.CharField(write_only=True) class Meta: model = PaymentMethod From 919c664b54e461fee74002164a268b1a36d8554e Mon Sep 17 00:00:00 2001 From: Eric Carmichael Date: Fri, 29 Dec 2023 13:08:43 -0800 Subject: [PATCH 10/22] Add env vars to tests --- .github/workflows/pythonpackage.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index f9eba92..ed1bd64 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -4,6 +4,10 @@ on: push: branches: [ '*' ] +env: + STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }} + STRIPE_PRIVATE_KEY: ${{ secrets.STRIPE_PRIVATE_KEY }} + jobs: build: From b0a187f7a10e18e9bdf47f9d9176587c9f52216f Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 11 Jan 2024 11:14:08 -0800 Subject: [PATCH 11/22] improve price test --- ckc/stripe/serializers.py | 2 ++ ckc/stripe/views.py | 2 +- tests/integration/test_payment_processing.py | 11 +++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ckc/stripe/serializers.py b/ckc/stripe/serializers.py index 81f17a2..2cddd71 100644 --- a/ckc/stripe/serializers.py +++ b/ckc/stripe/serializers.py @@ -72,12 +72,14 @@ class Meta: 'unit_amount', 'currency', 'recurring', + 'nickname', ) read_only_fields = ( 'id', 'unit_amount', 'currency', 'recurring', + 'nickname', ) diff --git a/ckc/stripe/views.py b/ckc/stripe/views.py index bbc6c71..9eb0af8 100644 --- a/ckc/stripe/views.py +++ b/ckc/stripe/views.py @@ -1,4 +1,4 @@ -from djstripe.models import PaymentMethod, Price, Plan, Customer +from djstripe.models import PaymentMethod, Price, Customer from rest_framework import viewsets, mixins from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated, AllowAny diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 092e261..28c0c2e 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -2,7 +2,7 @@ import stripe from django.urls import reverse -from djstripe.models import PaymentMethod, Customer +from djstripe.models import PaymentMethod, Customer, Price from djstripe.sync import sync_subscriber # from djstripe.core import Price from rest_framework.test import APITestCase @@ -117,9 +117,16 @@ def test_subscriptions(self): def test_subscription_plan_list(self): for i in range(3): - create_price(2000 + i, "month", product_name=f"Sample Product Name: {i}", currency="usd") + prod_name = f"Sample Product Name: {i}" + create_price(2000 + i, "month", product_name=prod_name, nickname=prod_name, currency="usd") url = reverse('prices-list') resp = self.client.get(url) assert resp.status_code == 200 assert len(resp.data) == 3 + from pprint import pprint + pprint(resp.data) + + for i in range(3): + assert resp.data[i]['unit_amount'] / 100 == 2000 + i + assert resp.data[i]['nickname'] == f"Sample Product Name: {i}" From a8a11d84f1171d96bf5c6f701510dc3f8679dc9d Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 11 Jan 2024 11:34:55 -0800 Subject: [PATCH 12/22] embelesh readme --- README.md | 40 ++++++++++++++++++++ testproject/urls.py | 1 - tests/integration/test_payment_processing.py | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 66a7bb3..f6b4415 100644 --- a/README.md +++ b/README.md @@ -181,3 +181,43 @@ class TestExceptionsViewSet(APIView): | command | description| | :--- | :----: | | `upload_file ` | uses `django-storages` settings to upload a file | + +### djstripe + +#### Create and charge a payment intent +```py +from ckc.stripe.utils.payments import create_payment_intent, confirm_payment_intent +#for manual control +intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="manual") +response_data, status_code = confirm_payment_intent(intent.id) +# alternatively, you can have stripe auto charge the intent +intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="automatic") +``` + +#### setting up a subscription plan +A subscription plan is a product with a recurring price. We will create a price and supply it with product info. the product will be auto created. You can create a plan with the following code: + +```py +from ckc.stripe.utils.subscriptions import create_price +price = create_price(2000, "month", product_name="Sample Product Name: 0", currency="usd") +``` + +subscribing a user to a subscription using a Price object +using the `subsciptions` endpoint you a user can be subscribed to a plan. + +note: you will need to setup a payment method for the user before subscribing them to a plan. see below for more info +```js +// REQUEST from a signed in user that wishes to subscribe to a plan +axios.post("/subscriptions/subscribe/", { price_id: price.id }) +``` + +#### Creating a payment method +using the stripe card element on the frontend, obtain a payment method id. and pass it up to the frontend to attach to a customer +```js +// REQUEST from a signed in user that wishes to create a payment method +axios.post("/payment-methods/", { pm_id: pm.id }) +``` + + + + diff --git a/testproject/urls.py b/testproject/urls.py index 7529e5c..64d3d58 100644 --- a/testproject/urls.py +++ b/testproject/urls.py @@ -11,7 +11,6 @@ router.register(r'creators-alternative', TestModelWithADifferentNamedCreatorViewSet) router.register(r'bmodel', BModelViewSet) router.register(r'payment-methods', PaymentMethodViewSet, basename='payment-methods') -# router.register(r'subscription-plans', SubscriptionPlanViewSet, basename='subscription-plans') router.register(r'prices', PriceViewSet, basename='prices') router.register(r'subscriptions', SubscribeViewSet, basename='subscriptions') diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 28c0c2e..adf3810 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -2,7 +2,7 @@ import stripe from django.urls import reverse -from djstripe.models import PaymentMethod, Customer, Price +from djstripe.models import PaymentMethod, Customer from djstripe.sync import sync_subscriber # from djstripe.core import Price from rest_framework.test import APITestCase From 94ffc1d438cccef75e2760878f4fad4dfcd56ee3 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 11 Jan 2024 11:36:48 -0800 Subject: [PATCH 13/22] flake --- tests/integration/test_payment_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index adf3810..f603e03 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -77,7 +77,7 @@ def test_payment_intents(self): assert json.loads(response_data).get('success', False) # automatic payment intent confirmation - intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="automatic") + intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="automatic") assert intent is not None assert intent.status == "succeeded" From f87855fef16318d19206b11ec4b10a8f24a44965 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 11 Jan 2024 11:40:06 -0800 Subject: [PATCH 14/22] flake --- .github/workflows/pythonpackage.yml | 2 +- ckc/stripe/serializers.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index ed1bd64..5eedbc0 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: python-version: [3.10.x, 3.11.x, 3.12.x] - django-version: ['<4', '>=4', '>=5'] + django-version: ['<4', '>4,<5', '>=5'] steps: - uses: actions/checkout@v2 diff --git a/ckc/stripe/serializers.py b/ckc/stripe/serializers.py index 2cddd71..145b7a5 100644 --- a/ckc/stripe/serializers.py +++ b/ckc/stripe/serializers.py @@ -3,6 +3,7 @@ from rest_framework import serializers + class PaymentMethodSerializer(serializers.ModelSerializer): pm_id = serializers.CharField(write_only=True) From 36615d1a1a207d4b398c4f2b503fc6486d3f5e08 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 11 Jan 2024 11:44:52 -0800 Subject: [PATCH 15/22] flake --- ckc/serializers.py | 11 +++++++---- ckc/stripe/serializers.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ckc/serializers.py b/ckc/serializers.py index d00dde0..4e19bd6 100644 --- a/ckc/serializers.py +++ b/ckc/serializers.py @@ -16,12 +16,15 @@ def create(self, validated_data): # get name of the user field we'll be writing request.user to, default created_by user_field = getattr(self.Meta, 'user_field', 'created_by') - assert hasattr(self.Meta.model, user_field), f"{self.Meta.model} needs to have field {user_field} so " \ - f"DefaultCreatedByMixin can write to it" + assert \ + hasattr(self.Meta.model, user_field), \ + f"{self.Meta.model} needs to have field {user_field} so " f"DefaultCreatedByMixin can write to it" if user_field not in validated_data: if 'request' not in self.context: - raise Exception('self.context does not contain "request". Have you overwritten get_serializer_context ' - 'and overwrote context?') + raise Exception( + 'self.context does not contain "request". ' + 'Have you overwritten get_serializer_context and overwrote context?' + ) validated_data[user_field] = self.context['request'].user return super().create(validated_data) diff --git a/ckc/stripe/serializers.py b/ckc/stripe/serializers.py index 145b7a5..9933d55 100644 --- a/ckc/stripe/serializers.py +++ b/ckc/stripe/serializers.py @@ -42,7 +42,7 @@ def create(self, validated_data): customer, created = Customer.get_or_create(subscriber=self.context['request'].user) try: payment_method = customer.add_payment_method(validated_data['pm_id']) - except (stripe.error.InvalidRequestError) as e: + except stripe.error.InvalidRequestError as e: raise serializers.ValidationError(e) return payment_method From a780bbb5bb66c8ae828bf5bb35eb3bcc42f812b5 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Thu, 11 Jan 2024 11:52:06 -0800 Subject: [PATCH 16/22] env vars --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f6b4415..3a70d85 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,11 @@ class TestExceptionsViewSet(APIView): | `upload_file ` | uses `django-storages` settings to upload a file | ### djstripe +#### env vars +```bash +STRIPE_PUBLIC_KEY=sk_test_... +STRIPE_PRIVATE_KEY=pk_test_... +``` #### Create and charge a payment intent ```py From 2a1808b2e252dd0a9da8eca3ae91d00039e94782 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Fri, 12 Jan 2024 10:45:45 -0800 Subject: [PATCH 17/22] setup signals --- README.md | 27 ++++++++++---------- ckc/stripe/{utils => }/payments.py | 3 ++- ckc/stripe/signals.py | 7 +++++ ckc/stripe/{utils => }/subscriptions.py | 12 ++++++--- ckc/stripe/utils/__init__.py | 0 ckc/stripe/views.py | 8 ++++-- setup.cfg | 3 +-- testproject/testapp/apps.py | 10 ++++++++ testproject/testapp/models.py | 8 ++++++ testproject/testapp/signal_handlers.py | 20 +++++++++++++++ tests/integration/test_payment_processing.py | 15 ++++++++--- 11 files changed, 86 insertions(+), 27 deletions(-) rename ckc/stripe/{utils => }/payments.py (96%) create mode 100644 ckc/stripe/signals.py rename ckc/stripe/{utils => }/subscriptions.py (81%) delete mode 100644 ckc/stripe/utils/__init__.py create mode 100644 testproject/testapp/apps.py create mode 100644 testproject/testapp/signal_handlers.py diff --git a/README.md b/README.md index 3a70d85..dc633ca 100644 --- a/README.md +++ b/README.md @@ -176,34 +176,31 @@ class TestExceptionsViewSet(APIView): raise SnackbarError("Something went wrong") ``` -#### `./manage.py` commands - -| command | description| -| :--- | :----: | -| `upload_file ` | uses `django-storages` settings to upload a file | - -### djstripe +### Payment helpers ([dj-stripe](https://dj-stripe.dev/)) #### env vars ```bash STRIPE_PUBLIC_KEY=sk_test_... STRIPE_PRIVATE_KEY=pk_test_... ``` -#### Create and charge a payment intent +#### Create and charge a payment intent + ```py -from ckc.stripe.utils.payments import create_payment_intent, confirm_payment_intent -#for manual control +from ckc.stripe.payments import create_payment_intent, confirm_payment_intent + +# for manual control intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="manual") response_data, status_code = confirm_payment_intent(intent.id) # alternatively, you can have stripe auto charge the intent -intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="automatic") +intent = create_payment_intent(payment_method.id, customer.id, 2000, confirmation_method="automatic") ``` #### setting up a subscription plan A subscription plan is a product with a recurring price. We will create a price and supply it with product info. the product will be auto created. You can create a plan with the following code: ```py -from ckc.stripe.utils.subscriptions import create_price +from ckc.stripe.subscriptions import create_price + price = create_price(2000, "month", product_name="Sample Product Name: 0", currency="usd") ``` @@ -223,6 +220,8 @@ using the stripe card element on the frontend, obtain a payment method id. and p axios.post("/payment-methods/", { pm_id: pm.id }) ``` +#### `./manage.py` commands - - +| command | description| +| :--- | :----: | +| `upload_file ` | uses `django-storages` settings to upload a file | diff --git a/ckc/stripe/utils/payments.py b/ckc/stripe/payments.py similarity index 96% rename from ckc/stripe/utils/payments.py rename to ckc/stripe/payments.py index e0155bd..97e0a3f 100644 --- a/ckc/stripe/utils/payments.py +++ b/ckc/stripe/payments.py @@ -4,6 +4,7 @@ from djstripe.models import Customer from django.conf import settings +from rest_framework.exceptions import ValidationError def create_checkout_session(user, success_url, cancel_url, line_items, metadata=None, payment_method_types=None): @@ -90,7 +91,7 @@ def create_payment_intent(payment_method_id, customer_id, amount, currency="usd" ) except stripe.error.CardError: - pass + raise ValidationError("Error encountered while creating payment intent") return intent diff --git a/ckc/stripe/signals.py b/ckc/stripe/signals.py new file mode 100644 index 0000000..00e3b98 --- /dev/null +++ b/ckc/stripe/signals.py @@ -0,0 +1,7 @@ +from django.dispatch import Signal + +# Define a signal for post-subscription +post_subscribe = Signal() + +# Define a signal for post-cancellation +post_cancel = Signal() diff --git a/ckc/stripe/utils/subscriptions.py b/ckc/stripe/subscriptions.py similarity index 81% rename from ckc/stripe/utils/subscriptions.py rename to ckc/stripe/subscriptions.py index 7345cba..7eb6038 100644 --- a/ckc/stripe/utils/subscriptions.py +++ b/ckc/stripe/subscriptions.py @@ -14,10 +14,14 @@ def create_price(amount, interval, interval_count=1, currency="usd", product_nam @returns stripe.Price """ - stripe_product = stripe.Product.create( - name=product_name, - description="Sample Description", - ) + try: + + stripe_product = stripe.Product.create( + name=product_name, + description="Sample Description", + ) + except stripe.error.StripeError: + raise ValueError("Error creating Stripe Product") product = Product.sync_from_stripe_data(stripe_product) recurring = kwargs.pop("recurring", {}) recurring.update({ diff --git a/ckc/stripe/utils/__init__.py b/ckc/stripe/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ckc/stripe/views.py b/ckc/stripe/views.py index 9eb0af8..0b4c381 100644 --- a/ckc/stripe/views.py +++ b/ckc/stripe/views.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from ckc.stripe.serializers import PaymentMethodSerializer, PriceSerializer, SubscribeSerializer +from ckc.stripe.signals import post_subscribe, post_cancel class PaymentMethodViewSet(viewsets.ModelViewSet): @@ -42,12 +43,15 @@ def subscribe(self, request): serializer = SubscribeSerializer(data=request.data) serializer.is_valid(raise_exception=True) - customer.subscribe(price=serializer.data['price_id']) + subscription = customer.subscribe(price=serializer.data['price_id']) + post_subscribe.send(sender=self.__class__, subscription=subscription, user=request.user) return Response(status=204) @action(methods=['post'], detail=False) def cancel(self, request): # get stripe customer customer, created = Customer.get_or_create(subscriber=request.user) - customer.subscription.cancel() + subscription = customer.subscription + subscription.cancel() + post_cancel.send(sender=self.__class__, subscription=subscription, user=request.user) return Response(status=204) diff --git a/setup.cfg b/setup.cfg index 8c825cd..f97b414 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,5 +63,4 @@ packages = find: zip_safe: False [options.extras_require] -stripe = - djstripe>=2.8.3 +stripe = djstripe>=2.8.3 diff --git a/testproject/testapp/apps.py b/testproject/testapp/apps.py new file mode 100644 index 0000000..7922782 --- /dev/null +++ b/testproject/testapp/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class MyAppConfig(AppConfig): + name = 'testapp' + + def ready(self): + # Import and register signal handlers here + print(dir()) + from . import signal_handlers # noqa diff --git a/testproject/testapp/models.py b/testproject/testapp/models.py index b4459fe..f17317c 100644 --- a/testproject/testapp/models.py +++ b/testproject/testapp/models.py @@ -1,6 +1,7 @@ from django.contrib.auth import get_user_model from django.contrib.gis.db.models import PointField from django.db import models +from djstripe.models import Subscription from ckc.models import SoftDeletableModel, JsonSnapshotModel @@ -55,3 +56,10 @@ class SnapshottedModelMissingOverride(JsonSnapshotModel, models.Model): # No _create_json_snapshot here! This is for testing purposes, to confirm we raise # an assertion when this method is missing pass + +# ---------------------------------------------------------------------------- +# For testing Subscription signals +# ---------------------------------------------------------------------------- +class SubscriptionThroughModel(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE) diff --git a/testproject/testapp/signal_handlers.py b/testproject/testapp/signal_handlers.py new file mode 100644 index 0000000..1cb39a1 --- /dev/null +++ b/testproject/testapp/signal_handlers.py @@ -0,0 +1,20 @@ +from django.core.exceptions import ValidationError +from django.dispatch import receiver + +from ckc.stripe.signals import post_subscribe, post_cancel +from ckc.stripe.views import SubscribeViewSet +from testapp.models import SubscriptionThroughModel + + +@receiver(post_subscribe, sender=SubscribeViewSet) +def subscribe_signal_handler(sender, **kwargs): + """ example function for how to define a post subscribe signal handler. """ + if sender != SubscribeViewSet: + raise ValidationError('sender must be SubscribeViewSet') + SubscriptionThroughModel.objects.get_or_create(user=kwargs['user'], subscription=kwargs['subscription']) + +@receiver(post_cancel, sender=SubscribeViewSet) +def cancel_signal_handler(sender, **kwargs): + if sender != SubscribeViewSet: + raise ValidationError('sender must be SubscribeViewSet') + SubscriptionThroughModel.objects.filter(user=kwargs['user'], subscription=kwargs['subscription']).delete() diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index f603e03..94c916c 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -1,4 +1,5 @@ import json +from unittest.mock import patch import stripe from django.urls import reverse @@ -9,17 +10,21 @@ from django.contrib.auth import get_user_model -from ckc.stripe.utils.payments import create_checkout_session, create_payment_intent, confirm_payment_intent -from ckc.stripe.utils.subscriptions import create_price +from ckc.stripe.payments import create_checkout_session, create_payment_intent, confirm_payment_intent +from ckc.stripe.subscriptions import create_price +from testapp.models import SubscriptionThroughModel User = get_user_model() class TestPaymentProcessing(APITestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username="test", password="test") + cls.customer, cls.created = Customer.get_or_create(subscriber=cls.user) + def setUp(self): - self.user = User.objects.create_user(username="test", password="test") self.client.force_authenticate(user=self.user) - return super().setUp() def test_payment_method(self): # simulate card being created on the frontend @@ -98,6 +103,7 @@ def test_subscriptions(self): customer, created = Customer.get_or_create(subscriber=self.user) subscription = customer.subscription assert subscription + assert SubscriptionThroughModel.objects.count() == 1 stripe_sub = stripe.Subscription.retrieve(subscription.id) assert stripe_sub is not None @@ -114,6 +120,7 @@ def test_subscriptions(self): stripe_sub = stripe.Subscription.retrieve(stripe_sub.id) assert stripe_sub is not None assert stripe_sub.status == "canceled" + assert SubscriptionThroughModel.objects.count() == 0 def test_subscription_plan_list(self): for i in range(3): From c9927494d1248b8b40fbdec20d88bdd0b1ed2f9e Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Fri, 12 Jan 2024 10:51:22 -0800 Subject: [PATCH 18/22] readme --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dc633ca..069947a 100644 --- a/README.md +++ b/README.md @@ -203,8 +203,35 @@ from ckc.stripe.subscriptions import create_price price = create_price(2000, "month", product_name="Sample Product Name: 0", currency="usd") ``` +#### setting up signal handlers +there are two signals that can be used. `post_subscribe` and `post_cancel`. you can use them like so: -subscribing a user to a subscription using a Price object +in signal_handlers.py +```py + +from django.dispatch import receiver +from ckc.stripe.signals import post_subscribe +from ckc.stripe.views import SubscribeViewSet + + +@receiver(post_subscribe, sender=SubscribeViewSet) +def subscribe_signal_handler(sender, **kwargs): + # your custom logic. + # kwargs will contain the following: + # user: the user that was subscribed + # subscription: the subscription object + pass +``` +in apps.py +```py +from django.apps import AppConfig +class YourAppConfig(AppConfig): + name = "your_app" + def ready(self): + import your_app.signal_handlers +``` + +#### subscribing a user to a subscription using a Price object using the `subsciptions` endpoint you a user can be subscribed to a plan. note: you will need to setup a payment method for the user before subscribing them to a plan. see below for more info From a3a67c0542f284b892bca01d2cba548f8e5fba38 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Tue, 16 Jan 2024 10:58:02 -0800 Subject: [PATCH 19/22] change version --- requirements.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0074cbe..58bc1a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,4 @@ pytest-django==4.5.2 flake8==6.0.0 # payment processing -dj-stripe==2.8.3 +dj-stripe==2.8.1 diff --git a/setup.cfg b/setup.cfg index f97b414..e6be0a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,4 +63,4 @@ packages = find: zip_safe: False [options.extras_require] -stripe = djstripe>=2.8.3 +stripe = djstripe>=2.8.1 From 6f43a1f07ed07b43d9305db67735033cef5aa35a Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Tue, 16 Jan 2024 11:01:18 -0800 Subject: [PATCH 20/22] change version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e6be0a1..c4879ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,4 +63,4 @@ packages = find: zip_safe: False [options.extras_require] -stripe = djstripe>=2.8.1 +stripe = dj-stripe>=2.8.1 From e60147e925356f992a8d4a6b03f0a11f23919329 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Fri, 19 Jan 2024 09:33:58 -0800 Subject: [PATCH 21/22] fix create_price --- README.md | 38 +++++++++++++------- ckc/stripe/subscriptions.py | 34 +++++++++--------- tests/integration/test_payment_processing.py | 23 ++++++++++-- 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 069947a..9986827 100644 --- a/README.md +++ b/README.md @@ -231,21 +231,33 @@ class YourAppConfig(AppConfig): import your_app.signal_handlers ``` -#### subscribing a user to a subscription using a Price object -using the `subsciptions` endpoint you a user can be subscribed to a plan. +[//]: # (#### subscribing a user to a subscription using a Price object) -note: you will need to setup a payment method for the user before subscribing them to a plan. see below for more info -```js -// REQUEST from a signed in user that wishes to subscribe to a plan -axios.post("/subscriptions/subscribe/", { price_id: price.id }) -``` +[//]: # (using the `subsciptions` endpoint you a user can be subscribed to a plan.) -#### Creating a payment method -using the stripe card element on the frontend, obtain a payment method id. and pass it up to the frontend to attach to a customer -```js -// REQUEST from a signed in user that wishes to create a payment method -axios.post("/payment-methods/", { pm_id: pm.id }) -``` +[//]: # () +[//]: # (note: you will need to setup a payment method for the user before subscribing them to a plan. see below for more info ) + +[//]: # (```js) + +[//]: # (// REQUEST from a signed in user that wishes to subscribe to a plan) + +[//]: # (axios.post("/subscriptions/subscribe/", { price_id: price.id })) + +[//]: # (```) + +[//]: # () +[//]: # (#### Creating a payment method) + +[//]: # (using the stripe card element on the frontend, obtain a payment method id. and pass it up to the frontend to attach to a customer) + +[//]: # (```js) + +[//]: # (// REQUEST from a signed in user that wishes to create a payment method) + +[//]: # (axios.post("/payment-methods/", { pm_id: pm.id })) + +[//]: # (```) #### `./manage.py` commands diff --git a/ckc/stripe/subscriptions.py b/ckc/stripe/subscriptions.py index 7eb6038..44ad394 100644 --- a/ckc/stripe/subscriptions.py +++ b/ckc/stripe/subscriptions.py @@ -2,7 +2,8 @@ from djstripe.models import Price, Product -def create_price(amount, interval, interval_count=1, currency="usd", product_name="Sample Product Name", **kwargs): +# def create_price(amount=None, interval=None, interval_count=1, currency="usd", product_name="Sample Product Name", **kwargs): +def create_price(product_kwargs, **price_kwargs): """ create and return a stripe price object @param amount: the amount to charge @@ -17,27 +18,28 @@ def create_price(amount, interval, interval_count=1, currency="usd", product_nam try: stripe_product = stripe.Product.create( - name=product_name, - description="Sample Description", + **product_kwargs, ) except stripe.error.StripeError: raise ValueError("Error creating Stripe Product") product = Product.sync_from_stripe_data(stripe_product) - recurring = kwargs.pop("recurring", {}) - recurring.update({ - "interval": interval, - "interval_count": interval_count, - }) + # recurring = kwargs.pop("recurring", {}) + # recurring.update({ + # "interval": interval, + # "interval_count": interval_count, + # }) price = Price.create( - unit_amount=amount, - currency=currency, - recurring={ - "interval": interval, - "interval_count": interval_count, - }, product=product, - active=True, - **kwargs + **price_kwargs + # unit_amount=amount, + # currency=currency, + # recurring={ + # "interval": interval, + # "interval_count": interval_count, + # }, + # product=product, + # active=True, + # **kwargs ) return price diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 94c916c..2c4b2b4 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -88,7 +88,16 @@ def test_payment_intents(self): def test_subscriptions(self): # create the subscription plan through dj stripe price object - price = create_price(2000, "month", product_name="Sample Product Name: 0", currency="usd") + price = create_price( + dict(name="Sample Product Name: 0", description='sample description'), + amount=2000, + recurring={ + "interval": "month", + "interval_count": 1, + }, + product_name="Sample Product Name: 0", + currency="usd" + ) assert price is not None assert price.id is not None @@ -125,7 +134,17 @@ def test_subscriptions(self): def test_subscription_plan_list(self): for i in range(3): prod_name = f"Sample Product Name: {i}" - create_price(2000 + i, "month", product_name=prod_name, nickname=prod_name, currency="usd") + price = create_price( + dict(name=prod_name, description='sample description'), + recurring={ + "interval": "month", + "interval_count": 1, + }, + nickname=prod_name, + active=True, + amount=2000 + i, + currency="usd" + ) url = reverse('prices-list') resp = self.client.get(url) From fc6645fb0ef975be52bbf7f1f63175b9aafb7a39 Mon Sep 17 00:00:00 2001 From: Harvey Hartwell Date: Mon, 22 Jan 2024 12:09:55 -0800 Subject: [PATCH 22/22] fix test --- tests/integration/test_payment_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_payment_processing.py b/tests/integration/test_payment_processing.py index 2c4b2b4..77e736d 100644 --- a/tests/integration/test_payment_processing.py +++ b/tests/integration/test_payment_processing.py @@ -134,7 +134,7 @@ def test_subscriptions(self): def test_subscription_plan_list(self): for i in range(3): prod_name = f"Sample Product Name: {i}" - price = create_price( + create_price( dict(name=prod_name, description='sample description'), recurring={ "interval": "month",