Changeset - a74244efb44e
[Not reviewed]
0 5 3
Ben Sturmfels (bsturmfels) - 10 days ago 2024-10-08 15:01:32
ben@sturm.com.au
Record Stripe payment intent, export to CSV
8 files changed with 85 insertions and 6 deletions:
0 comments (0 inline, 0 general)
conservancy/settings/dev.py
Show inline comments
 
import os
 

	
 
from .base import *  # NOQA
 

	
 
DEBUG = True
 
ALLOWED_HOSTS = ['*']
 

	
 
DATABASES = {
 
    'default': {
 
        'NAME': 'conservancy-website.sqlite3',
 
        'ENGINE': 'django.db.backends.sqlite3',
 
    }
 
}
 

	
 
SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
 

	
 
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
 

	
 
STRIPE_API_KEY = os.getenv('STRIPE_API_KEY', '')
 
STRIPE_ENDPOINT_SECRET = os.getenv('STRIPE_ENDPOINT_SECRET', '')
conservancy/settings/prod.py
Show inline comments
...
 
@@ -16,24 +16,27 @@ MANAGERS = [
 
    ('Bradley M. Kuhn', 'sysadmin@sfconservancy.org'),
 
]
 

	
 
DATABASES = {
 
    'default': {
 
        'NAME': '/var/lib/www/database/conservancy-website.sqlite3',
 
        'ENGINE': 'django.db.backends.sqlite3',
 
    }
 
}
 

	
 
# Apache/mod_wsgi doesn't make it straightforward to pass environment variables
 
# to Django (can't use the Apache config).
 
with open(BASE_DIR.parent / 'secrets.json') as f:  # NOQA
 
    secrets = json.load(f)
 

	
 
def get_secret(secrets, setting):
 
    try:
 
        return secrets[setting]
 
    except KeyError:
 
        raise ImproperlyConfigured(f'Missing secret \'{setting}\'')
 

	
 
SECRET_KEY = get_secret(secrets, 'SECRET_KEY')
 

	
 
SESSION_COOKIE_SECURE = True
 

	
 
STRIPE_API_KEY = get_secret(secrets, 'STRIPE_API_KEY')
 
STRIPE_ENDPOINT_SECRET = get_secret(secrets, 'STRIPE_ENDPOINT_SECRET')
conservancy/supporters/admin.py
Show inline comments
 
from django.contrib import admin
 

	
 
from .models import Supporter, SustainerOrder
 

	
 

	
 
@admin.register(Supporter)
 
class SupporterAdmin(admin.ModelAdmin):
 
    list_display = ('display_name', 'display_until_date')
 

	
 

	
 
@admin.register(SustainerOrder)
 
class SustainerOrderAdmin(admin.ModelAdmin):
 
    fields = [
 
        'created_time',
 
        'paid_time',
 
        'payment_method',
 
        'payment_id',
 
        'name',
 
        'email',
 
        'amount',
 
        'acknowledge_publicly',
 
        'add_to_mailing_list',
 
        'tshirt_size',
 
        'street',
 
        'city',
 
        'state',
 
        'zip_code',
 
        'country',
 
    ]
 

	
 
    readonly_fields = ['created_time', 'paid_time']
 
    readonly_fields = ['created_time', 'paid_time', 'payment_method', 'payment_id']
 
    list_display = ['created_time', 'name', 'email', 'amount', 'paid']
 
    list_filter = ['paid_time']
conservancy/supporters/management/commands/__init__.py
Show inline comments
 
new file 100644
conservancy/supporters/management/commands/export_stripe.py
Show inline comments
 
new file 100644
 
import csv
 
import sys
 

	
 
from django.core.management.base import BaseCommand
 
from ...models import SustainerOrder
 

	
 

	
 
