Skip to content

Commit

Permalink
setup signals
Browse files Browse the repository at this point in the history
  • Loading branch information
hhartwell committed Jan 12, 2024
1 parent 7d52c69 commit 2a1808b
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 27 deletions.
27 changes: 13 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,34 +176,31 @@ class TestExceptionsViewSet(APIView):
raise SnackbarError("Something went wrong")
```
#### `./manage.py` commands
| command | description|
| :--- | :----: |
| `upload_file <source> <destination>` | 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")
```
Expand All @@ -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 <source> <destination>` | uses `django-storages` settings to upload a file |
3 changes: 2 additions & 1 deletion ckc/stripe/utils/payments.py → ckc/stripe/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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


Expand Down
7 changes: 7 additions & 0 deletions ckc/stripe/signals.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Empty file removed ckc/stripe/utils/__init__.py
Empty file.
8 changes: 6 additions & 2 deletions ckc/stripe/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
3 changes: 1 addition & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,4 @@ packages = find:
zip_safe: False

[options.extras_require]
stripe =
djstripe>=2.8.3
stripe = djstripe>=2.8.3
10 changes: 10 additions & 0 deletions testproject/testapp/apps.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions testproject/testapp/models.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
20 changes: 20 additions & 0 deletions testproject/testapp/signal_handlers.py
Original file line number Diff line number Diff line change
@@ -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()
15 changes: 11 additions & 4 deletions tests/integration/test_payment_processing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from unittest.mock import patch

import stripe
from django.urls import reverse
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down

0 comments on commit 2a1808b

Please sign in to comment.