Skip to content

Commit

Permalink
Implement stripe payments
Browse files Browse the repository at this point in the history
Support subscriptions, webhooks, success URL, etc.
  • Loading branch information
Ivan committed Apr 4, 2023
1 parent 61e53f1 commit b2b53f9
Show file tree
Hide file tree
Showing 13 changed files with 597 additions and 78 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.34] - 2023-04-4

### Added

- Subscriptions support [@AivGitHub](https://github.com/AivGitHub/).

## [0.0.33] - 2023-03-28

### Added
Expand Down
37 changes: 28 additions & 9 deletions api/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
from http import HTTPStatus
import json

from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt

import stripe
from payments.core import stripe
from payments.models import get_payment_instance, Subscription


@method_decorator(csrf_exempt, name='dispatch')
class StripeWebhook(View):
def post(self, request, *args, **kwargs):
try:
sig_header = request.headers['Stripe-Signature']
signature = request.headers['Stripe-Signature']
except KeyError:
return HttpResponse(status=HTTPStatus.FORBIDDEN)

try:
event = stripe.Webhook.construct_event(
request.body, sig_header, settings.STRIPE_ENDPOINT_SECRET
)
event = stripe.Webhook.construct_event(request.body, signature, settings.STRIPE_ENDPOINT_SECRET)
except ValueError as e:
raise PermissionDenied()
except stripe.error.SignatureVerificationError as verification_err:
return HttpResponse(status=HTTPStatus.FORBIDDEN)
except stripe.error.SignatureVerificationError:
return HttpResponse(status=HTTPStatus.FORBIDDEN)

print(event.data.object.object)
if event.type == 'checkout.session.completed':
self.checkout_session_completed(event)
elif event.type == 'invoice.payment_succeeded':
self.invoice_payment_succeeded(event)
elif event.type == 'customer.subscription.updated':
self.customer_subscription_updated(event)

return HttpResponse(status=HTTPStatus.OK)

@staticmethod
def customer_subscription_updated(event: stripe.Event):
payment_instance = Subscription.objects.get(psp_id=event.data.object.id)
payment_instance.update_from_event(event)

@staticmethod
def checkout_session_completed(event: stripe.Event):
payment_instance = get_payment_instance(event)
payment_instance.from_event(event, save=True)

@staticmethod
def invoice_payment_succeeded(event: stripe.Event):
pass
12 changes: 11 additions & 1 deletion payments/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
from django.contrib import admin

from payments.models import Subscription
from payments.models import Price, Product, Subscription


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
pass


@admin.register(Price)
class PriceAdmin(admin.ModelAdmin):
pass


@admin.register(Subscription)
Expand Down
16 changes: 16 additions & 0 deletions payments/migrations/0003_delete_subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 4.1.3 on 2023-04-04 19:48

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('payments', '0002_subscription_psp_id_subscription_psp_reference'),
]

operations = [
migrations.DeleteModel(
name='Subscription',
),
]
76 changes: 76 additions & 0 deletions payments/migrations/0004_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Generated by Django 4.1.3 on 2023-04-04 21:31

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('payments', '0003_delete_subscription'),
]

operations = [
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=False, verbose_name='Active')),
('description', models.CharField(blank=True, max_length=256, null=True, verbose_name='Description')),
('psp_id', models.CharField(max_length=64, verbose_name='PSP ID')),
('metadata', models.JSONField(blank=True, null=True, verbose_name='Metadata')),
('name', models.CharField(blank=True, max_length=256, null=True, verbose_name='Description')),
('object_name', models.CharField(max_length=16, verbose_name='Object name')),
('product_type', models.CharField(max_length=32, verbose_name='Product type')),
],
),
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('psp_id', models.CharField(max_length=64, verbose_name='PSP ID')),
('active', models.BooleanField(default=False, verbose_name='Active')),
('current_period_end', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Current period end')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='payments.product')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Price',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=False, verbose_name='Active')),
('billing_scheme', models.CharField(max_length=128, verbose_name='Billing scheme')),
('currency', models.CharField(max_length=8, verbose_name='Currency')),
('psp_id', models.CharField(max_length=64, verbose_name='PSP ID')),
('metadata', models.JSONField(blank=True, null=True, verbose_name='Metadata')),
('object_name', models.CharField(max_length=16, verbose_name='Object name')),
('recurring', models.JSONField(blank=True, null=True, verbose_name='Recurring')),
('payment_type', models.CharField(max_length=32, verbose_name='Payment type')),
('unit_amount', models.PositiveIntegerField(verbose_name='Unit amount')),
('unit_amount_decimal', models.CharField(max_length=32, verbose_name='Unit amount decimal')),
('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='prices', to='payments.product')),
],
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('psp_id', models.CharField(max_length=64, verbose_name='PSP ID')),
('active', models.BooleanField(default=False, verbose_name='Active')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='payments.product')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]
Loading

0 comments on commit b2b53f9

Please sign in to comment.