class Command(BaseCommand):
 
    help = "Closes the specified poll for voting"
 

	
 
    def handle(self, *args, **options):
 
        orders = SustainerOrder.objects.filter(paid_time__isnull=False)
 
        columns = ['time', 'name', 'email', 'amount', 'transaction_id', 'public_ack', 'shirt_size', 'join_list', 'street', 'city', 'state', 'zip_code', 'country']
 
        writer = csv.writer(sys.stdout)
 
        writer.writerow(columns)
 
        for order in orders:
 
            writer.writerow([
 
                order.created_time,
 
                order.name,
 
                order.email,
 
                order.amount,
 
                order.payment_id,
 
                order.acknowledge_publicly,
 
                repr(order.tshirt_size if order.tshirt_size else ''),
 
                order.add_to_mailing_list,
 
                order.street,
 
                order.city,
 
                order.state,
 
                order.zip_code,
 
                order.country,
 
            ])
conservancy/supporters/migrations/0004_sustainerorder_payment_id_and_more.py
Show inline comments
 
new file 100644
 
# Generated by Django 4.2.11 on 2024-10-08 09:44
 

	
 
from django.db import migrations, models
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('supporters', '0003_remove_sustainerorder_monthly_recurring_and_more'),
 
    ]
 

	
 
    operations = [
 
        migrations.AddField(
 
            model_name='sustainerorder',
 
            name='payment_id',
 
            field=models.CharField(blank=True, max_length=255),
 
        ),
 
        migrations.AddField(
 
            model_name='sustainerorder',
 
            name='payment_method',
 
            field=models.CharField(default='Stripe', max_length=10),
 
        ),
 
    ]
conservancy/supporters/models.py
Show inline comments
...
 
@@ -6,81 +6,83 @@ 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"),),
 
            (("", "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),
 
        ])
 
    recurring = models.CharField(max_length=10)
 
    payment_method = models.CharField(max_length=10, default='Stripe')
 
    payment_id = models.CharField(max_length=255, blank=True)
 
    paid_time = models.DateTimeField(null=True, blank=True)
 
    acknowledge_publicly = models.BooleanField(default=True)
 
    add_to_mailing_list = models.BooleanField(default=True)
 
    tshirt_size = models.CharField(max_length=50, choices=TSHIRT_CHOICES)
 
    tshirt_size = models.CharField(max_length=50, choices=TSHIRT_CHOICES, blank=True)
 
    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/views.py
Show inline comments
 
from datetime import datetime
 
import logging
 

	
 
from django.conf import settings
 
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: str, base_url: str):
 
    # https://docs.stripe.com/payments/accept-a-payment
 
    # https://docs.stripe.com/api/checkout/sessions
 
    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': 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)
 
            order.recurring = form.data['recurring']
 
            if order.recurring == 'month':
 
                order.amount = form.cleaned_data['amount_monthly']
 
            order.save()
 
            base_url = f'{request.scheme}://{request.get_host()}'
 
            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'
 
stripe.api_key = settings.STRIPE_API_KEY
 
if stripe.api_key == '':
 
    logger.warning('Missing STRIPE_API_KEY')
 

	
 

	
 
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'],
 
        expand=['line_items', 'invoice'],
 
    )
 

	
 
    # 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()
 
            if checkout_session['payment_intent']:
 
                # Payments get a payment intent directly
 
                order.payment_id = checkout_session['payment_intent']
 
            else:
 
                # Subscriptions go get a payment intent generated on the invoice
 
                order.payment_id = checkout_session['invoice']['payment_intent']
 
            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']
 
    event = None
 

	
 
    # From webhook dashboard
 
    endpoint_secret = 'whsec_lLy9pqxAAHdl4fwiC0cFg1KwR6y4CvOH'
 
    endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
 
    if endpoint_secret == '':
 
        logger.warning('Missing STRIPE_ENDPOINT_SECRET')
 

	
 
    try:
 
        event = stripe.Webhook.construct_event(
 
            payload, sig_header, endpoint_secret
 
        )
 
    except ValueError:
 
        # Invalid payload
 
        return HttpResponse(status=400)
 
    except stripe.error.SignatureVerificationError:
 
        # Invalid signature
 
        return HttpResponse(status=400)
 

	
 
    if (
 
            event['type'] == 'checkout.session.completed'
 
            or event['type'] == 'checkout.session.async_payment_succeeded'
 
    ):
 
        fulfill_checkout(event['data']['object']['id'])
 

	
 
    return HttpResponse(status=200)
0 comments (0 inline, 0 general)