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 | ||||
| 
 | ||||
| class SustainerForm(forms.ModelForm): | ||||
|     amount_monthly = forms.IntegerField(initial=12, required=False) | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = SustainerOrder | ||||
|         fields = [ | ||||
|  | @ -22,4 +24,6 @@ class SustainerForm(forms.ModelForm): | |||
|     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' | ||||
|  |  | |||
|  | @ -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) | ||||
|     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) | ||||
|  |  | |||
|  | @ -27,6 +27,9 @@ | |||
|    progress::-webkit-progress-value { | ||||
|      background: #224c57; | ||||
|    } | ||||
|    .btn:active { | ||||
|      transform: scale(1.05); | ||||
|    } | ||||
|   </style> | ||||
| {% endblock %} | ||||
| 
 | ||||
|  | @ -275,7 +278,7 @@ reach for reproducibility. </p> | |||
|       </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> | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,6 +6,11 @@ | |||
| {% 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 %} | ||||
|  | @ -17,8 +22,10 @@ | |||
|           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> | ||||
|  | @ -27,9 +34,16 @@ | |||
|       </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"> | ||||
|  | @ -63,7 +77,9 @@ | |||
|         </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 %} | ||||
|  |  | |||
|  | @ -45,7 +45,8 @@ def sponsors(request): | |||
|     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( | ||||
|  | @ -55,13 +56,15 @@ def create_checkout_session(reference_id, email, amount, base_url): | |||
|                     '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/', | ||||
|         ) | ||||
|  | @ -78,9 +81,13 @@ 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() | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue