Changeset - 3fe83d14667f
[Not reviewed]
0 3 1
Ben Sturmfels (bsturmfels) - 18 days ago 2024-09-30 07:40:29
ben@sturm.com.au
Add annual renew
4 files changed with 57 insertions and 15 deletions:
0 comments (0 inline, 0 general)
conservancy/supporters/migrations/0003_remove_sustainerorder_monthly_recurring_and_more.py
Show inline comments
 
new file 100644
 
# Generated by Django 4.2.11 on 2024-09-30 03:33
 

	
 
from django.db import migrations, models
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('supporters', '0002_sustainerorder_monthly_recurring_and_more'),
 
    ]
 

	
 
    operations = [
 
        migrations.RemoveField(
 
            model_name='sustainerorder',
 
            name='monthly_recurring',
 
        ),
 
        migrations.AddField(
 
            model_name='sustainerorder',
 
            name='recurring',
 
            field=models.CharField(default='', max_length=10),
 
            preserve_default=False,
 
        ),
 
        migrations.AlterField(
 
            model_name='sustainerorder',
 
            name='acknowledge_publicly',
 
            field=models.BooleanField(default=True),
 
        ),
 
        migrations.AlterField(
 
            model_name='sustainerorder',
 
            name='add_to_mailing_list',
 
            field=models.BooleanField(default=True),
 
        ),
 
    ]
conservancy/supporters/models.py
Show inline comments
 
from django.core import validators
 
from django.db import models
 

	
 

	
 
class Supporter(models.Model):
 
    """Conservancy Supporter listing"""
 

	
 
    display_name = models.CharField(max_length=200, blank=False)
 
    display_until_date = models.DateTimeField("date until which this supporter name is displayed")
 
    ledger_entity_id = models.CharField(max_length=200, blank=False)
 

	
 
    def test(self):
 
        return "TESTING"
 
    def __str__(self):
 
        return self.display_name
 

	
 
    class Meta:
 
        ordering = ('ledger_entity_id',)
 

	
 

	
 
class SustainerOrder(models.Model):
 
    RENEW_CHOICES = [
 
        ('', 'None'),
 
        ('month', 'Monthly'),
 
        ('year', 'Annual'),
 
    ]
 
    TSHIRT_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"),
 
            ),
 
        ),
 
    ]
 

	
 
    created_time = models.DateTimeField(auto_now_add=True)
 
    name = models.CharField(max_length=255)
 
    email = models.EmailField()
 
    amount = models.IntegerField(
 
        validators=[
 
            validators.MinValueValidator(100),
 
        ])
 
    monthly_recurring = models.BooleanField(default=False)
 
    recurring = models.CharField(max_length=10)
 
    paid_time = models.DateTimeField(null=True, blank=True)
 
    acknowledge_publicly = models.BooleanField(default=False)
 
    add_to_mailing_list = models.BooleanField(default=False)
 
    acknowledge_publicly = models.BooleanField(default=True)
 
    add_to_mailing_list = models.BooleanField(default=True)
 
    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}'
 

	
 
    def paid(self):
 
        return self.paid_time is not None
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>
 
        <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 class="mr1"><input type="radio" name="recurring" value="" x-model="recurring"> Once</label>
 
        <label class="mr1"><input type="radio" name="recurring" value="month" x-model="recurring"> Monthly</label>
 
        <label><input type="radio" name="recurring" value="year" x-model="recurring"> Annual</label>
 
      </label></div>
 
      <div class="mb2" x-show="recurring === 'once'"><label>Amount
 
      <div class="mb2" x-show="recurring === ''"><label>Amount
 
        <span class="db mt1">$ {{ form.amount }}</span>
 
      </label></div>
 
      <div class="mb2" x-show="recurring === 'monthly'"><label>Amount
 
      <div class="mb2" x-show="recurring === 'month'"><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>
 
      </label></div>
 
      <div class="mb2" x-show="recurring === 'year'"><label>Amount
 
          <span class="db mt1">$ {{ form.amount }}</span>
 
      </label></div>
 
      <div class="mv3"><label class="lh-title">{{ form.acknowledge_publicly }} Acknowledge me on the public <a href="">list of sustainers</a></label></div>
 
      <div class="mv3"><label class="lh-title">{{ form.add_to_mailing_list }} 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. -->
 
      <template x-if="tshirt_required">
 
        <fieldset id="address">
 
          <legend>Postal address</legend>
 
        <div class="mb2"><label>Street
 
          <span class="db mt1">{{ form.street }}</span>
 
        </label></div>
 
        <div class="mb2"><label>City
 
          <span class="db mt1">{{ form.city }}</span>
 
        </label></div>
 
        <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" 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
 
