Add prototype monthly recurring payment via Stripe

This commit is contained in:
Ben Sturmfels 2024-09-18 15:34:59 +10:00
parent 26a6928a20
commit ce4ae22fa5
Signed by: bsturmfels
GPG key ID: 023C05E2C9C068F0
6 changed files with 110 additions and 9 deletions

View file

@ -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'

View file

@ -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,
),
),
]

View file

@ -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)

View file

@ -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>

View file

@ -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 %}

View file

@ -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()