Changeset - ce4ae22fa59e
[Not reviewed]
0 5 1
Ben Sturmfels (bsturmfels) - 1 month ago 2024-09-18 05:34:59
ben@sturm.com.au
Add prototype monthly recurring payment via Stripe
6 files changed with 110 insertions and 9 deletions:
0 comments (0 inline, 0 general)
conservancy/supporters/forms.py
Show inline comments
 
from django import forms
 

	
 
from .models import SustainerOrder
 

	
 
class SustainerForm(forms.ModelForm):
 
    amount_monthly = forms.IntegerField(initial=12, required=False)
 

	
 
    class Meta:
 
        model = SustainerOrder
 
        fields = [
 
            'name',
 
            'email',
 
            'amount',
 
            'acknowledge_publicly',
 
            'add_to_mailing_list',
 
            'tshirt_size',
 
            'street',
 
            'city',
 
            'state',
 
            'zip_code',
 
            'country',
 
        ]
 

	
 
    def __init__(self, *args, **kwargs):
 
        super().__init__(*args, **kwargs)
 
        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'
conservancy/supporters/migrations/0002_sustainerorder_monthly_recurring_and_more.py
Show inline comments
 
new file 100644
 
# 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,
 
            ),
 
        ),
 
    ]
conservancy/supporters/models.py
Show inline comments
...
 
@@ -51,28 +51,28 @@ class SustainerOrder(models.Model):
 
                ("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"),
 
            ),
 
        ),
 
    ]
 

	
 
    created_time = models.DateTimeField(auto_now_add=True)
 
    name = models.CharField(max_length=255)
 
    email = models.EmailField()
 
    amount = models.IntegerField(
 
        default=128,
 
        validators=[
 
            validators.MinValueValidator(100),
 
        ])
 
    monthly_recurring = models.BooleanField(default=False)
 
    paid_time = models.DateTimeField(null=True, blank=True)
 
    acknowledge_publicly = models.BooleanField(default=False)
 
    add_to_mailing_list = models.BooleanField(default=False)
 
    tshirt_size = models.CharField(max_length=50, choices=TSHIRT_CHOICES)
 
    street = models.CharField(max_length=255, blank=True)
 
    city = models.CharField(max_length=255, blank=True)
 
    state = models.CharField(max_length=255, blank=True)
 
    zip_code = models.CharField(max_length=255, blank=True)
 
    country = models.CharField(max_length=255, blank=True)
 

	
 
    def __str__(self):
 
        return f'Sustainer order {self.id}: {self.email}'
conservancy/supporters/templates/supporters/sustainers_stripe.html
Show inline comments
...
 
@@ -18,24 +18,27 @@
 
     background: #ddd;
 
     border: none;
 
   }
 
   progress::-moz-progress-bar {
 
     background: #224c57;
 
   }
 
   progress::-webkit-progress-bar {
 
     background: #ddd;
 
   }
 
   progress::-webkit-progress-value {
 
     background: #224c57;
 
   }
 
   .btn:active {
 
     transform: scale(1.05);
 
   }
 
  </style>
 
{% endblock %}
 

	
 
{% block content %}
 
  <h1 class="lh-title tc mt4 mb0">Become a Sustainer Now</h1>
 
  <p class="measure-wide center tc">Sustainers help us do our work in a strategic, long-term way.</p>
 

	
 
  <div id="sustainer-grid" class="mv4">
 
    <div style="grid-row: 1 / span 2">
 
      <video controls poster="https://sfconservancy.org/videos/sfc-introduction-video_poster.jpg" class="mb3">
 
        <source src="https://sfconservancy.org/videos/sfc-introduction_1080p.mp4">
 
        <track src="/docs/sfc-introduction-vtt-captions.txt" kind="subtitles" srclang="en" label="English">
...
 
@@ -266,25 +269,25 @@ reach for reproducibility. </p>
 
    <div>
 
      <progress min="0" max="100" value="84.5" class="w-100">84.5%</progress>
 
      <div class="mv3">
 
        <div class="f2 b">$15,558</div>
 
        <div class="f5 b black-50">Remaining of the $100,000 goal</div>
 
      </div>
 
      <div class="mv3">
 
        <div class="f2 b">15 days</div>
 
        <div class="f5 b black-50">Remaining</div>
 
      </div>
 
      <div class="mt4">
 
        <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>
 
      </div>
 

	
 
      <div class="mt3">
 
        <figure>
 
          <img src="/static/img/tshirt-2023.png" alt="Software Freedom Conservancy T-shirt">
 
          <figcaption class="tc black-70" style="margin-top: -20px">Our new Sustainer T-shirt</figcaption>
 
        </figure>
 
      </div>
 
    </div>
 
  </div>
 
{% endblock %}
conservancy/supporters/templates/supporters/sustainers_stripe2.html
Show inline comments
 
{% extends "base_conservancy.html" %}
 
{% load static %}
 
