Add prototype monthly recurring payment via Stripe
This commit is contained in:
parent
26a6928a20
commit
ce4ae22fa5
6 changed files with 110 additions and 9 deletions
|
@ -3,6 +3,8 @@ from django import forms
|
||||||
from .models import SustainerOrder
|
from .models import SustainerOrder
|
||||||
|
|
||||||
class SustainerForm(forms.ModelForm):
|
class SustainerForm(forms.ModelForm):
|
||||||
|
amount_monthly = forms.IntegerField(initial=12, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SustainerOrder
|
model = SustainerOrder
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -22,4 +24,6 @@ class SustainerForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['amount'].widget.attrs['style'] = 'width: 5rem'
|
self.fields['amount'].widget.attrs['style'] = 'width: 5rem'
|
||||||
|
self.fields['amount'].initial = 128
|
||||||
|
self.fields['amount_monthly'].widget.attrs['style'] = 'width: 5rem'
|
||||||
self.fields['tshirt_size'].widget.attrs['x-model'] = 'tshirt_size'
|
self.fields['tshirt_size'].widget.attrs['x-model'] = 'tshirt_size'
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Generated by Django 4.2.16 on 2024-09-18 01:27
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('supporters', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sustainerorder',
|
||||||
|
name='monthly_recurring',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sustainerorder',
|
||||||
|
name='amount',
|
||||||
|
field=models.IntegerField(
|
||||||
|
validators=[django.core.validators.MinValueValidator(100)]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sustainerorder',
|
||||||
|
name='paid_time',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sustainerorder',
|
||||||
|
name='tshirt_size',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
('', (('None', 'None'),)),
|
||||||
|
(
|
||||||
|
"Men's",
|
||||||
|
(
|
||||||
|
("Men's S", "Men's S"),
|
||||||
|
("Men's M", "Men's M"),
|
||||||
|
("Men's L", "Men's L"),
|
||||||
|
("Men's XL", "Men's XL"),
|
||||||
|
("Men's 2XL", "Men's 2XL"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Standard women's",
|
||||||
|
(
|
||||||
|
("Standard women's S", "Standard women's S"),
|
||||||
|
("Standard women's M", "Standard women's M"),
|
||||||
|
("Standard women's L", "Standard women's L"),
|
||||||
|
("Standard women's XL", "Standard women's XL"),
|
||||||
|
("Standard women's 2XL", "Standard women's 2XL"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Fitted women's",
|
||||||
|
(
|
||||||
|
("Fitted women's S", "Fitted women's S"),
|
||||||
|
("Fitted women's M", "Fitted women's M"),
|
||||||
|
("Fitted women's L", "Fitted women's L"),
|
||||||
|
("Fitted women's XL", "Fitted women's XL"),
|
||||||
|
("Fitted women's 2XL", "Fitted women's 2XL"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -60,10 +60,10 @@ class SustainerOrder(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
amount = models.IntegerField(
|
amount = models.IntegerField(
|
||||||
default=128,
|
|
||||||
validators=[
|
validators=[
|
||||||
validators.MinValueValidator(100),
|
validators.MinValueValidator(100),
|
||||||
])
|
])
|
||||||
|
monthly_recurring = models.BooleanField(default=False)
|
||||||
paid_time = models.DateTimeField(null=True, blank=True)
|
paid_time = models.DateTimeField(null=True, blank=True)
|
||||||
acknowledge_publicly = models.BooleanField(default=False)
|
acknowledge_publicly = models.BooleanField(default=False)
|
||||||
add_to_mailing_list = models.BooleanField(default=False)
|
add_to_mailing_list = models.BooleanField(default=False)
|
||||||
|
|
|
@ -27,6 +27,9 @@
|
||||||
progress::-webkit-progress-value {
|
progress::-webkit-progress-value {
|
||||||
background: #224c57;
|
background: #224c57;
|
||||||
}
|
}
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -275,7 +278,7 @@ reach for reproducibility. </p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt4">
|
<div class="mt4">
|
||||||
<a href="{% url "stripe2" %}">
|
<a href="{% url "stripe2" %}">
|
||||||
<button type="submit" class="pointer" style="height: 40px; width: 100%; font-size: 18px; font-weight: bold; color: white; background-color: var(--orange); border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Become a Sustainer!</button>
|
<button type="submit" class="pointer btn" style="height: 40px; width: 100%; font-size: 18px; font-weight: bold; color: white; background-color: var(--orange); border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Become a Sustainer!</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,11 @@
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script defer src="{% static "js/vendor/alpine-3.14.1.js" %}"></script>
|
<script defer src="{% static "js/vendor/alpine-3.14.1.js" %}"></script>
|
||||||
|
<style>
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -17,8 +22,10 @@
|
||||||
x-data="{
|
x-data="{
|
||||||
tshirt_size: 'None',
|
tshirt_size: 'None',
|
||||||
tshirt_required: function () { return this.tshirt_size !== 'None' },
|
tshirt_required: function () { return this.tshirt_size !== 'None' },
|
||||||
|
recurring: 'once',
|
||||||
}">
|
}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.errors }}
|
||||||
<div class="mb2"><label>Name
|
<div class="mb2"><label>Name
|
||||||
<span class="db mt1">{{ form.name }}</span>
|
<span class="db mt1">{{ form.name }}</span>
|
||||||
</label></div>
|
</label></div>
|
||||||
|
@ -27,9 +34,16 @@
|
||||||
</label>
|
</label>
|
||||||
<p class="f7 black-60 mt1">To send your receipt</p>
|
<p class="f7 black-60 mt1">To send your receipt</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb2"><label>Amount
|
<div class="mb2"><label>
|
||||||
|
<label class="mr1"><input type="radio" name="recurring" value="once" x-model="recurring"> Once</label>
|
||||||
|
<label><input type="radio" name="recurring" value="monthly" x-model="recurring"> Monthly</label>
|
||||||
|
</label></div>
|
||||||
|
<div class="mb2" x-show="recurring === 'once'"><label>Amount
|
||||||
<span class="db mt1">$ {{ form.amount }}</span>
|
<span class="db mt1">$ {{ form.amount }}</span>
|
||||||
</label></div>
|
</label></div>
|
||||||
|
<div class="mb2" x-show="recurring === 'monthly'"><label>Amount
|
||||||
|
<span class="db mt1">$ {{ form.amount_monthly }}</span>
|
||||||
|
</label></div>
|
||||||
<div class="mv3"><label class="lh-title"><input type="checkbox"> Acknowledge me on the public <a href="">list of sustainers</a></label></div>
|
<div class="mv3"><label class="lh-title"><input type="checkbox"> Acknowledge me on the public <a href="">list of sustainers</a></label></div>
|
||||||
<div class="mv3"><label class="lh-title"><input type="checkbox"> Add me to the low-traffic <a href="https://lists.sfconservancy.org/pipermail/announce/">announcements</a> email list</label></div>
|
<div class="mv3"><label class="lh-title"><input type="checkbox"> Add me to the low-traffic <a href="https://lists.sfconservancy.org/pipermail/announce/">announcements</a> email list</label></div>
|
||||||
<div class="mv3">
|
<div class="mv3">
|
||||||
|
@ -63,7 +77,9 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="mt3"><button type="submit" style="height: 40px; width: 100%; font-size: 18px; font-weight: bold; color: white; background-color: var(--orange); border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Pay via Stripe</button></div>
|
<div class="mt3"><button type="submit" class="btn" style="height: 40px; width: 100%; font-size: 18px; font-weight: bold; color: white; background-color: var(--orange); border-radius: 0.5rem; border: none; border-bottom: 2px solid rgba(0,0,0,0.1);">Pay via Stripe</button></div>
|
||||||
|
|
||||||
|
<p class="f7 mt3">If you have concerns or issues paying with Stripe, we also accept payment by <a href="#">paper check</a> and <a href="#">wire transfer</a>.</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -45,7 +45,8 @@ def sponsors(request):
|
||||||
return render(request, "supporters/sponsors.html", c)
|
return render(request, "supporters/sponsors.html", c)
|
||||||
|
|
||||||
|
|
||||||
def create_checkout_session(reference_id, email, amount, base_url):
|
def create_checkout_session(reference_id, email: str, amount: int, recurring: bool, base_url: str):
|
||||||
|
# https://docs.stripe.com/payments/accept-a-payment
|
||||||
YOUR_DOMAIN = base_url
|
YOUR_DOMAIN = base_url
|
||||||
try:
|
try:
|
||||||
checkout_session = stripe.checkout.Session.create(
|
checkout_session = stripe.checkout.Session.create(
|
||||||
|
@ -55,13 +56,15 @@ def create_checkout_session(reference_id, email, amount, base_url):
|
||||||
'price_data': {
|
'price_data': {
|
||||||
'currency': 'usd',
|
'currency': 'usd',
|
||||||
'product_data': {'name': 'Contribution'},
|
'product_data': {'name': 'Contribution'},
|
||||||
'unit_amount': amount * 100,
|
'unit_amount': amount * 100, # in cents
|
||||||
|
# https://docs.stripe.com/products-prices/pricing-models#variable-pricing
|
||||||
|
'recurring': {'interval': 'month'} if recurring else None,
|
||||||
},
|
},
|
||||||
'quantity': 1,
|
'quantity': 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
customer_email=email,
|
customer_email=email,
|
||||||
mode='payment',
|
mode='subscription' if recurring else 'payment',
|
||||||
success_url=YOUR_DOMAIN + '/sustainer/success/?session_id={CHECKOUT_SESSION_ID}',
|
success_url=YOUR_DOMAIN + '/sustainer/success/?session_id={CHECKOUT_SESSION_ID}',
|
||||||
cancel_url=YOUR_DOMAIN + '/sustainer/stripe/',
|
cancel_url=YOUR_DOMAIN + '/sustainer/stripe/',
|
||||||
)
|
)
|
||||||
|
@ -78,9 +81,13 @@ def sustainers_stripe2(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = forms.SustainerForm(request.POST)
|
form = forms.SustainerForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
order = form.save()
|
order = form.save(commit=False)
|
||||||
|
if form.data['recurring'] == 'monthly':
|
||||||
|
order.amount = form.cleaned_data['amount_monthly']
|
||||||
|
order.monthly_recurring = True
|
||||||
|
order.save()
|
||||||
base_url = f'{request.scheme}://{request.get_host()}'
|
base_url = f'{request.scheme}://{request.get_host()}'
|
||||||
stripe_checkout_url = create_checkout_session(order.id, order.email, order.amount, base_url)
|
stripe_checkout_url = create_checkout_session(order.id, order.email, order.amount, order.monthly_recurring, base_url)
|
||||||
return redirect(stripe_checkout_url)
|
return redirect(stripe_checkout_url)
|
||||||
else:
|
else:
|
||||||
form = forms.SustainerForm()
|
form = forms.SustainerForm()
|
||||||
|
|
Loading…
Reference in a new issue