from datetime import datetime
 
import logging
 

	
 
from django.http import HttpResponse
 
from django.shortcuts import render, redirect
 
from django.utils import timezone
 
import stripe
 

	
 
from .. import ParameterValidator
 
from . import forms
 
from .models import Supporter, SustainerOrder
 

	
 
logger = logging.getLogger(__name__)
 

	
 
def sustainers(request):
 
    with ParameterValidator(request.GET, 'upgrade_id') as validator:
 
        try:
 
            amount_param = float(request.GET['upgrade'])
 
        except (KeyError, ValueError):
 
            validator.fail()
 
        else:
 
            validator.validate('{:.2f}'.format(amount_param))
 
    partial_amount = amount_param if validator.valid else 0
 
    context = {
 
        'partial_amount': partial_amount,
 
        'minimum_amount': 120 - partial_amount,
 
    }
 
    return render(request, "supporters/sustainers.html", context)
 

	
 

	
 
def sponsors(request):
 
    """Conservancy Sponsors Page view
 

	
 
    Performs object queries necessary to render the sponsors page.
 
    """
 
    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: str, amount: int, recurring: bool, base_url: str):
 
def create_checkout_session(reference_id, email: str, amount: int, recurring: str, 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,  # in cents
 
                        # https://docs.stripe.com/products-prices/pricing-models#variable-pricing
 
                        'recurring': {'interval': 'month'} if recurring else None,
 
                        'recurring': {'interval': recurring} if recurring else None,
 
                    },
 
                    'quantity': 1,
 
                },
 
            ],
 
            customer_email=email,
 
            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(commit=False)
 
            if form.data['recurring'] == 'monthly':
 
            order.recurring = form.data['recurring']
 
            if order.recurring == 'month':
 
                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, order.monthly_recurring, base_url)
 
            stripe_checkout_url = create_checkout_session(order.id, order.email, order.amount, order.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,
 
    # even concurrently, with the same session ID
 

	
 
    # TODO: Make sure fulfillment hasn't already been
 
    # peformed for this Checkout Session
 

	
 
    # Retrieve the Checkout Session from the API with line_items expanded
 
    checkout_session = stripe.checkout.Session.retrieve(
 
        session_id,
 
        expand=['line_items'],
 
    )
 

	
 
    # Check the Checkout Session's payment_status property
 
    # to determine if fulfillment should be peformed
 
    if checkout_session.payment_status != 'unpaid':
 
        # TODO: Perform fulfillment of the line items
 

	
 
        # TODO: Record/save fulfillment status for this
 
        # Checkout Session
 
        logger.info(f'Session ID {session_id} PAID!')
 
        try:
 
            order = SustainerOrder.objects.get(id=checkout_session['client_reference_id'], paid_time=None)
 
            order.paid_time=timezone.now()
 
            order.save()
 
            logger.info(f'Marked sustainer order {order.id} (order.email) as paid')
 
        except SustainerOrder.DoesNotExist:
 
            logger.info('No action')
 

	
 

	
 
def success(request):
 
    fulfill_checkout(request.GET['session_id'])
 
    return render(request, 'supporters/stripe_success.html', {})
 

	
 

	
 
def webhook(request):
 
    payload = request.body
 
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
0 comments (0 inline, 0 general)