From cc851c95c66d8623de77d35af6eeb45f6128a733 Mon Sep 17 00:00:00 2001 From: Vikalp Jain Date: Tue, 27 Dec 2016 18:29:53 +0530 Subject: [PATCH 1/3] Support Python 3 and some updates to support with latest version of Libraries (#1) * Remove Site Dependency from the site. * remove django importlib dependecy and switch to python's import lib * remove django importlib dependecy and switch to python's import lib * Add migrations folder * Update Url files * fix(serializers): Fix all the models serializers to include fields * add json field dependency * Update customer create to take metadata as well --- payments/admin.py | 29 +--- payments/api/serializers.py | 7 + payments/api/urls.py | 8 +- payments/migrations/0001_initial.py | 210 ++++++++++++++++++++++++++++ payments/migrations/__init__.py | 0 payments/models.py | 9 +- payments/settings.py | 6 +- payments/utils.py | 6 +- setup.py | 1 + 9 files changed, 236 insertions(+), 40 deletions(-) create mode 100644 payments/migrations/0001_initial.py create mode 100644 payments/migrations/__init__.py diff --git a/payments/admin.py b/payments/admin.py index a2b9828..41a33b0 100644 --- a/payments/admin.py +++ b/payments/admin.py @@ -14,30 +14,13 @@ from .utils import get_user_model -def user_search_fields(): +def user_search_fields(): # coverage: omit User = get_user_model() - USERNAME_FIELD = getattr(User, "USERNAME_FIELD", None) - fields = [] - if USERNAME_FIELD is not None: - # Using a Django 1.5+ User model - fields = [ - "user__{0}".format(USERNAME_FIELD) - ] - - try: - # get_field_by_name throws FieldDoesNotExist if the field is not - # present on the model - # pylint: disable=W0212,E1103 - User._meta.get_field_by_name("email") - fields += ["user__email"] - except FieldDoesNotExist: - pass - else: - # Using a pre-Django 1.5 User model - fields = [ - "user__username", - "user__email" - ] + fields = [ + "user__{0}".format(User.USERNAME_FIELD) + ] + if "email" in [f.name for f in User._meta.fields]: + fields += ["user__email"] return fields diff --git a/payments/api/serializers.py b/payments/api/serializers.py index ebf222b..86a1af6 100644 --- a/payments/api/serializers.py +++ b/payments/api/serializers.py @@ -26,6 +26,7 @@ class EventProcessingExceptionSerializer(ModelSerializer): class Meta: model = EventProcessingException + fields = '__all__' class EventSerializer(ModelSerializer): @@ -33,21 +34,25 @@ class EventSerializer(ModelSerializer): class Meta: model = Event + fields = '__all__' class CurrentSubscriptionSerializer(ModelSerializer): class Meta: model = CurrentSubscription + fields = '__all__' class ChargeSerializer(ModelSerializer): class Meta: model = Charge + fields = '__all__' class InvoiceItemSerializer(ModelSerializer): class Meta: model = InvoiceItem + fields = '__all__' class InvoiceSerializer(ModelSerializer): @@ -56,6 +61,7 @@ class InvoiceSerializer(ModelSerializer): class Meta: model = Invoice + fields = '__all__' class CurrentCustomerSerializer(ModelSerializer): @@ -64,6 +70,7 @@ class CurrentCustomerSerializer(ModelSerializer): class Meta: model = Customer + fields = '__all__' """ diff --git a/payments/api/urls.py b/payments/api/urls.py index e97e977..6f87d10 100644 --- a/payments/api/urls.py +++ b/payments/api/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url -import views +from payments.api import views -urlpatterns = patterns('', +urlpatterns = [ url(r'^current-user/$', views.CurrentCustomerDetailView.as_view(), name='stripe-current-customer-detail'), url(r'^subscription/$', views.SubscriptionView.as_view(), name='stripe-subscription'), url(r'^change-card/$', views.ChangeCardView.as_view(), name='stripe-change-card'), @@ -12,4 +12,4 @@ url(r'^events/$', views.EventListView.as_view(), name='stripe-events'), url(r'^webhook/$', views.WebhookView.as_view(), name='stripe-webhook'), url(r'^cancel/$', views.CancelView.as_view(), name='stripe-cancel'), -) \ No newline at end of file +] diff --git a/payments/migrations/0001_initial.py b/payments/migrations/0001_initial.py new file mode 100644 index 0000000..7e582e5 --- /dev/null +++ b/payments/migrations/0001_initial.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-20 16:32 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import jsonfield.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Charge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=255, unique=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('card_last_4', models.CharField(blank=True, max_length=4)), + ('card_kind', models.CharField(blank=True, max_length=50)), + ('currency', models.CharField(default='usd', max_length=10)), + ('amount', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('amount_refunded', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('description', models.TextField(blank=True)), + ('paid', models.NullBooleanField()), + ('disputed', models.NullBooleanField()), + ('refunded', models.NullBooleanField()), + ('captured', models.NullBooleanField()), + ('fee', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('receipt_sent', models.BooleanField(default=False)), + ('charge_created', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CurrentSubscription', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('plan', models.CharField(max_length=100)), + ('quantity', models.IntegerField()), + ('start', models.DateTimeField()), + ('status', models.CharField(max_length=25)), + ('cancel_at_period_end', models.BooleanField(default=False)), + ('canceled_at', models.DateTimeField(blank=True, null=True)), + ('current_period_end', models.DateTimeField(blank=True, null=True)), + ('current_period_start', models.DateTimeField(blank=True, null=True)), + ('ended_at', models.DateTimeField(blank=True, null=True)), + ('trial_end', models.DateTimeField(blank=True, null=True)), + ('trial_start', models.DateTimeField(blank=True, null=True)), + ('amount', models.DecimalField(decimal_places=2, max_digits=9)), + ('currency', models.CharField(default='usd', max_length=10)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ], + ), + migrations.CreateModel( + name='Customer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=255, unique=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('card_fingerprint', models.CharField(blank=True, max_length=200)), + ('card_last_4', models.CharField(blank=True, max_length=4)), + ('card_kind', models.CharField(blank=True, max_length=50)), + ('date_purged', models.DateTimeField(editable=False, null=True)), + ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=255, unique=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('kind', models.CharField(max_length=250)), + ('livemode', models.BooleanField(default=False)), + ('webhook_message', jsonfield.fields.JSONField()), + ('validated_message', jsonfield.fields.JSONField(null=True)), + ('valid', models.NullBooleanField()), + ('processed', models.BooleanField(default=False)), + ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='payments.Customer')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EventProcessingException', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', models.TextField()), + ('message', models.CharField(max_length=500)), + ('traceback', models.TextField()), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='payments.Event')), + ], + ), + migrations.CreateModel( + name='Invoice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=255)), + ('attempted', models.NullBooleanField()), + ('attempts', models.PositiveIntegerField(null=True)), + ('closed', models.BooleanField(default=False)), + ('paid', models.BooleanField(default=False)), + ('period_end', models.DateTimeField()), + ('period_start', models.DateTimeField()), + ('subtotal', models.DecimalField(decimal_places=2, max_digits=9)), + ('total', models.DecimalField(decimal_places=2, max_digits=9)), + ('currency', models.CharField(default='usd', max_length=10)), + ('date', models.DateTimeField()), + ('charge', models.CharField(blank=True, max_length=50)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='payments.Customer')), + ], + options={ + 'ordering': ['-date'], + }, + ), + migrations.CreateModel( + name='InvoiceItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('amount', models.DecimalField(decimal_places=2, max_digits=9)), + ('currency', models.CharField(default='usd', max_length=10)), + ('period_start', models.DateTimeField()), + ('period_end', models.DateTimeField()), + ('proration', models.BooleanField(default=False)), + ('line_type', models.CharField(max_length=50)), + ('description', models.CharField(blank=True, max_length=200)), + ('plan', models.CharField(blank=True, max_length=100)), + ('quantity', models.IntegerField(null=True)), + ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='payments.Invoice')), + ], + ), + migrations.CreateModel( + name='Transfer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('stripe_id', models.CharField(max_length=255, unique=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('amount', models.DecimalField(decimal_places=2, max_digits=9)), + ('currency', models.CharField(default='usd', max_length=25)), + ('status', models.CharField(max_length=25)), + ('date', models.DateTimeField()), + ('description', models.TextField(blank=True, null=True)), + ('adjustment_count', models.IntegerField(null=True)), + ('adjustment_fees', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('adjustment_gross', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('charge_count', models.IntegerField(null=True)), + ('charge_fees', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('charge_gross', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('collected_fee_count', models.IntegerField(null=True)), + ('collected_fee_gross', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('net', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('refund_count', models.IntegerField(null=True)), + ('refund_fees', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('refund_gross', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('validation_count', models.IntegerField(null=True)), + ('validation_fees', models.DecimalField(decimal_places=2, max_digits=9, null=True)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transfers', to='payments.Event')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TransferChargeFee', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=9)), + ('currency', models.CharField(default='usd', max_length=10)), + ('application', models.TextField(blank=True, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('kind', models.CharField(max_length=150)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('transfer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='charge_fee_details', to='payments.Transfer')), + ], + ), + migrations.AddField( + model_name='currentsubscription', + name='customer', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='current_subscription', to='payments.Customer'), + ), + migrations.AddField( + model_name='charge', + name='customer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='charges', to='payments.Customer'), + ), + migrations.AddField( + model_name='charge', + name='invoice', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='charges', to='payments.Invoice'), + ), + ] diff --git a/payments/migrations/__init__.py b/payments/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payments/models.py b/payments/models.py index cba6578..e84e035 100644 --- a/payments/models.py +++ b/payments/models.py @@ -11,8 +11,6 @@ from django.utils.encoding import smart_str from django.template.loader import render_to_string -from django.contrib.sites.models import Site - import stripe from jsonfield.fields import JSONField @@ -370,7 +368,7 @@ def cancel(self, at_period_end=True): cancelled.send(sender=self, stripe_response=sub) @classmethod - def create(cls, user, card=None, plan=None, charge_immediately=True): + def create(cls, user, card=None, plan=None, charge_immediately=True, metadata={}): if card and plan: plan = PAYMENTS_PLANS[plan]["stripe_plan_id"] @@ -390,7 +388,8 @@ def create(cls, user, card=None, plan=None, charge_immediately=True): email=user.email, card=card, plan=plan or DEFAULT_PLAN, - trial_end=trial_end + trial_end=trial_end, + metadata=metadata ) if stripe_customer.active_card: @@ -882,11 +881,9 @@ def sync_from_stripe_data(cls, data): def send_receipt(self): if not self.receipt_sent: - site = Site.objects.get_current() protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") ctx = { "charge": self, - "site": site, "protocol": protocol, } subject = render_to_string("payments/email/subject.txt", ctx) diff --git a/payments/settings.py b/payments/settings.py index 6f1942a..06a23e8 100644 --- a/payments/settings.py +++ b/payments/settings.py @@ -49,9 +49,5 @@ def plan_from_stripe_id(stripe_id): def get_api_key(): - if settings.DEBUG: - api_key = settings.STRIPE_PUBLIC_KEY - else: - api_key = settings.STRIPE_SECRET_KEY - + api_key = settings.STRIPE_SECRET_KEY return api_key \ No newline at end of file diff --git a/payments/utils.py b/payments/utils.py index de335e8..20a9ad8 100644 --- a/payments/utils.py +++ b/payments/utils.py @@ -1,8 +1,10 @@ import datetime import decimal +from importlib import import_module + from django.core.exceptions import ImproperlyConfigured -from django.utils import importlib, timezone +from django.utils import timezone def convert_tstamp(response, field_name=None): @@ -36,7 +38,7 @@ def load_path_attr(path): # pragma: no cover i = path.rfind(".") module, attr = path[:i], path[i + 1:] try: - mod = importlib.import_module(module) + mod = import_module(module) except ImportError as e: raise ImproperlyConfigured("Error importing {0}: '{1}'".format(module, e)) try: diff --git a/setup.py b/setup.py index 92f2335..e896757 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ def read(*parts): "pytz", "six", "djangorestframework>=3.1.1", + "jsonfield>=1.0.3", ], test_suite="runtests.runtests", tests_require=[ From b5bdb6f4cb1684e19f0393f15f8062899dcdc769 Mon Sep 17 00:00:00 2001 From: Benjamin Ryon Date: Fri, 30 Dec 2016 01:42:39 -0800 Subject: [PATCH 2/3] Add view to accept card token instead of full card details. (#2) * Add view to accept card token instead of full card details. * Use generic error handling for non-present tokens case. --- README.rst | 3 ++- payments/api/serializers.py | 4 ++++ payments/api/urls.py | 1 + payments/api/views.py | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 286f2ac..dfbc567 100644 --- a/README.rst +++ b/README.rst @@ -18,6 +18,7 @@ Endpoints * current-user/ (GET) * subscription/ (GET/POST) * change-card/ (GET/POST) +* change-card-token/ (POST) * charges/ (GET) * invoices/ (GET) * plans/ (GET) @@ -25,6 +26,6 @@ Endpoints * webhook/ (POST) * cancel/ (POST) -**ALL TEMPLATES AND AJAX VIEWS HAS BEEN REMOVED, USE ADDED ENDPOINTS** +**ALL TEMPLATES AND AJAX VIEWS HAVE BEEN REMOVED, USE ADDED ENDPOINTS** Documentation can be found at http://django-stripe-payments.readthedocs.org diff --git a/payments/api/serializers.py b/payments/api/serializers.py index 86a1af6..32327e0 100644 --- a/payments/api/serializers.py +++ b/payments/api/serializers.py @@ -97,6 +97,10 @@ class CardSerializer(Serializer): address_country = serializers.CharField(required=False, allow_null=True) +class CardTokenSerializer(Serializer): + token = serializers.CharField(required=True, help_text=u'Card token generated by stripe.js, or other api call.') + + class CancelSerializer(Serializer): confirm = serializers.BooleanField(required=True) diff --git a/payments/api/urls.py b/payments/api/urls.py index 6f87d10..f10e350 100644 --- a/payments/api/urls.py +++ b/payments/api/urls.py @@ -6,6 +6,7 @@ url(r'^current-user/$', views.CurrentCustomerDetailView.as_view(), name='stripe-current-customer-detail'), url(r'^subscription/$', views.SubscriptionView.as_view(), name='stripe-subscription'), url(r'^change-card/$', views.ChangeCardView.as_view(), name='stripe-change-card'), + url(r'^change-card-token/$', views.ChangeCardTokenView.as_view(), name='stripe-change-card-token'), url(r'^charges/$', views.ChargeListView.as_view(), name='stripe-charges'), url(r'^invoices/$', views.InvoiceListView.as_view(), name='stripe-invoices'), url(r'^plans/$', views.PlanListView.as_view(), name='stripe-plans'), diff --git a/payments/api/views.py b/payments/api/views.py index 8a94c16..03ded9e 100644 --- a/payments/api/views.py +++ b/payments/api/views.py @@ -15,6 +15,7 @@ CurrentCustomerSerializer, SubscriptionSerializer, CardSerializer, + CardTokenSerializer, CancelSerializer, ChargeSerializer, InvoiceSerializer, @@ -120,6 +121,42 @@ def post(self, request, *args, **kwargs): return Response(error_data, status=status.HTTP_400_BAD_REQUEST) +class ChangeCardTokenView(StripeView): + """ + Add or update customer card token + + This is useful if you are planing to use strip.js to + retrieve the card token. This isolates the full credit + card number from your server. + """ + serializer_class = CardTokenSerializer + + def post(self, request, *args, **kwargs): + try: + serializer = self.serializer_class(data=request.data) + + if serializer.is_valid(): + validated_data = serializer.validated_data + + customer = self.get_customer() + + token = validated_data['token'] + customer.update_card(token) + send_invoice = customer.card_fingerprint == "" + + if send_invoice: + customer.send_invoice() + customer.retry_unpaid_invoices() + + return Response(validated_data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + except stripe.StripeError as e: + error_data = {u'error': smart_str(e) or u'Unknown error'} + return Response(error_data, status=status.HTTP_400_BAD_REQUEST) + + class CancelView(StripeView): """ Cancel customer subscription """ serializer_class = CancelSerializer From a94bd781fbac83a416d80fbe97ce6bd1a55362b8 Mon Sep 17 00:00:00 2001 From: Vikalp Jain Date: Thu, 26 Jan 2017 14:55:34 +0530 Subject: [PATCH 3/3] chore(payments): Make support for sending trial days to 0 --- payments/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/payments/models.py b/payments/models.py index e84e035..3e2048e 100644 --- a/payments/models.py +++ b/payments/models.py @@ -553,7 +553,11 @@ def subscribe(self, plan, quantity=None, trial_days=None, cu = self.stripe_customer subscription_params = {} - if trial_days: + # https://stripe.com/docs/api?lang=python#create_subscription-trial_end Special keyword Now is for ending + # the trial now + if trial_days is 'now': + subscription_params["trial_end"] = 'now' + elif trial_days: subscription_params["trial_end"] = \ datetime.datetime.utcnow() + datetime.timedelta(days=trial_days) if token: