From 281f138687a760964510f30388a53a58e93a8ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Barbier?= Date: Mon, 4 Dec 2023 09:37:22 +0700 Subject: [PATCH] Refactor Subscription model --- seven23/api/saas/views.py | 67 +++++++++---------- seven23/api/users/tests_users.py | 2 +- seven23/api/views.py | 6 +- .../0016_profile_stripe_customer_id.py | 18 +++++ seven23/models/profile/models.py | 1 + seven23/models/profile/tests.py | 2 +- seven23/models/saas/admin.py | 4 +- ...tripesubscription_delete_stripecustomer.py | 33 +++++++++ .../0018_alter_stripesubscription_user.py | 21 ++++++ seven23/models/saas/models.py | 35 ++++++++-- seven23/models/saas/serializers.py | 9 +-- 11 files changed, 147 insertions(+), 51 deletions(-) create mode 100644 seven23/models/profile/migrations/0016_profile_stripe_customer_id.py create mode 100644 seven23/models/saas/migrations/0017_stripesubscription_delete_stripecustomer.py create mode 100644 seven23/models/saas/migrations/0018_alter_stripesubscription_user.py diff --git a/seven23/api/saas/views.py b/seven23/api/saas/views.py index fde93bb..9603d36 100644 --- a/seven23/api/saas/views.py +++ b/seven23/api/saas/views.py @@ -23,7 +23,7 @@ from seven23 import settings from seven23.models.terms.models import TermsAndConditions -from seven23.models.saas.models import Price, StripeCustomer +from seven23.models.saas.models import Price, StripeSubscription stripe.api_key = settings.STRIPE_SECRET_KEY @@ -36,7 +36,7 @@ def StripeGenerateSession(request): return Response(status=status.HTTP_400_BAD_REQUEST) session = stripe.billing_portal.Session.create( - customer=request.user.stripe.stripe_customer_id, + customer=request.user.profile.stripe_customer_id, return_url=request.GET.get("return_url"), ) @@ -60,7 +60,7 @@ def StripeGenerateSession(request): if hasattr(request.user, 'stripe'): # if user model has stripe foreign object - stripe_customer_id = request.user.stripe.stripe_customer_id + stripe_customer_id = request.user.profile.stripe_customer_id trial_end_date = None @@ -68,7 +68,6 @@ def StripeGenerateSession(request): trial_end_date = request.user.profile.valid_until # Make sure trial_end_date is at least two days in the future # (Stripe requirement) - print(trial_end_date, timezone.now() + timezone.timedelta(days=2, hours=1)) if trial_end_date < timezone.now() + timezone.timedelta(days=2, hours=1): trial_end_date = timezone.now() + timezone.timedelta(days=2, hours=1) @@ -109,62 +108,60 @@ def StripeWebhook(request): return HttpResponse(status=400) # Handle the event - if event.type == 'customer.subscription.updated': + if event.type == 'customer.subscription.created' or event.type == 'customer.subscription.updated': + subscription = event.data.object - stripeCustomer = get_object_or_404(StripeCustomer, stripe_subscription_id=subscription.id) + stripeCustomer, created = StripeSubscription.objects.get_or_create( + subscription_id=subscription.id + ) - if subscription.cancel_at: - stripeCustomer.is_active = False - stripeCustomer.user.profile.valid_until = timezone.make_aware(datetime.utcfromtimestamp(subscription.cancel_at), timezone=timezone.utc) - stripeCustomer.user.profile.save() + if subscription.trial_end: + stripeCustomer.trial_end = timezone.make_aware(datetime.utcfromtimestamp(subscription.trial_end), timezone=timezone.utc) else: - stripeCustomer.is_active = True - stripeCustomer.user.profile.valid_until = timezone.make_aware(datetime.utcfromtimestamp(subscription.current_period_end), timezone=timezone.utc) - stripeCustomer.user.profile.save() + stripeCustomer.trial_end = None - if subscription.trial_end and timezone.make_aware(datetime.utcfromtimestamp(subscription.trial_end), timezone=timezone.utc) > stripeCustomer.user.profile.valid_until: - stripeCustomer.user.profile.valid_until = timezone.make_aware(datetime.utcfromtimestamp(subscription.trial_end), timezone=timezone.utc) - stripeCustomer.user.profile.save() + if subscription.current_period_end: + stripeCustomer.current_period_end = timezone.make_aware(datetime.utcfromtimestamp(subscription.current_period_end), timezone=timezone.utc) + else: + stripeCustomer.current_period_end = None + + if subscription.cancel_at: + stripeCustomer.cancel_at = timezone.make_aware(datetime.utcfromtimestamp(subscription.cancel_at), timezone=timezone.utc) + else: + stripeCustomer.cancel_at = None + stripeCustomer.status = subscription.status stripeCustomer.price = get_object_or_404(Price, stripe_price_id=subscription.plan.id) + stripeCustomer.is_active = True stripeCustomer.save() return Response(status=status.HTTP_200_OK) elif event.type == 'customer.subscription.deleted': subscription = event.data.object try: - StripeCustomer.objects.filter(stripe_subscription_id=subscription.id).delete() + StripeSubscription.objects.filter(subscription_id=subscription.id).delete() except: pass return Response(status=status.HTTP_200_OK) elif event.type == 'checkout.session.completed': - # We create a StripeCustomer object to store user's data from Strip + # We create a StripeCustomer object to store user's data from Stripe checkout = event.data.object + user = get_object_or_404(User, pk=checkout.client_reference_id) + # checkout.client_reference_id is 1, 2, 4 ... user.pk # checkout.subscription is sub_1OHNiZILP1DzcVdZmb3bqdLr # checkout.customer is cus_P5Yqm6ZYR1CHFc - user = get_object_or_404(User, pk=checkout.client_reference_id) - subscription = stripe.Subscription.retrieve(checkout.subscription) - price = get_object_or_404(Price, stripe_price_id=subscription.plan.id) + subscription, created = StripeSubscription.objects.get_or_create( + subscription_id=checkout.subscription, + ) + subscription.user = user + subscription.save() - user.profile.valid_until = timezone.make_aware(datetime.utcfromtimestamp(subscription.current_period_end), timezone=timezone.utc) - if subscription.trial_end and timezone.make_aware(datetime.utcfromtimestamp(subscription.trial_end), timezone=timezone.utc) > user.profile.valid_until: - user.profile.valid_until = timezone.make_aware(datetime.utcfromtimestamp(subscription.trial_end), timezone=timezone.utc) + user.profile.stripe_customer_id = checkout.customer user.profile.save() - StripeCustomer.objects.filter(user=user).delete() - - sub = StripeCustomer.objects.create( - user=user, - stripe_customer_id=checkout.customer, - stripe_subscription_id=checkout.subscription, - price=price, - is_active=True, - ) - sub.save() - return Response(status=status.HTTP_200_OK) else: print(event) diff --git a/seven23/api/users/tests_users.py b/seven23/api/users/tests_users.py index 0b365ed..3b12e17 100644 --- a/seven23/api/users/tests_users.py +++ b/seven23/api/users/tests_users.py @@ -50,5 +50,5 @@ def test_registration_new_user(self): self.assertTrue('auto_sync' in data['profile']) self.assertTrue('key_verified' in data['profile']) self.assertTrue('social_networks' in data['profile']) - self.assertFalse(data['profile']['auto_sync']) + self.assertTrue(data['profile']['auto_sync']) self.assertFalse(data['profile']['key_verified']) \ No newline at end of file diff --git a/seven23/api/views.py b/seven23/api/views.py index 5b828cb..5e5b71a 100644 --- a/seven23/api/views.py +++ b/seven23/api/views.py @@ -13,8 +13,8 @@ from seven23 import settings from seven23.models.terms.models import TermsAndConditions -from seven23.models.saas.serializers import PriceSerializer, StripeCustomerSerializer -from seven23.models.saas.models import Price, StripeCustomer +from seven23.models.saas.serializers import PriceSerializer, StripeSubscriptionSerializer +from seven23.models.saas.models import Price from allauth.account.models import EmailAddress @@ -34,7 +34,7 @@ def api_init(request): result['subscription'] = False if hasattr(request.user, 'stripe'): - result['subscription'] = StripeCustomerSerializer(request.user.stripe).data + result['subscription'] = StripeSubscriptionSerializer(request.user.stripe).data result['subscription_price'] = PriceSerializer(request.user.stripe.price).data if result['saas']: diff --git a/seven23/models/profile/migrations/0016_profile_stripe_customer_id.py b/seven23/models/profile/migrations/0016_profile_stripe_customer_id.py new file mode 100644 index 0000000..47b568e --- /dev/null +++ b/seven23/models/profile/migrations/0016_profile_stripe_customer_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2023-12-03 10:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profile', '0015_remove_profile_stripe_customer_id'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='stripe_customer_id', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/seven23/models/profile/models.py b/seven23/models/profile/models.py index 9072a04..7482986 100644 --- a/seven23/models/profile/models.py +++ b/seven23/models/profile/models.py @@ -37,6 +37,7 @@ class Profile(models.Model): help_text=_(u'Last call on the API as a registered user'), auto_now_add=True, editable=False) + stripe_customer_id = models.CharField(max_length=255, blank=True) valid_until = models.DateTimeField(_(u'Valid until'), help_text=_(u'On SASS, this is the validation date'), default=timezone.now) diff --git a/seven23/models/profile/tests.py b/seven23/models/profile/tests.py index f1ccdd4..4e13704 100644 --- a/seven23/models/profile/tests.py +++ b/seven23/models/profile/tests.py @@ -27,7 +27,7 @@ def test_profile_creation(self): self.assertEqual(self.user.profile.valid_until > timezone.now(), True) self.assertEqual(self.user.profile.valid_until < expected_date, True) - self.assertEqual(self.user.profile.auto_sync, False) + self.assertEqual(self.user.profile.auto_sync, True) self.assertEqual(self.user.profile.key_verified, False) self.user.profile.key_verified = True diff --git a/seven23/models/saas/admin.py b/seven23/models/saas/admin.py index 492c9f8..52dd2a5 100644 --- a/seven23/models/saas/admin.py +++ b/seven23/models/saas/admin.py @@ -2,7 +2,7 @@ Terms and Conditions administration """ from django.contrib import admin -from seven23.models.saas.models import StripeCustomer, Price +from seven23.models.saas.models import StripeSubscription, Price -admin.site.register(StripeCustomer) +admin.site.register(StripeSubscription) admin.site.register(Price) \ No newline at end of file diff --git a/seven23/models/saas/migrations/0017_stripesubscription_delete_stripecustomer.py b/seven23/models/saas/migrations/0017_stripesubscription_delete_stripecustomer.py new file mode 100644 index 0000000..f031e77 --- /dev/null +++ b/seven23/models/saas/migrations/0017_stripesubscription_delete_stripecustomer.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.7 on 2023-12-03 10:25 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('saas', '0016_alter_stripecustomer_is_active'), + ] + + operations = [ + migrations.CreateModel( + name='StripeSubscription', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subscription_id', models.CharField(max_length=255)), + ('trial_end', models.DateTimeField(null=True)), + ('current_period_end', models.DateTimeField(null=True)), + ('cancel_at', models.DateTimeField(null=True)), + ('status', models.CharField(max_length=255)), + ('is_active', models.BooleanField(default=True)), + ('price', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customers', to='saas.price')), + ('user', models.OneToOneField(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='stripe', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.DeleteModel( + name='StripeCustomer', + ), + ] diff --git a/seven23/models/saas/migrations/0018_alter_stripesubscription_user.py b/seven23/models/saas/migrations/0018_alter_stripesubscription_user.py new file mode 100644 index 0000000..404fe4b --- /dev/null +++ b/seven23/models/saas/migrations/0018_alter_stripesubscription_user.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.7 on 2023-12-03 11:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('saas', '0017_stripesubscription_delete_stripecustomer'), + ] + + operations = [ + migrations.AlterField( + model_name='stripesubscription', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stripe', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/seven23/models/saas/models.py b/seven23/models/saas/models.py index fc9e7b6..120d113 100644 --- a/seven23/models/saas/models.py +++ b/seven23/models/saas/models.py @@ -25,12 +25,37 @@ class Price(models.Model): def __str__(self): return u'%s %s %s / %s months' % (self.stripe_price_id, self.price, self.currency, self.duration) -class StripeCustomer(models.Model): - user = models.OneToOneField(to=User, related_name="stripe", on_delete=models.CASCADE) - stripe_customer_id = models.CharField(max_length=255) - stripe_subscription_id = models.CharField(max_length=255) +class StripeSubscription(models.Model): + subscription_id = models.CharField(max_length=255) + user = models.OneToOneField(to=User, related_name="stripe", null=True, on_delete=models.CASCADE) price = models.ForeignKey(Price, related_name="customers", null=True, on_delete=models.CASCADE) + # Dates + trial_end = models.DateTimeField(null=True, blank=True) + current_period_end = models.DateTimeField(null=True, blank=True) + cancel_at = models.DateTimeField(null=True, blank=True) + # Status + status = models.CharField(max_length=255) is_active = models.BooleanField(default=True) def __str__(self): - return self.user.username \ No newline at end of file + return u'%s %s' % (self.user, self.subscription_id) + + def is_trial(self): + return self.trial_end == self.current_period_end + + def is_canceled(self): + return self.cancel_at is not None + + def save(self, *args, **kwargs): + if self.user: + valid_until = self.user.profile.valid_until + if self.cancel_at: + self.is_active = False + self.user.profile.valid_until = self.cancel_at + elif self.trial_end and self.trial_end > self.current_period_end: + self.user.profile.valid_until = self.trial_end + elif self.current_period_end: + self.user.profile.valid_until = self.current_period_end + self.user.profile.save() + + super(StripeSubscription, self).save(*args, **kwargs) # Call the "real" save() method \ No newline at end of file diff --git a/seven23/models/saas/serializers.py b/seven23/models/saas/serializers.py index 1cdff92..099cab9 100644 --- a/seven23/models/saas/serializers.py +++ b/seven23/models/saas/serializers.py @@ -2,15 +2,16 @@ Serializer for Currency module """ from rest_framework import serializers -from seven23.models.saas.models import Price, StripeCustomer +from seven23.models.saas.models import Price, StripeSubscription -class StripeCustomerSerializer(serializers.HyperlinkedModelSerializer): +class StripeSubscriptionSerializer(serializers.HyperlinkedModelSerializer): """ Serialize Currency model """ + class Meta: - model = StripeCustomer - fields = ('pk', 'stripe_customer_id', 'stripe_subscription_id', 'is_active') + model = StripeSubscription + fields = ('pk', 'subscription_id', 'current_period_end', 'is_active', 'is_trial', 'is_canceled') class PriceSerializer(serializers.HyperlinkedModelSerializer): """