{% block subtitle %}Support Conservancy - {% endblock %}
 
{% block category %}sustainer{% endblock %}
 

	
 
{% block head %}
 
  {{ block.super }}
 
  <script defer src="{% static "js/vendor/alpine-3.14.1.js" %}"></script>
 
  <style>
 
   .btn:active {
 
     transform: scale(1.05);
 
   }
 
  </style>
 
{% endblock %}
 

	
 
{% block content %}
 
  <h1 class="lh-title tc mt4 mb0">Become a Sustainer Now</h1>
 
  <p class="measure-wide center tc">Sustainers help us do our work in a strategic, long-term way.</p>
 

	
 
  <div class="bg-black-05 pa3 br3 mb4 center" style="max-width: 24rem; border: 1px solid #ccc">
 
    <form id="sustainer" method="post" action="."
 
          x-data="{
 
                    tshirt_size: 'None',
 
                    tshirt_required: function () { return this.tshirt_size !== 'None' },
 
                    recurring: 'once',
 
                  }">
 
      {% csrf_token %}
 
      {{ form.errors }}
 
      <div class="mb2"><label>Name
 
        <span class="db mt1">{{ form.name }}</span>
 
      </label></div>
 
      <div class="mb2"><label>Email
 
        <span class="db mt1">{{ form.email }}</span>
 
      </label>
 
      <p class="f7 black-60 mt1">To send your receipt</p>
 
      </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>
 
      </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"> Add me to the low-traffic <a href="https://lists.sfconservancy.org/pipermail/announce/">announcements</a> email list</label></div>
 
      <div class="mv3">
 
        <label>T-shirt:
 
          <!-- Form field has an x-model attribute in forms.py. -->
 
          <span class="db mt1">{{ form.tshirt_size }}</span>
 
        </label>
 
        <p class="f7 black-60 mt1">Sizing:
 
          <a href="https://sfconservancy.org/videos/women-2017-to-2020-t-shirt-sizing.jpg" target="_blank" class="black-60">Women's</a>,
 
          <a href="https://sfconservancy.org/videos/men-2017-to-2020-t-shirt-sizing.jpg" target="_blank" class="black-60">Men's</a></p>
 
      </div>
 
      <!-- Using Alpine.js to show/hide the address based on T-shirt choice. -->
...
 
@@ -54,16 +68,18 @@
 
        <div class="mb2"><label>State/Region
 
          <span class="db mt1">{{ form.state }}</span>
 
        </label></div>
 
        <div class="mb2"><label>Zip/Postal
 
          <span class="db mt1">{{ form.zip_code }}</span>
 
        </label></div>
 
        <div class="mb2"><label>Country
 
          <span class="db mt1">{{ form.country }}</span>
 
        </label></div>
 
        </fieldset>
 
      </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>
 
  </div>
 
{% endblock %}
conservancy/supporters/views.py
Show inline comments
...
 
@@ -36,60 +36,67 @@ def sponsors(request):
 
    supporters = Supporter.objects.all().filter(display_until_date__gte=datetime.now())
 
    supporters_count = len(supporters)
 
    anonymous_count  = len(supporters.filter(display_name='Anonymous'))
 
    supporters = supporters.exclude(display_name='Anonymous').order_by('ledger_entity_id')
 
    c = {
 
        'supporters' : supporters,
 
        'supporters_count' : supporters_count,
 
        'anonymous_count' : anonymous_count
 
    }
 
    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
 
    try:
 
        checkout_session = stripe.checkout.Session.create(
 
            client_reference_id=str(reference_id),
 
            line_items=[
 
                {
 
                    'price_data': {
 
                        'currency': 'usd',
 
                        '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,
 
                },
 
            ],
 
            customer_email=email,
 
            mode='payment',
 
            mode='subscription' if recurring else 'payment',
 
            success_url=YOUR_DOMAIN + '/sustainer/success/?session_id={CHECKOUT_SESSION_ID}',
 
            cancel_url=YOUR_DOMAIN + '/sustainer/stripe/',
 
        )
 
    except Exception as e:
 
        return str(e)
 
    return checkout_session.url
 

	
 

	
 
def sustainers_stripe(request):
 
    return render(request, 'supporters/sustainers_stripe.html', {})
 

	
 

	
 
def sustainers_stripe2(request):
 
    if request.method == 'POST':
 
        form = forms.SustainerForm(request.POST)
 
        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()}'
 
            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)
 
    else:
 
        form = forms.SustainerForm()
 
    return render(request, 'supporters/sustainers_stripe2.html', {'form': form})
 

	
 

	
 
stripe.api_key = 'sk_test_zaAqrpHmpkXnHQfAs4UWkE3d'
 

	
 
def fulfill_checkout(session_id):
 
    print("Fulfilling Checkout Session", session_id)
 

	
 
    # TODO: Make this function safe to run multiple times,
0 comments (0 inline, 0 general)