Skip to content

Commit

Permalink
[BE] RO-1085 - add coupon model (#11)
Browse files Browse the repository at this point in the history
* [BE] RO-1085 - add coupon model

* Lint

* Run isort

* Return save

* CR

* Correct typo

* Migrate Subscription.coupon

* Isort

* RO-1087 - [BE] remove coupon from Stripe after is_deleted is set to True (#12)

* RO-1087 - [BE] remove coupon from Stripe after is_deleted is set to True

* Pepify

* CR

* CR

* RO-1089 - [BE] set is_deleted flag to True after coupon was deleted @stripe (#13)

* RO-1089 - [BE] set is_deleted flag to True after coupon was deleted @stripe

* Add __init__.py to tests/

* Correct

* CR

* CR

* CR

* RO-1088 - [BE] create and update coupons in admin after change @stripe (#14)

* RO-1088 - [BE] create and update coupons in admin after change @stripe

* CR
  • Loading branch information
poxip committed Aug 23, 2017
1 parent 669eb28 commit f9efd0c
Show file tree
Hide file tree
Showing 9 changed files with 756 additions and 24 deletions.
25 changes: 24 additions & 1 deletion aa_stripe/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
from django.contrib import admin

from aa_stripe.models import StripeCharge, StripeCustomer, StripeSubscription, StripeSubscriptionPlan, StripeWebhook
from aa_stripe.forms import StripeCouponForm
from aa_stripe.models import (StripeCharge, StripeCoupon, StripeCustomer, StripeSubscription, StripeSubscriptionPlan,
StripeWebhook)


class ReadOnlyBase(object):
Expand Down Expand Up @@ -46,6 +48,26 @@ class StripeChargeAdmin(ReadOnly):
ordering = ("-created",)


class StripeCouponAdmin(admin.ModelAdmin):
form = StripeCouponForm
list_display = ("id", "coupon_id", "amount_off", "percent_off", "currency", "created", "is_deleted",
"is_created_at_stripe")
list_filter = ("coupon_id", "amount_off", "percent_off", "currency", "created", "is_deleted",
"is_created_at_stripe")
readonly_fields = ("stripe_response", "created", "updated", "is_deleted")
ordering = ("-created",)

def get_readonly_fields(self, request, obj=None):
if obj:
return [field for field in self.form.Meta.fields if field not in ["metadata"]]

return self.readonly_fields

def has_delete_permission(self, request, obj=None):
# allow deleting single object, but disable bulk delete (bulk delete does not call models' .delete() method)
return bool(obj)


class StripeSubscriptionAdmin(ReadOnly):
list_display = (
"id", "stripe_subscription_id", "user", "is_created_at_stripe", "status", "created", "updated", "end_date",
Expand All @@ -65,6 +87,7 @@ class StripeWebhookAdmin(ReadOnly):

admin.site.register(StripeCustomer, StripeCustomerAdmin)
admin.site.register(StripeCharge, StripeChargeAdmin)
admin.site.register(StripeCoupon, StripeCouponAdmin)
admin.site.register(StripeSubscription, StripeSubscriptionAdmin)
admin.site.register(StripeSubscriptionPlan, StripeSubscriptionPlanAdmin)
admin.site.register(StripeWebhook, StripeWebhookAdmin)
4 changes: 4 additions & 0 deletions aa_stripe/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@

class StripeMethodNotAllowed(Exception):
details = _("Already created at stripe")


class StripeWebhookAlreadyParsed(Exception):
details = _("This webhook has already been parsed")
42 changes: 42 additions & 0 deletions aa_stripe/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django import forms
from django.utils.translation import ugettext_lazy as _

from aa_stripe.models import StripeCoupon


class StripeCouponForm(forms.ModelForm):
def clean_coupon_id(self):
coupon_id = self.cleaned_data.get("coupon_id")
if coupon_id:
if StripeCoupon.objects.filter(coupon_id=coupon_id, is_deleted=False).exists():
raise forms.ValidationError(_("Coupon with this id already exists"))

return coupon_id

def clean_duration_in_months(self):
duration = self.cleaned_data.get("duration")
duration_in_months = self.cleaned_data.get("duration_in_months")
if duration == StripeCoupon.DURATION_REPEATING and not duration_in_months:
raise forms.ValidationError(_("Cannot be empty with when duration is set to repeating"))

if duration_in_months and duration != StripeCoupon.DURATION_REPEATING:
raise forms.ValidationError(_("Cannot be set when duration is not set to repeating"))

return duration_in_months

def clean(self):
if self.instance.pk:
return self.cleaned_data

discount_list = [self.cleaned_data.get("amount_off"), self.cleaned_data.get("percent_off")]
if not any(discount_list) or all(discount_list):
raise forms.ValidationError(_("Coupon must specify amount_off or percent_off"))

return self.cleaned_data

class Meta:
model = StripeCoupon
fields = [
"coupon_id", "amount_off", "currency", "duration", "duration_in_months", "livemode", "max_redemptions",
"metadata", "percent_off", "redeem_by", "times_redeemed", "valid", "is_deleted"
]
95 changes: 95 additions & 0 deletions aa_stripe/migrations/0010_auto_20170822_1004.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-22 10:04
from __future__ import unicode_literals

from datetime import datetime

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


def migrate_subcription(apps, schema_editor):
StripeSubscription = apps.get_model("aa_stripe", "StripeSubscription")
StripeCoupon = apps.get_model("aa_stripe", "StripeCoupon")
stripe.api_key = settings.STRIPE_API_KEY

for subscription in StripeSubscription.objects.all():
if StripeCoupon.objects.filter(coupon_id=subscription.coupon_code, is_deleted=False).exists():
# do not allow duplicates
continue

try:
coupon = stripe.Coupon.retrieve(id=subscription.coupon_code)
except stripe.error.InvalidRequestError:
print("Coupon {} does not exist, cannot migrate".format(subscription.coupon_code))
continue

subscription.coupon = StripeCoupon.objects.create(
coupon_id=coupon.get("id"), stripe_response=coupon, amount_off=coupon.get("amount_off"),
currency=coupon.get("currency"), duration=coupon.get("duration"),
duration_in_months=coupon.get("duration_in_month"), livemode=coupon.get("livemode"),
max_redemptions=coupon.get("max_redemptions"), metadata=coupon.get("metadata"),
percent_off=coupon.get("percent_off"), redeem_by=coupon.get("redeem_by"),
times_redeemed=coupon.get("times_redeemed"), valid=coupon.get("valid"),
created=timezone.make_aware(datetime.fromtimestamp(coupon.get("created"))),
is_created_at_stripe=True
)
subscription.save()


class Migration(migrations.Migration):

dependencies = [
('aa_stripe', '0009_auto_20170725_1205'),
]

operations = [
migrations.CreateModel(
name='StripeCoupon',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated', models.DateTimeField(auto_now=True)),
('stripe_response', jsonfield.fields.JSONField(default=dict)),
('coupon_id', models.CharField(help_text='Identifier for the coupon', max_length=255)),
('amount_off', models.PositiveIntegerField(blank=True, help_text='Amount (in the currency specified) that will be taken off the subtotal ofany invoices for this customer.', null=True)),
('currency', models.CharField(default='usd', help_text='If amount_off has been set, the three-letter ISO code for thecurrency of the amount to take off.', max_length=3)),
('duration', models.CharField(choices=[('forever', 'forever'), ('once', 'once'), ('repeating', 'repeating')], help_text='Describes how long a customer who applies this coupon will get the discount.', max_length=255)),
('duration_in_months', models.PositiveIntegerField(blank=True, help_text='If duration is repeating, the number of months the coupon applies.Null if coupon duration is forever or once.', null=True)),
('livemode', models.BooleanField(default=False, help_text='Flag indicating whether the object exists in live mode or test mode.')),
('max_redemptions', models.PositiveIntegerField(blank=True, help_text='Maximum number of times this coupon can be redeemed, in total, before it is no longer valid.', null=True)),
('metadata', jsonfield.fields.JSONField(default=dict, help_text='Set of key/value pairs that you can attach to an object. It can be useful forstoring additional information about the object in a structured format.')),
('percent_off', models.PositiveIntegerField(blank=True, help_text='Percent that will be taken off the subtotal of any invoicesfor this customer for the duration ofthe coupon. For example, a coupon with percent_off of 50 will make a $100 invoice $50 instead.', null=True)),
('redeem_by', models.DateTimeField(blank=True, help_text='Date after which the coupon can no longer be redeemed.', null=True)),
('times_redeemed', models.PositiveIntegerField(default=0, help_text='Number of times this coupon has been applied to a customer.')),
('valid', models.BooleanField(default=False, help_text='Taking account of the above properties, whether this coupon can still be applied to a customer.')),
('created', models.DateTimeField()),
('is_deleted', models.BooleanField(default=False)),
('is_created_at_stripe', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
),
migrations.RenameField(
model_name='stripesubscription',
old_name='coupon',
new_name='coupon_code',
),
migrations.AddField(
model_name='stripesubscription',
name='coupon',
field=models.ForeignKey(blank=True, help_text='https://stripe.com/docs/api/python#create_subscription-coupon', null=True, on_delete=django.db.models.deletion.SET_NULL, to='aa_stripe.StripeCoupon'),
),
migrations.RunPython(
code=migrate_subcription,
hints={'target_db': 'default'}
),
migrations.RemoveField(
model_name='stripesubscription',
name='coupon_code'
)
]

0 comments on commit f9efd0c

Please sign in to comment.