Merge branch 'master' into task/3994/dg_add_signup_clarification_line
This commit is contained in:
commit
a743e4e8a0
8 changed files with 185 additions and 39 deletions
|
@ -8,6 +8,7 @@ Next:
|
||||||
* #3601: [dcl, hosting] Change minimum required RAM from 2GB to 1GB
|
* #3601: [dcl, hosting] Change minimum required RAM from 2GB to 1GB
|
||||||
* #3973: [dcl] Update datacenterlight and glasfaser contact address to Linthal and company name to "ungleich glarus ag"
|
* #3973: [dcl] Update datacenterlight and glasfaser contact address to Linthal and company name to "ungleich glarus ag"
|
||||||
* #3993: [dg] Fix new user membership payment by setting cardholder_name field for UserBillingAddressForm
|
* #3993: [dg] Fix new user membership payment by setting cardholder_name field for UserBillingAddressForm
|
||||||
|
* #3799: [dg] Make digital glarus billing work as monthly subscription
|
||||||
1.2.13: 2017-12-09
|
1.2.13: 2017-12-09
|
||||||
* [cms] Introduce UngleichHeaderBackgroundImageAndTextSliderPlugin that allows to have scrolling images and texts
|
* [cms] Introduce UngleichHeaderBackgroundImageAndTextSliderPlugin that allows to have scrolling images and texts
|
||||||
* [cms] Remove <p> tag for ungleich cms customer item template
|
* [cms] Remove <p> tag for ungleich cms customer item template
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.4 on 2017-12-23 22:56
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('digitalglarus', '0024_bookingcancellation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='membershiporder',
|
||||||
|
name='stripe_subscription_id',
|
||||||
|
field=models.CharField(max_length=100, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
|
import time
|
||||||
|
|
||||||
from datetime import datetime, date, timedelta
|
from datetime import datetime, date, timedelta
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -59,6 +59,17 @@ class MembershipType(models.Model):
|
||||||
return "{} - {}".format(datetime.strftime(start_date, "%b, %d %Y"),
|
return "{} - {}".format(datetime.strftime(start_date, "%b, %d %Y"),
|
||||||
datetime.strftime(end_date, "%b, %d %Y"))
|
datetime.strftime(end_date, "%b, %d %Y"))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def next_month_in_sec_since_epoch(self):
|
||||||
|
"""
|
||||||
|
First day of the next month expressed in seconds since the epoch time
|
||||||
|
:return: Time in seconds
|
||||||
|
"""
|
||||||
|
start_date, end_date = self.first_month_range
|
||||||
|
first_day_next_month = end_date + timedelta(days=1)
|
||||||
|
epoch_time = int(time.mktime(first_day_next_month.timetuple()))
|
||||||
|
return epoch_time
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
type = models.ForeignKey(MembershipType)
|
type = models.ForeignKey(MembershipType)
|
||||||
|
@ -71,12 +82,12 @@ class Membership(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_current_membership(cls, user):
|
def get_current_membership(cls, user):
|
||||||
|
has_order_current_month = Q(
|
||||||
has_order_current_month = Q(membershiporder__customer__user=user,
|
membershiporder__customer__user=user,
|
||||||
membershiporder__created_at__month=datetime.today().month)
|
membershiporder__created_at__month=datetime.today().month
|
||||||
|
)
|
||||||
# import pdb;pdb.set_trace()
|
# import pdb;pdb.set_trace()
|
||||||
return cls.objects.\
|
return cls.objects.filter(has_order_current_month).last()
|
||||||
filter(has_order_current_month).last()
|
|
||||||
|
|
||||||
# def get_current_active_membership(cls, user):
|
# def get_current_active_membership(cls, user):
|
||||||
# membership = cls.get_current_membership(user)
|
# membership = cls.get_current_membership(user)
|
||||||
|
@ -84,8 +95,7 @@ class Membership(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_user(cls, user):
|
def get_by_user(cls, user):
|
||||||
return cls.objects.\
|
return cls.objects.filter(membershiporder__customer__user=user).last()
|
||||||
filter(membershiporder__customer__user=user).last()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, data):
|
def create(cls, data):
|
||||||
|
@ -96,18 +106,23 @@ class Membership(models.Model):
|
||||||
def activate_or_crete(cls, data, user):
|
def activate_or_crete(cls, data, user):
|
||||||
membership = cls.get_by_user(user)
|
membership = cls.get_by_user(user)
|
||||||
membership_id = membership.id if membership else None
|
membership_id = membership.id if membership else None
|
||||||
obj, created = cls.objects.update_or_create(id=membership_id, defaults=data)
|
obj, created = cls.objects.update_or_create(
|
||||||
|
id=membership_id, defaults=data
|
||||||
|
)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_digitalglarus_active_member(cls, user):
|
def is_digitalglarus_active_member(cls, user):
|
||||||
# past_month = (datetime.today() - relativedelta(months=1)).month
|
# past_month = (datetime.today() - relativedelta(months=1)).month
|
||||||
has_order_current_month = Q(membershiporder__customer__user=user,
|
has_order_current_month = Q(
|
||||||
membershiporder__created_at__month=datetime.today().month)
|
membershiporder__customer__user=user,
|
||||||
|
membershiporder__created_at__month=datetime.today().month
|
||||||
|
)
|
||||||
# has_order_past_month = Q(membershiporder__customer__user=user,
|
# has_order_past_month = Q(membershiporder__customer__user=user,
|
||||||
# membershiporder__created_at__month=past_month)
|
# membershiporder__created_at__month=past_month)
|
||||||
active_membership = Q(active=True)
|
active_membership = Q(active=True)
|
||||||
# return cls.objects.filter(has_order_past_month | has_order_current_month).\
|
# return cls.objects.filter(
|
||||||
|
# has_order_past_month | has_order_current_month).\
|
||||||
return cls.objects.filter(has_order_current_month).\
|
return cls.objects.filter(has_order_current_month).\
|
||||||
filter(active_membership).exists()
|
filter(active_membership).exists()
|
||||||
|
|
||||||
|
@ -129,6 +144,7 @@ class MembershipOrder(Ordereable, models.Model):
|
||||||
membership = models.ForeignKey(Membership)
|
membership = models.ForeignKey(Membership)
|
||||||
start_date = models.DateField()
|
start_date = models.DateField()
|
||||||
end_date = models.DateField()
|
end_date = models.DateField()
|
||||||
|
stripe_subscription_id = models.CharField(max_length=100, null=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def current_membership_dates(cls, user):
|
def current_membership_dates(cls, user):
|
||||||
|
@ -172,10 +188,12 @@ class MembershipOrder(Ordereable, models.Model):
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, data):
|
def create(cls, data):
|
||||||
stripe_charge = data.pop('stripe_charge', None)
|
stripe_charge = data.pop('stripe_charge', None)
|
||||||
|
stripe_subscription_id = data.pop('stripe_subscription_id', None)
|
||||||
instance = cls.objects.create(**data)
|
instance = cls.objects.create(**data)
|
||||||
instance.stripe_charge_id = stripe_charge.id
|
instance.stripe_charge_id = stripe_charge.id
|
||||||
instance.last4 = stripe_charge.source.last4
|
instance.last4 = stripe_charge.source.last4
|
||||||
instance.cc_brand = stripe_charge.source.brand
|
instance.cc_brand = stripe_charge.source.brand
|
||||||
|
instance.stripe_subscription_id = stripe_subscription_id
|
||||||
instance.save()
|
instance.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
|
@ -95,12 +95,9 @@
|
||||||
<a class="btn btn-primary btn-grey btn-deactivate print" href="{% url 'digitalglarus:membership_deactivate' %}">Deactivate</a>
|
<a class="btn btn-primary btn-grey btn-deactivate print" href="{% url 'digitalglarus:membership_deactivate' %}">Deactivate</a>
|
||||||
</div>
|
</div>
|
||||||
{% elif not current_membership.active %}
|
{% elif not current_membership.active %}
|
||||||
<form method="POST" action="{% url 'digitalglarus:membership_reactivate' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="edit-button">
|
<div class="edit-button">
|
||||||
<button type="submit" class="btn btn-primary btn-grey btn-deactivate print" href="{% url 'digitalglarus:membership_reactivate' %}">Reactivate</button>
|
<a class="btn btn-primary btn-grey btn-deactivate" href="{% url 'digitalglarus:membership_pricing' %}">Reactivate</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="edit-button">
|
<div class="edit-button">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from model_mommy import mommy
|
from model_mommy import mommy
|
||||||
from unittest import mock
|
from unittest import mock, skipIf
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -126,6 +126,11 @@ class MembershipPaymentViewTest(BaseTestCase):
|
||||||
self.assertEqual(response.context['membership_type'],
|
self.assertEqual(response.context['membership_type'],
|
||||||
self.membership_type)
|
self.membership_type)
|
||||||
|
|
||||||
|
@skipIf(
|
||||||
|
settings.STRIPE_API_PRIVATE_KEY_TEST is None or
|
||||||
|
settings.STRIPE_API_PRIVATE_KEY_TEST is "",
|
||||||
|
"""Stripe details unavailable, so skipping CeleryTaskTestCase"""
|
||||||
|
)
|
||||||
@mock.patch('utils.stripe_utils.StripeUtils.create_customer')
|
@mock.patch('utils.stripe_utils.StripeUtils.create_customer')
|
||||||
def test_post(self, stripe_mocked_call):
|
def test_post(self, stripe_mocked_call):
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
@ -9,25 +11,34 @@ from django.utils.translation import get_language
|
||||||
from djangocms_blog.models import Post
|
from djangocms_blog.models import Post
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from .models import Supporter
|
|
||||||
from .mixins import ChangeMembershipStatusMixin
|
|
||||||
from utils.forms import ContactUsForm
|
from utils.forms import ContactUsForm
|
||||||
from utils.mailer import BaseEmail
|
from utils.mailer import BaseEmail
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
from membership.models import StripeCustomer
|
from membership.models import StripeCustomer
|
||||||
from utils.views import LoginViewMixin, SignupViewMixin, \
|
from utils.views import (
|
||||||
PasswordResetViewMixin, PasswordResetConfirmViewMixin
|
LoginViewMixin, SignupViewMixin, PasswordResetViewMixin,
|
||||||
from utils.forms import PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm
|
PasswordResetConfirmViewMixin
|
||||||
|
)
|
||||||
|
from utils.forms import (
|
||||||
|
PasswordResetRequestForm, UserBillingAddressForm, EditCreditCardForm
|
||||||
|
)
|
||||||
from utils.stripe_utils import StripeUtils
|
from utils.stripe_utils import StripeUtils
|
||||||
from utils.models import UserBillingAddress
|
from utils.models import UserBillingAddress
|
||||||
|
from utils.tasks import send_plain_email_task
|
||||||
|
|
||||||
from .forms import LoginForm, SignupForm, MembershipBillingForm, BookingDateForm,\
|
from .forms import (
|
||||||
|
LoginForm, SignupForm, MembershipBillingForm, BookingDateForm,
|
||||||
BookingBillingForm, CancelBookingForm
|
BookingBillingForm, CancelBookingForm
|
||||||
|
)
|
||||||
|
from .models import (
|
||||||
|
MembershipType, Membership, MembershipOrder, Booking, BookingPrice,
|
||||||
|
BookingOrder, BookingCancellation, Supporter
|
||||||
|
)
|
||||||
|
from .mixins import (
|
||||||
|
MembershipRequiredMixin, IsNotMemberMixin, ChangeMembershipStatusMixin
|
||||||
|
)
|
||||||
|
|
||||||
from .models import MembershipType, Membership, MembershipOrder, Booking, BookingPrice,\
|
logger = logging.getLogger(__name__)
|
||||||
BookingOrder, BookingCancellation
|
|
||||||
|
|
||||||
from .mixins import MembershipRequiredMixin, IsNotMemberMixin
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
class IndexView(TemplateView):
|
||||||
|
@ -271,7 +282,6 @@ class BookingPaymentView(LoginRequiredMixin, MembershipRequiredMixin, FormView):
|
||||||
booking_data = {
|
booking_data = {
|
||||||
'start_date': start_date,
|
'start_date': start_date,
|
||||||
'end_date': end_date,
|
'end_date': end_date,
|
||||||
'start_date': start_date,
|
|
||||||
'free_days': free_days,
|
'free_days': free_days,
|
||||||
'price': normal_price,
|
'price': normal_price,
|
||||||
'final_price': final_price,
|
'final_price': final_price,
|
||||||
|
@ -355,16 +365,21 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
|
||||||
membership_type = data.get('membership_type')
|
membership_type = data.get('membership_type')
|
||||||
|
|
||||||
# Get or create stripe customer
|
# Get or create stripe customer
|
||||||
customer = StripeCustomer.get_or_create(email=self.request.user.email,
|
customer = StripeCustomer.get_or_create(
|
||||||
token=token)
|
email=self.request.user.email, token=token
|
||||||
|
)
|
||||||
if not customer:
|
if not customer:
|
||||||
form.add_error("__all__", "Invalid credit card")
|
form.add_error("__all__", "Invalid credit card")
|
||||||
return self.render_to_response(self.get_context_data(form=form))
|
return self.render_to_response(
|
||||||
|
self.get_context_data(form=form)
|
||||||
|
)
|
||||||
|
|
||||||
# Make stripe charge to a customer
|
# Make stripe charge to a customer
|
||||||
stripe_utils = StripeUtils()
|
stripe_utils = StripeUtils()
|
||||||
charge_response = stripe_utils.make_charge(amount=membership_type.first_month_price,
|
charge_response = stripe_utils.make_charge(
|
||||||
customer=customer.stripe_id)
|
amount=membership_type.first_month_price,
|
||||||
|
customer=customer.stripe_id
|
||||||
|
)
|
||||||
charge = charge_response.get('response_object')
|
charge = charge_response.get('response_object')
|
||||||
|
|
||||||
# Check if the payment was approved
|
# Check if the payment was approved
|
||||||
|
@ -373,6 +388,58 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
|
||||||
'paymentError': charge_response.get('error'),
|
'paymentError': charge_response.get('error'),
|
||||||
'form': form
|
'form': form
|
||||||
})
|
})
|
||||||
|
email_to_admin_data = {
|
||||||
|
'subject': "Could not create charge for Digital Glarus "
|
||||||
|
"user: {user}".format(
|
||||||
|
user=self.request.user.email
|
||||||
|
),
|
||||||
|
'from_email': 'info@digitalglarus.ch',
|
||||||
|
'to': ['info@ungleich.ch'],
|
||||||
|
'body': "\n".join(
|
||||||
|
["%s=%s" % (k, v) for (k, v) in
|
||||||
|
charge_response.items()]),
|
||||||
|
}
|
||||||
|
send_plain_email_task.delay(email_to_admin_data)
|
||||||
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
|
# Subscribe the customer to dg plan from the next month onwards
|
||||||
|
stripe_plan = stripe_utils.get_or_create_stripe_plan(
|
||||||
|
amount=membership_type.price,
|
||||||
|
name='Digital Glarus {sub_type_name} Subscription'.format(
|
||||||
|
sub_type_name=membership_type.name
|
||||||
|
),
|
||||||
|
stripe_plan_id='dg-{sub_type_name}'.format(
|
||||||
|
sub_type_name=membership_type.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
subscription_result = stripe_utils.subscribe_customer_to_plan(
|
||||||
|
customer.stripe_id,
|
||||||
|
[{"plan": stripe_plan.get('response_object').stripe_plan_id}],
|
||||||
|
trial_end=membership_type.next_month_in_sec_since_epoch
|
||||||
|
)
|
||||||
|
stripe_subscription_obj = subscription_result.get(
|
||||||
|
'response_object'
|
||||||
|
)
|
||||||
|
# Check if call to create subscription was ok
|
||||||
|
if (stripe_subscription_obj is None or
|
||||||
|
(stripe_subscription_obj.status != 'active' and
|
||||||
|
stripe_subscription_obj.status != 'trialing')):
|
||||||
|
context.update({
|
||||||
|
'paymentError': subscription_result.get('error'),
|
||||||
|
'form': form
|
||||||
|
})
|
||||||
|
email_to_admin_data = {
|
||||||
|
'subject': "Could not create Stripe subscription for "
|
||||||
|
"Digital Glarus user: {user}".format(
|
||||||
|
user=self.request.user.email
|
||||||
|
),
|
||||||
|
'from_email': 'info@digitalglarus.ch',
|
||||||
|
'to': ['info@ungleich.ch'],
|
||||||
|
'body': "\n".join(
|
||||||
|
["%s=%s" % (k, v) for (k, v) in
|
||||||
|
subscription_result.items()]),
|
||||||
|
}
|
||||||
|
send_plain_email_task.delay(email_to_admin_data)
|
||||||
return render(request, self.template_name, context)
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
charge = charge_response.get('response_object')
|
charge = charge_response.get('response_object')
|
||||||
|
@ -412,6 +479,7 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
|
||||||
'customer': customer,
|
'customer': customer,
|
||||||
'billing_address': billing_address,
|
'billing_address': billing_address,
|
||||||
'stripe_charge': charge,
|
'stripe_charge': charge,
|
||||||
|
'stripe_subscription_id': stripe_subscription_obj.id,
|
||||||
'amount': membership_type.first_month_price,
|
'amount': membership_type.first_month_price,
|
||||||
'start_date': membership_start_date,
|
'start_date': membership_start_date,
|
||||||
'end_date': membership_end_date
|
'end_date': membership_end_date
|
||||||
|
@ -481,8 +549,43 @@ class MembershipDeactivateView(LoginRequiredMixin, UpdateView):
|
||||||
def post(self, *args, **kwargs):
|
def post(self, *args, **kwargs):
|
||||||
membership = self.get_object()
|
membership = self.get_object()
|
||||||
membership.deactivate()
|
membership.deactivate()
|
||||||
|
messages.add_message(
|
||||||
messages.add_message(self.request, messages.SUCCESS, self.success_message)
|
self.request, messages.SUCCESS, self.success_message
|
||||||
|
)
|
||||||
|
# cancel Stripe subscription
|
||||||
|
stripe_utils = StripeUtils()
|
||||||
|
membership_order = MembershipOrder.objects.filter(
|
||||||
|
customer__user=self.request.user
|
||||||
|
).last()
|
||||||
|
if membership_order:
|
||||||
|
if membership_order.stripe_subscription_id:
|
||||||
|
result = stripe_utils.unsubscribe_customer(
|
||||||
|
subscription_id=membership_order.stripe_subscription_id
|
||||||
|
)
|
||||||
|
stripe_subscription_obj = result.get('response_object')
|
||||||
|
# Check if the subscription was canceled
|
||||||
|
if (stripe_subscription_obj is None or
|
||||||
|
stripe_subscription_obj.status != 'canceled'):
|
||||||
|
error_msg = result.get('error')
|
||||||
|
logger.error(
|
||||||
|
"Could not cancel Digital Glarus subscription. "
|
||||||
|
"Reason: {reason}".format(
|
||||||
|
reason=error_msg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"User {user} may have Stripe subscriptions created "
|
||||||
|
"manually. Please check.".format(
|
||||||
|
user=self.request.user.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"MembershipOrder for {user} not found".format(
|
||||||
|
user=self.request.user.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return HttpResponseRedirect(self.success_url)
|
return HttpResponseRedirect(self.success_url)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
{% load i18n %}
|
||||||
{% if instance.image %}
|
{% if instance.image %}
|
||||||
<div class="bg_img" style="background-image:url({{ instance.image.url }})"></div>
|
<div class="bg_img" style="background-image:url({{ instance.image.url }})"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -210,12 +210,14 @@ class StripeUtils(object):
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
@handleStripeError
|
@handleStripeError
|
||||||
def subscribe_customer_to_plan(self, customer, plans):
|
def subscribe_customer_to_plan(self, customer, plans, trial_end=None):
|
||||||
"""
|
"""
|
||||||
Subscribes the given customer to the list of given plans
|
Subscribes the given customer to the list of given plans
|
||||||
|
|
||||||
:param customer: The stripe customer identifier
|
:param customer: The stripe customer identifier
|
||||||
:param plans: A list of stripe plans.
|
:param plans: A list of stripe plans.
|
||||||
|
:param trial_end: An integer representing when the Stripe subscription
|
||||||
|
is supposed to end
|
||||||
Ref: https://stripe.com/docs/api/python#create_subscription-items
|
Ref: https://stripe.com/docs/api/python#create_subscription-items
|
||||||
e.g.
|
e.g.
|
||||||
plans = [
|
plans = [
|
||||||
|
@ -227,8 +229,7 @@ class StripeUtils(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
subscription_result = self.stripe.Subscription.create(
|
subscription_result = self.stripe.Subscription.create(
|
||||||
customer=customer,
|
customer=customer, items=plans, trial_end=trial_end
|
||||||
items=plans,
|
|
||||||
)
|
)
|
||||||
return subscription_result
|
return subscription_result
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue