From cb631823b2560fcc81c42fd732b8931fff843c51 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Mon, 28 Nov 2022 06:30:43 -0700 Subject: [PATCH] Add basic modelling --- src/core/models.py | 40 ++++++- src/static/scripts/subscriptions.js | 13 ++ .../templates/storefront/sub_payment.html | 112 ++++++++++++++++++ .../templates/storefront/subscriptions.html | 7 ++ src/storefront/views.py | 85 +++++++++++++ 5 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 src/storefront/templates/storefront/sub_payment.html diff --git a/src/core/models.py b/src/core/models.py index f6816f1..5bad9e0 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -166,7 +166,7 @@ class ProductVariant(models.Model): ) name = models.CharField(max_length=255) sku = models.CharField(max_length=255, blank=True) - stripe_id = models.CharField(max_length=255, blank=True) + stripe_id = models.CharField(max_length=255, blank=True, db_index=True) price = models.DecimalField( max_digits=settings.DEFAULT_MAX_DIGITS, decimal_places=settings.DEFAULT_DECIMAL_PLACES, @@ -199,6 +199,44 @@ class ProductVariant(models.Model): ordering = ['sorting', 'weight'] +class StripePrice: + # When this model is updated, it will query stripe and update all of the instances of this model + DAY = 'day' + WEEK = 'week' + MONTH = 'month' + YEAR = 'year' + INTERVAL_CHOICES = [ + (DAY, 'Days'), + (WEEK, 'Weeks'), + (MONTH, 'Month'), + (YEAR, 'Year'), + ] + + stripe_id = models.CharField(max_length=255, blank=True, db_index=True) + variant = models.ForeignKey( + ProductVariant, + related_name='stripe_prices', + on_delete=models.CASCADE, + blank=True, + null=True, + ) + unit_amount = models.DecimalField( + max_digits=settings.DEFAULT_MAX_DIGITS, + decimal_places=settings.DEFAULT_DECIMAL_PLACES, + blank=True, + null=True, + ) + interval = models.CharField( + max_length=16, + choices=INTERVAL_CHOICES, + default=MONTH + ) + interval_count = models.PositiveIntegerField(default=1) + + created_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True) + + class ProductOption(models.Model): """ Description: Consistent accross all variants diff --git a/src/static/scripts/subscriptions.js b/src/static/scripts/subscriptions.js index 61e94f4..d541d08 100644 --- a/src/static/scripts/subscriptions.js +++ b/src/static/scripts/subscriptions.js @@ -121,6 +121,19 @@ class Subscription { this.items.push(item) return this.items } + + createSubscription() { + fetch('/create-subscription', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + priceId: priceId, + customerId: customerId, + }), + }) + } } diff --git a/src/storefront/templates/storefront/sub_payment.html b/src/storefront/templates/storefront/sub_payment.html new file mode 100644 index 0000000..b489747 --- /dev/null +++ b/src/storefront/templates/storefront/sub_payment.html @@ -0,0 +1,112 @@ +{% extends 'base.html' %} +{% load static %} + +{% block head %} + + + +{% endblock %} + +{% block content %} +
+

Subscriptions

+

SUBSCRIBE AND SAVE

+
+
+
+
+
+ +
+ +
+ +
+
+
+
+ +{% endblock %} diff --git a/src/storefront/templates/storefront/subscriptions.html b/src/storefront/templates/storefront/subscriptions.html index a1d034e..a0eac8c 100644 --- a/src/storefront/templates/storefront/subscriptions.html +++ b/src/storefront/templates/storefront/subscriptions.html @@ -2,6 +2,8 @@ {% load static %} {% block head %} + + {% endblock %} @@ -11,6 +13,11 @@

SUBSCRIBE AND SAVE

+
+ {% for product in stripe_products %} + {{ product }} + {% endfor %} +
{% csrf_token %} diff --git a/src/storefront/views.py b/src/storefront/views.py index e8c4ed4..7134324 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -379,6 +379,57 @@ class OrderCreateView(CreateView): return JsonResponse(data) +# stripe listen --forward-to localhost:8000/stripe-webhook +@csrf_exempt +@require_POST +def stripe_webhook(request): + # You can use webhooks to receive information about asynchronous payment events. + # For more about our webhook events check out https://stripe.com/docs/webhooks. + webhook_secret = os.getenv('STRIPE_WEBHOOK_SECRET') + request_data = json.loads(request.data) + + if webhook_secret: + # Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured. + signature = request.headers.get('stripe-signature') + try: + event = stripe.Webhook.construct_event( + payload=request.data, sig_header=signature, secret=webhook_secret) + data = event['data'] + except Exception as e: + return e + # Get the type of webhook event sent - used to check the status of PaymentIntents. + event_type = event['type'] + else: + data = request_data['data'] + event_type = request_data['type'] + + data_object = data['object'] + + if event_type == 'invoice.paid': + # Used to provision services after the trial has ended. + # The status of the invoice will show up as paid. Store the status in your + # database to reference when a user accesses your service to avoid hitting rate + # limits. + messages.success(request, 'Paid') + logger.warning(data) + + if event_type == 'invoice.payment_failed': + # If the payment fails or the customer does not have a valid payment method, + # an invoice.payment_failed event is sent, the subscription becomes past_due. + # Use this webhook to notify your user that their payment has + # failed and to retrieve new card details. + messages.warning(request, 'Payment failed') + logger.warning(data) + + if event_type == 'customer.subscription.deleted': + # handle subscription canceled automatically based + # upon your subscription settings. Or if the user cancels it. + messages.error(request, 'Deleted') + logger.warning(data) + + return jsonify({'status': 'success'}) + + @csrf_exempt @require_POST def paypal_order_transaction_capture(request, transaction_id): @@ -539,13 +590,47 @@ class SubscriptionCreateView(FormView): success_url = reverse_lazy('storefront:payment-done') def get_context_data(self, *args, **kwargs): + stripe.api_key = settings.STRIPE_API_KEY context = super().get_context_data(*args, **kwargs) context['STRIPE_API_KEY'] = settings.STRIPE_API_KEY context['product_list'] = Product.objects.filter( visible_in_listings=True ) + context['stripe_products'] = stripe.Price.list() return context + def form_valid(self, form): + # TODO: Construct items element + items = [] + subscription = self.create_subscription(items) + return super().form_valid(form) + + def create_subscription(self, items): + # items=[{ + # 'price': price_id, + # 'quantity': quantity + # }, { + # 'price': next_price_id, + # 'quantity': quantity + # }] + + try: + # Create the subscription. Note we're expanding the Subscription's + # latest invoice and that invoice's payment_intent + # so we can pass it to the front end to confirm the payment + subscription = stripe.Subscription.create( + customer=self.request.user.stripe_id, + items=items, + payment_behavior='default_incomplete', + payment_settings={'save_default_payment_method': 'on_subscription'}, + expand=['latest_invoice.payment_intent'], + ) + # TODO: pass this secret to the sub_payment.html as 'CLIENT_SECRET' + # clientSecret=subscription.latest_invoice.payment_intent.client_secret + return subscription + except Exception as e: + return messages.error(self.request, e.user_message) + class CreatePayment(View): def post(self, request, *args, **kwargs):