From 6df3227d63c8f6e040b954a6bca3aa55ad58b0da Mon Sep 17 00:00:00 2001 From: Deimos Date: Tue, 24 Sep 2019 19:00:04 -0600 Subject: [PATCH] Add recurring Stripe donations This uses Stripe's Subscriptions capability to set up recurring donations. Requires setting up a Product for the recurring donation, and defining its product ID in the INI file. --- tildes/production.ini.example | 2 + tildes/scss/modules/_form.scss | 10 ++++ tildes/tildes/templates/donate_stripe.jinja2 | 16 +++++ tildes/tildes/views/donate.py | 63 +++++++++++++++----- 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/tildes/production.ini.example b/tildes/production.ini.example index e81fd6c..b4e5923 100644 --- a/tildes/production.ini.example +++ b/tildes/production.ini.example @@ -23,6 +23,8 @@ redis.sessions.timeout = 3600 sqlalchemy.url = postgresql+psycopg2://tildes:@:6432/tildes +stripe.recurring_donation_product_id = prod_ProductID + tildes.default_user_comment_label_weight = 1.0 tildes.welcome_message_sender = Deimos diff --git a/tildes/scss/modules/_form.scss b/tildes/scss/modules/_form.scss index 2ec4e46..345b00b 100644 --- a/tildes/scss/modules/_form.scss +++ b/tildes/scss/modules/_form.scss @@ -1,6 +1,12 @@ // Copyright (c) 2018 Tildes contributors // SPDX-License-Identifier: AGPL-3.0-or-later +.form-group { + .form-radio { + margin-left: 1rem; + } +} + .form-narrow { max-width: 20rem; @@ -97,6 +103,10 @@ textarea.form-input { } } +.form-radio { + display: block; +} + .form-search .form-input { margin-right: 0.4rem; } diff --git a/tildes/tildes/templates/donate_stripe.jinja2 b/tildes/tildes/templates/donate_stripe.jinja2 index 0557b81..220f78e 100644 --- a/tildes/tildes/templates/donate_stripe.jinja2 +++ b/tildes/tildes/templates/donate_stripe.jinja2 @@ -17,6 +17,22 @@
+
+ + + + +
+
diff --git a/tildes/tildes/views/donate.py b/tildes/tildes/views/donate.py index f4c659a..7b525fe 100644 --- a/tildes/tildes/views/donate.py +++ b/tildes/tildes/views/donate.py @@ -37,32 +37,65 @@ def get_donate_stripe(request: Request) -> dict: { "amount": Float(required=True, validate=Range(min=1.0)), "currency": String(required=True, validate=OneOf(("CAD", "USD"))), + "interval": String(required=True, validate=OneOf(("onetime", "month", "year"))), } ) -def post_donate_stripe(request: Request, amount: int, currency: str) -> dict: +def post_donate_stripe( + request: Request, amount: int, currency: str, interval: str +) -> dict: """Process a Stripe donation.""" try: stripe.api_key = request.registry.settings["api_keys.stripe.secret"] publishable_key = request.registry.settings["api_keys.stripe.publishable"] + product_id = request.registry.settings["stripe.recurring_donation_product_id"] except KeyError: raise HTTPInternalServerError incr_counter("donation_initiations", type="stripe") - session = stripe.checkout.Session.create( - payment_method_types=["card"], - line_items=[ - { - "name": "One-time donation - tildes.net", - "amount": int(amount * 100), - "currency": currency, - "quantity": 1, - } - ], - submit_type="donate", - success_url="https://tildes.net/donate_success", - cancel_url="https://docs.tildes.net/donate", - ) + if interval == "onetime": + session = stripe.checkout.Session.create( + payment_method_types=["card"], + line_items=[ + { + "name": "One-time donation - tildes.net", + "amount": int(amount * 100), + "currency": currency, + "quantity": 1, + } + ], + submit_type="donate", + success_url="https://tildes.net/donate_success", + cancel_url="https://docs.tildes.net/donate", + ) + else: + product = stripe.Product.retrieve(product_id) + existing_plans = stripe.Plan.list(product=product, active=True, limit=100) + + # look through existing plans to see if there's already a matching one, or + # create a new plan if not + for existing_plan in existing_plans: + if ( + existing_plan.amount == int(amount * 100) + and existing_plan.currency == currency.lower() + and existing_plan.interval == interval + ): + plan = existing_plan + break + else: + plan = stripe.Plan.create( + amount=int(amount * 100), + currency=currency, + interval=interval, + product=product, + ) + + session = stripe.checkout.Session.create( + payment_method_types=["card"], + subscription_data={"items": [{"plan": plan.id}]}, + success_url="https://tildes.net/donate_success", + cancel_url="https://docs.tildes.net/donate", + ) return {"publishable_key": publishable_key, "session_id": session.id}