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 | 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' | ||||||
|  |  | ||||||
|  | @ -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) |     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) | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 %} | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue