From 8040c64723dd7909ce881950d89e74607eb1ea47 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 5 Jul 2023 01:25:36 -0400 Subject: [PATCH 1/9] email subscriber if a payment failed because of 3D secure --- subscribie/blueprints/checkout/__init__.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/subscribie/blueprints/checkout/__init__.py b/subscribie/blueprints/checkout/__init__.py index e443aa2a..50662d30 100644 --- a/subscribie/blueprints/checkout/__init__.py +++ b/subscribie/blueprints/checkout/__init__.py @@ -847,6 +847,35 @@ def stripe_webhook(): # Signal that a Stripe payment_intent.payment_failed event has been received, # noqa: E501 # so that receivers (such as notify Subscriber) are notified signal_payment_failed.send(stripe_event=eventObj) + except IndexError as e: + log.error(f"payment_intent.payment_failed reason is {e}") + eventObj = event["data"]["object"] + personName = eventObj["last_payment_error"]["payment_method"][ + "billing_details" + ]["name"] + personEmail = eventObj["last_payment_error"]["payment_method"][ + "billing_details" + ]["email"] + # Notify Subscriber if payment_failed event was related to a 3D secure failed payment # noqa: E501 + if ( + eventObj["last_payment_error"]["payment_method"]["card"][ + "three_d_secure_usage" + ]["supported"] + == True + ): + emailBody = f"""Hi {personName}, \n\n A recent subscription charge failed to be collected:\n\n + The failure code was: {eventObj["last_payment_error"]["code"]}\n\n + The failure message was: {eventObj["last_payment_error"]["message"]}\n\n + Please note, For extra fraud proction, Some banks enable 3D secure in their cards which requires the customer to complete an additional step before completing a transaction. Please try again with the correct code.""" # noqa: E501 + log.info(emailBody) + email = User.query.first().email + company = Company.query.first() + msg = EmailMessageQueue() + msg["Subject"] = company.name + " " + "Your subscription payment failed" + msg["FROM"] = current_app.config["EMAIL_LOGIN_FROM"] + msg["TO"] = personEmail + msg.set_content(emailBody) + msg.queue() except Exception as e: log.error(f"Unhandled error processing payment_intent.payment_failed: {e}") return "OK", 200 From cd46c3ab7cf73310592e758a59044673b5d43084 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Wed, 5 Jul 2023 22:53:35 +0100 Subject: [PATCH 2/9] wip #1194 introduce stub stripe_process_event_payment_intent_failed match statement needds python 3.10 --- subscribie/blueprints/checkout/__init__.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/subscribie/blueprints/checkout/__init__.py b/subscribie/blueprints/checkout/__init__.py index 50662d30..7377331d 100644 --- a/subscribie/blueprints/checkout/__init__.py +++ b/subscribie/blueprints/checkout/__init__.py @@ -801,6 +801,10 @@ def stripe_process_event_payment_intent_succeeded(event): return "OK", 200 +def stripe_process_event_payment_intent_failed(event): + pass + + @checkout.route("/stripe_webhook", methods=["POST"]) def stripe_webhook(): """Recieve stripe webhook from proxy (not directly from Stripe) @@ -942,11 +946,13 @@ def stripe_webhook(): ) return "OK", 200 - if ( - stripe_livemode == event["livemode"] - and event["type"] == "payment_intent.succeeded" - ): - return stripe_process_event_payment_intent_succeeded(event) + stripe_event_type = event["type"] + + match stripe_event_type: + case "payment_intent.succeeded": + return stripe_process_event_payment_intent_succeeded(event) + case "payment_intent.payment_failed": + return stripe_process_event_payment_intent_failed(event) msg = {"msg": "Unknown event", "event": event} log.debug(msg) From bb7098f07d868cb1ae93015a484cf1e560811ac0 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 6 Jul 2023 01:28:09 -0400 Subject: [PATCH 3/9] handling how failed payment itents will be insert in the database and displayed in the subscribers and transactions dashboard --- subscribie/blueprints/admin/__init__.py | 21 ++++++--- .../admin/templates/admin/subscribers.html | 18 ++++++++ .../admin/templates/admin/transactions.html | 2 +- subscribie/blueprints/checkout/__init__.py | 43 ++++++++++++++----- 4 files changed, 65 insertions(+), 19 deletions(-) diff --git a/subscribie/blueprints/admin/__init__.py b/subscribie/blueprints/admin/__init__.py index aa5534f3..8063a2bf 100644 --- a/subscribie/blueprints/admin/__init__.py +++ b/subscribie/blueprints/admin/__init__.py @@ -389,12 +389,20 @@ def refund_stripe_subscription(payment_id): ) if "confirm" in request.args and request.args["confirm"] == "1": try: - stripe_refund = stripe.Refund.create( - payment_intent=payment_id, - stripe_account=connect_account_id, - ) - if Transaction.query.filter_by(external_id=payment_id).first() is None: - return "payment doesn't exist" + if ( + Transaction.query.filter_by(external_id=payment_id).first() is None + or Transaction.query.filter_by(external_id=payment_id) + .first() + .payment_status + != "paid" + ): + flash("The transaction doesn't exist or wasn't succesful") + return redirect(url_for("admin.transactions")) + else: + stripe_refund = stripe.Refund.create( + payment_intent=payment_id, + stripe_account=connect_account_id, + ) transaction = Transaction.query.filter_by(external_id=payment_id).first() transaction.external_refund_id = stripe_refund.id database.session.commit() @@ -1398,7 +1406,6 @@ def transactions(): f"No transactions found. View all transactions" # noqa: E501 ) flash(msg) - return render_template( "admin/transactions.html", transactions=query.paginate(page=page, per_page=10), diff --git a/subscribie/blueprints/admin/templates/admin/subscribers.html b/subscribie/blueprints/admin/templates/admin/subscribers.html index 9953cc5e..22d52f88 100644 --- a/subscribie/blueprints/admin/templates/admin/subscribers.html +++ b/subscribie/blueprints/admin/templates/admin/subscribers.html @@ -239,6 +239,24 @@

Search...

+ {% elif transaction.payment_status != 'succeeded' %} +
+
    +
  • Title: + Failed Payment +
  • +
  • Transaction ID: {{ transaction.uuid }}
  • +
  • Date: {{ transaction.created_at.strftime('%Y-%m-%d') }}
  • +
  • + Price: + {{ transaction.showSellPrice() }} +
  • + +
  • Status: + {{ transaction.payment_status }} +
  • +
+
{% endif %} {% endfor %} {% else %} diff --git a/subscribie/blueprints/admin/templates/admin/transactions.html b/subscribie/blueprints/admin/templates/admin/transactions.html index 0be0b80e..f0285ad6 100644 --- a/subscribie/blueprints/admin/templates/admin/transactions.html +++ b/subscribie/blueprints/admin/templates/admin/transactions.html @@ -104,7 +104,7 @@

All Transactions ({{ transactions.total }})

- {% if transaction.external_refund_id is sameas None %} + {% if transaction.external_refund_id is sameas None and transaction.payment_status == 'paid' %} Refund {% endif %} diff --git a/subscribie/blueprints/checkout/__init__.py b/subscribie/blueprints/checkout/__init__.py index 7377331d..1c173fd4 100644 --- a/subscribie/blueprints/checkout/__init__.py +++ b/subscribie/blueprints/checkout/__init__.py @@ -801,8 +801,27 @@ def stripe_process_event_payment_intent_succeeded(event): return "OK", 200 -def stripe_process_event_payment_intent_failed(event): - pass +def stripe_process_event_payment_intent_failed(event, is_donation): + log.info("Processing payment_intent.failed") + data = event["data"]["object"] + transaction = Transaction() + transaction.currency = data["currency"] + transaction.amount = data["amount"] + person_email = data["last_payment_error"]["payment_method"]["billing_details"][ + "email" + ] + transaction.payment_status = ( + "paid" if data["status"] == "succeeded" else data["status"] + ) + transaction.external_id = data["id"] + transaction.external_src = "stripe" + transaction.person = Person.query.filter_by(email=person_email).one() + transaction.is_donation = is_donation + transaction.comment = data["last_payment_error"]["message"] + # commit the changes + database.session.add(transaction) + database.session.commit() + return "OK", 200 @checkout.route("/stripe_webhook", methods=["POST"]) @@ -817,7 +836,6 @@ def stripe_webhook(): """ event = request.json is_donation = False - stripe_livemode = PaymentProvider.query.first().stripe_livemode if stripe_livemode != event["livemode"]: log.warn( @@ -882,7 +900,7 @@ def stripe_webhook(): msg.queue() except Exception as e: log.error(f"Unhandled error processing payment_intent.payment_failed: {e}") - return "OK", 200 + return "OK", 200 # Handle the checkout.session.completed event if event["type"] == "checkout.session.completed": log.info("Processing checkout.session.completed event") @@ -945,14 +963,17 @@ def stripe_webhook(): stripe_external_id=session["id"], ) return "OK", 200 - stripe_event_type = event["type"] - - match stripe_event_type: - case "payment_intent.succeeded": - return stripe_process_event_payment_intent_succeeded(event) - case "payment_intent.payment_failed": - return stripe_process_event_payment_intent_failed(event) + if ( + stripe_event_type == "payment_intent.succeeded" + and stripe_livemode == event["livemode"] + ): + return stripe_process_event_payment_intent_succeeded(event) + elif ( + stripe_event_type == "payment_intent.payment_failed" + and stripe_livemode == event["livemode"] + ): + return stripe_process_event_payment_intent_failed(event, is_donation) msg = {"msg": "Unknown event", "event": event} log.debug(msg) From ddea360204d48dc19cc893d463d34d1ab8e9febf Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Thu, 6 Jul 2023 21:36:41 +0100 Subject: [PATCH 4/9] #1194 wip add test fail 3D secure payment --- ...criber_order_plan_with_failed_3D_secure.js | 70 +++++++++++++++++++ .../worker2.spec.js | 2 + 2 files changed, 72 insertions(+) create mode 100644 tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js diff --git a/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js b/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js new file mode 100644 index 00000000..622ac5f8 --- /dev/null +++ b/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js @@ -0,0 +1,70 @@ +const { test, expect } = require('@playwright/test'); +const SUBSCRIBER_EMAIL_USER = process.env.SUBSCRIBER_EMAIL_USER; + +test.describe("order plan with failed 3D secure payment:", () => { + test("@1194 @subscriber", async ({ page }) => { + console.log("Ordering plan with failed 3D secure payment..."); + // Buy item with subscription & upfront fee + // (but use failed payment card 4000003800000446) + await page.goto('/'); // Go to home before selecting product + await page.click('[name="One-Off Soaps"]'); + + // Fill in order form + await page.fill('#given_name', 'John'); + await page.fill('#family_name', 'Smith'); + await page.fill('#email', SUBSCRIBER_EMAIL_USER); + await page.fill('#mobile', '07123456789'); + await page.fill('#address_line_one', '123 Short Road'); + await page.fill('#city', 'London'); + await page.fill('#postcode', 'L01 T3U'); + expect(await page.screenshot()).toMatchSnapshot('upfront-new-customer-form.png'); + await page.click('text="Continue to Payment"'); + + // Begin stripe checkout + const order_summary_content = await page.textContent(".title-1"); + expect(order_summary_content === "Order Summary"); + expect(await page.screenshot()).toMatchSnapshot('upfront-pre-stripe-checkout.png'); + await page.click('#checkout-button'); + + //Verify first payment is correct (upfront charge + first recuring charge) + const first_payment_content = await page.textContent('#ProductSummary-totalAmount'); + expect(first_payment_content === "£5.66"); + const upfront_charge_content = await page.textContent('.Text-fontSize--16'); + expect(upfront_charge_content === "One-Off Soaps"); + + // Pay with test card + await page.fill('#cardNumber', '4000003800000446'); + await page.fill('#cardExpiry', '04 / 24'); + await page.fill('#cardCvc', '123'); + await page.fill('#billingName', 'John Smith'); + await page.selectOption('select#billingCountry', 'GB'); + await page.fill('#billingPostalCode', 'LN1 7FH'); + expect(await page.screenshot()).toMatchSnapshot('upfront-stripe-checkout-filled.png'); + await page.click('.SubmitButton'); + + // Fail the 3D secure payment on purpose + await page.getByRole('button', { name: 'Fail authentication' }).click(); + + // Go to My Subscribers page + // Crude wait before we check subscribers to allow webhooks time + await new Promise(x => setTimeout(x, 5000)); //5 seconds + await page.goto('/admin/subscribers') + expect(await page.screenshot()).toMatchSnapshot('upfront-view-subscribers.png'); + + // Click Refresh Subscription + await page.click('#refresh_subscriptions'); // this is the refresh subscription + await page.textContent('.alert-heading') === "Notification"; + // screeshot to the active subscriber + await page.goto('admin/dashboard'); + expect(await page.screenshot()).toMatchSnapshot('upfront-active-subscribers.png'); + // go back to subscriptions + await page.goto('/admin/subscribers') + // Verify that subscriber is present in the list + const subscriber_email_content = await page.textContent('.subscriber-email'); + expect(subscriber_email_content === SUBSCRIBER_EMAIL_USER); + + // Verify that plan is attached to subscriber + const subscriber_plan_title_content = await page.textContent('.subscription-title'); + expect(subscriber_plan_title_content === 'One-Off Soaps'); + }); +}); diff --git a/tests/browser-automated-tests-playwright/worker2.spec.js b/tests/browser-automated-tests-playwright/worker2.spec.js index 8455476f..61f8b71d 100644 --- a/tests/browser-automated-tests-playwright/worker2.spec.js +++ b/tests/browser-automated-tests-playwright/worker2.spec.js @@ -28,3 +28,5 @@ test.beforeEach(async ({ page }) => { const enabling_donations = require('./tests/1065_shop_owner_enabling_donations'); const checkout_donations = require('./tests/1065_subscriber_checkout_donation'); + + const order_plan_failed_3D_secure = require("./tests/1194_subscriber_order_plan_with_failed_3D_secure.js"); From de3c7e7a2820cc39cca3dbcdaa9f96e8ef101105 Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Thu, 6 Jul 2023 22:32:31 +0100 Subject: [PATCH 5/9] ref #1194 wip test 3Ds Fail authentication frame --- .../1194_subscriber_order_plan_with_failed_3D_secure.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js b/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js index 622ac5f8..66b14425 100644 --- a/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js +++ b/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js @@ -41,9 +41,12 @@ test.describe("order plan with failed 3D secure payment:", () => { await page.fill('#billingPostalCode', 'LN1 7FH'); expect(await page.screenshot()).toMatchSnapshot('upfront-stripe-checkout-filled.png'); await page.click('.SubmitButton'); - + + await page.waitForTimeout(5000); // Fail the 3D secure payment on purpose - await page.getByRole('button', { name: 'Fail authentication' }).click(); + await page.frame({ + name: 'acsFrame' + }).getByRole('button', { name: 'Fail authentication' }).click(); // Go to My Subscribers page // Crude wait before we check subscribers to allow webhooks time From b6cf3d1c9c550c1c2829c67e8874167d13cdb478 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 6 Jul 2023 23:16:16 -0400 Subject: [PATCH 6/9] checking for failed payment inside subscribers --- ...ubscriber_order_plan_with_failed_3D_secure.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js b/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js index 66b14425..b3beee48 100644 --- a/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js +++ b/tests/browser-automated-tests-playwright/tests/1194_subscriber_order_plan_with_failed_3D_secure.js @@ -54,20 +54,12 @@ test.describe("order plan with failed 3D secure payment:", () => { await page.goto('/admin/subscribers') expect(await page.screenshot()).toMatchSnapshot('upfront-view-subscribers.png'); - // Click Refresh Subscription - await page.click('#refresh_subscriptions'); // this is the refresh subscription - await page.textContent('.alert-heading') === "Notification"; - // screeshot to the active subscriber - await page.goto('admin/dashboard'); - expect(await page.screenshot()).toMatchSnapshot('upfront-active-subscribers.png'); - // go back to subscriptions - await page.goto('/admin/subscribers') // Verify that subscriber is present in the list - const subscriber_email_content = await page.textContent('.subscriber-email'); - expect(subscriber_email_content === SUBSCRIBER_EMAIL_USER); + const subscriber_email_content = await page.textContent('.failed-payment-title'); + expect(subscriber_email_content === "Failed Payment"); // Verify that plan is attached to subscriber - const subscriber_plan_title_content = await page.textContent('.subscription-title'); - expect(subscriber_plan_title_content === 'One-Off Soaps'); + const subscriber_plan_title_content = await page.textContent('.failed-payment-status'); + expect(subscriber_plan_title_content === 'requires_payment_method'); }); }); From 9e6650c8cde32cf9d6f413dc98537c32620bd9cd Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 6 Jul 2023 23:24:43 -0400 Subject: [PATCH 7/9] removing unused code --- subscribie/blueprints/checkout/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/subscribie/blueprints/checkout/__init__.py b/subscribie/blueprints/checkout/__init__.py index 1c173fd4..8d53c4b3 100644 --- a/subscribie/blueprints/checkout/__init__.py +++ b/subscribie/blueprints/checkout/__init__.py @@ -810,9 +810,7 @@ def stripe_process_event_payment_intent_failed(event, is_donation): person_email = data["last_payment_error"]["payment_method"]["billing_details"][ "email" ] - transaction.payment_status = ( - "paid" if data["status"] == "succeeded" else data["status"] - ) + transaction.payment_status = data["status"] transaction.external_id = data["id"] transaction.external_src = "stripe" transaction.person = Person.query.filter_by(email=person_email).one() From 5f7e2b8872a6fc50ef19e59a9dab1e03cff6df8f Mon Sep 17 00:00:00 2001 From: chrisjsimpson Date: Sun, 9 Jul 2023 22:50:04 +0100 Subject: [PATCH 8/9] wip #1194 handle events payment_intent.requires_action & invoice.payment_failed --- docs/content/en/docs/Architecture/testing.md | 8 ++--- subscribie/blueprints/checkout/__init__.py | 36 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/content/en/docs/Architecture/testing.md b/docs/content/en/docs/Architecture/testing.md index 4b4c6a41..f6006ec8 100644 --- a/docs/content/en/docs/Architecture/testing.md +++ b/docs/content/en/docs/Architecture/testing.md @@ -57,7 +57,7 @@ The test suite needs to listen to these events locally when running tests. tldr: 1. Install the Stripe cli -2. Run `stripe listen --events checkout.session.completed,payment_intent.succeeded,payment_intent.payment_failed,payment_intent.payment_failed --forward-to 127.0.0.1:5000/stripe_webhook` +2. Run `stripe listen --events checkout.session.completed,payment_intent.succeeded,payment_intent.payment_failed,payment_intent.payment_failed,payment_intent.requires_action,invoice.payment_failed --forward-to 127.0.0.1:5000/stripe_webhook` > For testing failed payments using [test cards table](https://stripe.com/docs/testing), the test card `4000000000000341` is especially useful because the cards in the previous table can’t be attached to a Customer object, but `4000000000000341` can be (and will fail which is useful for testing failed subscription payments such as `insufficient_funds`). @@ -69,7 +69,7 @@ If you're doing local development, then you need Stripe to send you the test pay 2. Login into stripe via `stripe login` (this shoud open the browser with stripe page where you should enter your credentials). If this command doesn't work use `stripe login -i` (this will login you in interactive mode where instead of opening browser you'll have to put Stripe secret key directly into terminal) 3. Run ``` - stripe listen --events checkout.session.completed,payment_intent.succeeded,payment_intent.payment_failed --forward-to 127.0.0.1:5000/stripe_webhook + stripe listen --events checkout.session.completed,payment_intent.succeeded,payment_intent.payment_failed,payment_intent.payment_failed,payment_intent.requires_action,invoice.payment_failed --forward-to 127.0.0.1:5000/stripe_webhook ``` You will see: ``` @@ -77,7 +77,7 @@ If you're doing local development, then you need Stripe to send you the test pay ``` 4. Please note, the stripe webhook secret is *not* needed for local development - for production, Stripe webhook verification is done in [Stripe-connect-webhook-endpoint-router](https://github.com/Subscribie/stripe-connect-webhook-endpoint-router) (you don't need this for local development). ``` - stripe listen --events checkout.session.completed,payment_intent.succeeded --forward-to 127.0.0.1:5000/stripe_webhook + stripe listen --events checkout.session.completed,payment_intent.succeeded,payment_intent.payment_failed,payment_intent.payment_failed,payment_intent.requires_action,invoice.payment_failed --forward-to 127.0.0.1:5000/stripe_webhook ``` Remember Stripe will give you a key valid for 90 days, if you get the following error you will need to do step 2 again: @@ -94,7 +94,7 @@ Error while authenticating with Stripe: Authorization failed, status=401 ## Run Playwright tests > **Important:** Stripe cli must be running locally to recieve payment events: ->`stripe listen --events checkout.session.completed,payment_intent.succeeded --forward-to 127.0.0.1:5000/stripe_webhook` +>`stripe listen --events checkout.session.completed,payment_intent.succeeded,payment_intent.payment_failed,payment_intent.payment_failed,payment_intent.requires_action,invoice.payment_failed --forward-to 127.0.0.1:5000/stripe_webhook`
diff --git a/subscribie/blueprints/checkout/__init__.py b/subscribie/blueprints/checkout/__init__.py index 8d53c4b3..d0d83af7 100644 --- a/subscribie/blueprints/checkout/__init__.py +++ b/subscribie/blueprints/checkout/__init__.py @@ -822,6 +822,37 @@ def stripe_process_event_payment_intent_failed(event, is_donation): return "OK", 200 +@backoff.on_exception(backoff.expo, Exception, max_tries=20) +def stripe_process_event_payment_intent_requires_action(event): + """Process three_d_secure_redirect required action condition""" + log.info("Processing event payment_intent.requires_action. Event details:\n{event}") + try: + if ( + event["data"]["object"]["next_action"]["type"] == "use_stripe_sdk" + and event["data"]["object"]["next_action"]["use_stripe_sdk"]["type"] + == "three_d_secure_redirect" + ): + log.info(f"A 3D secure payment didn't go through. three_d_secure_redirect. The event was:\n{event}") # noqa: E501 + else: + log.error( + f"Unknown next_action type: {event['data']['object']['next_action']['type']}" # noqa: E501 + ) # noqa: E501 + except Exception as e: + log.error( + f"Unknown error in stripe_process_event_payment_intent_requires_action: {e}" + ) # noqa: E501 + + +@backoff.on_exception(backoff.expo, Exception, max_tries=20) +def stripe_process_event_invoice_payment_failed(event): + breakpoint() + try: + hosted_invoice_url = event['data']['object']['hosted_invoice_url'] + log.info(f"The invoice.payment_failed hosted_invoice_url is {hosted_invoice_url}") # noqa: E501 + except Exception as e: + log.error(f"Unable to process event invoice.payment_failed unknown exception: {e}") # noqa: E501 + + @checkout.route("/stripe_webhook", methods=["POST"]) def stripe_webhook(): """Recieve stripe webhook from proxy (not directly from Stripe) @@ -841,6 +872,11 @@ def stripe_webhook(): ) log.info(f"Received stripe webhook event type {event['type']}") + if event["type"] == "payment_intent.requires_action": + stripe_process_event_payment_intent_requires_action(event) + if event["type"] == "invoice.payment_failed": + stripe_process_event_invoice_payment_failed(event) + # Handle the payment_intent.payment_failed if event["type"] == "payment_intent.payment_failed": log.info("Stripe webhook event: payment_intent.payment_failed") From cfa143ac3bd1cd73643b745ebd399adfd9292142 Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 11 Jul 2023 02:58:32 -0400 Subject: [PATCH 9/9] wip show subscribers macro creation --- .../admin/templates/admin/subscribers.html | 175 +++-------------- .../macros/subscribers-plan-macro.html | 183 ++++++++++++++++++ 2 files changed, 210 insertions(+), 148 deletions(-) create mode 100644 subscribie/blueprints/admin/templates/macros/subscribers-plan-macro.html diff --git a/subscribie/blueprints/admin/templates/admin/subscribers.html b/subscribie/blueprints/admin/templates/admin/subscribers.html index 22d52f88..6450f58d 100644 --- a/subscribie/blueprints/admin/templates/admin/subscribers.html +++ b/subscribie/blueprints/admin/templates/admin/subscribers.html @@ -1,4 +1,5 @@ {% extends "admin/layout.html" %} +{% import "macros/subscribers-plan-macro.html" as plan_template %} {% block title %} Subscribers {% endblock %} {% block body %} @@ -84,124 +85,23 @@

Search...

    • -
    • Title: - {{ subscription.plan.title }} -
    • -
    • - Interval: - {% if subscription.plan is not sameas None and subscription.plan.interval_unit is not sameas None %} - {{ subscription.plan.interval_unit.capitalize() }} - {% else %} - {{ subscription.plan.interval_unit }} - {% endif %} -
    • + {{ plan_template.subscriber_plan_title(subscription.title, "subscription_title") }} + {{ plan_template.subscriber_plan_interval(subscription.plan) }} {% if subscription.chosen_options %} -
    • -
      - Chosen Options -
        - {% for choice in subscription.chosen_options %} -
      • {{ choice.choice_group_title }}: {{ choice.option_title }}
      • - {% endfor %} -
      -
      -
    • + {{ plan_template.subscriber_plan_chosen_options(subscription.chosen_options) }} {% endif %} -
    • Subscription ID: {{ subscription.uuid }}
    • -
    • Date started: {{ subscription.created_at.strftime('%Y-%m-%d') }}
    • -
    • - {% if subscription.plan.requirements and subscription.plan.requirements.subscription %} - Price: - {{ subscription.showIntervalAmount() }} - {% else %} - (One-off. Not a subscription) - {% endif %} -
    • -
    • Sell price: - - {% if subscription.plan.requirements and subscription.plan.requirements.instant_payment %} - {{ subscription.showSellPrice() }}
    • - {% else %} - (No up-front fee) - {% endif %} - -
    • Status: - {% if subscription.plan.requirements and subscription.plan.requirements.subscription %} - {% if subscription.stripe_pause_collection == "void" %} - Paused - {% else %} - {{ subscription.stripe_status }} - {% endif %} - {% else %} - Paid - {% endif %} -
    • - {% if subscription.stripe_cancel_at %} - Automatically Cancels at: - {{ subscription.stripe_cancel_at | timestampToDate }} - {% endif %} -
    • -
    • -
    • - {% if subscription.plan.requirements and subscription.plan.requirements.note_to_seller_required %} -
      - Order Note - {% if subscription.note %} - {{ subscription.note.note }} - {% else %} - No note was given. - {% endif %} -
      - {% endif %} -
    • -
    • Actions: - {% if subscription.plan.requirements and subscription.plan.requirements.subscription %} - {% if subscription.stripe_status|lower in ['active', 'trialing', 'past_due', 'unpaid'] %} - {% if subscription.stripe_status|lower != 'trialing' and subscription.stripe_pause_collection != 'void' %} - - Pause - | - - Cancel - | - {% endif %} - {% endif %} - {% if subscription.stripe_pause_collection|lower == 'void' %} - - Resume - | - - Cancel - | - {% endif %} - {% endif %}
    • -
    • History: - View Transactions - -
    • -
    • Documents: - {% if subscription.documents|length == 0 %} - None - {% else %} -
        - {% for document in subscription.documents %} - {# Show documents assocated with subscription (if any) #} -
      • - {{ document.name }} | - {{ document.created_at.strftime('%Y-%m-%d') }}
      • - {% endfor %} -
      - {% endif %} -
    • + {{ plan_template.subscriber_plan_subscription_id("Subscription ID", subscription.uuid) }} + {{ plan_template.subscriber_plan_started_date(subscription.created_at) }} + {{ plan_template.subscriber_plan_interval_amount(subscription.plan.requirements, subscription) }} + {{ plan_template.subscriber_plan_sell_price("Sell price", "subscribers-plan-sell-price", subscription, transaction | default(null)) }} + {{ plan_template.subscriber_plan_status("subscription-status",subscription, transaction | default(null)) }} + {% if subscription.stripe_cancel_at %} + {{ plan_template.subscriber_plan_cancel_at(subscription.stripe_cancel_at) }} + {% endif %} + {{ plan_template.subscriber_plan_order_note(subscription.plan.requirements, subscription) }} + {{ plan_template.subscriber_plan_actions(subscription.plan.requirements, subscription) }} + {{ plan_template.subscriber_plan_history(subscription.person.uuid) }} + {{ plan_template.subscriber_plan_documents(subscription.documents) }}
  • @@ -214,47 +114,26 @@

    Search...

    {% if transaction.is_donation %}
      -
    • Title: - Donation -
    • -
    • Transaction ID: {{ transaction.uuid }}
    • -
    • Date: {{ transaction.created_at.strftime('%Y-%m-%d') }}
    • -
    • - Price: - {{ transaction.showSellPrice() }} -
    • - -
    • Status: - {{ transaction.payment_status }} -
    • -
    • - {% if transaction.comment %} -
      - Donation Note - {{ transaction.comment }} - {% else %} - No note was given. -
      - {% endif %} -
    • + {{ plan_template.subscriber_plan_title("Donation", "subscription_title") }} + {{ plan_template.subscriber_plan_subscription_id("Transaction ID", transaction.uuid) }} + {{ plan_template.subscriber_plan_started_date(transaction.created_at) }} + {{ plan_template.subscriber_plan_sell_price("Donation amount", "donation_amount", subscription | default(null), transaction) }} + {{ plan_template.subscriber_plan_status("transaction-status",subscription | default(null), transaction.payment_status) }} + {{ plan_template.subscriber_transaction_comment("Donation Note", transaction.comment) }}
    {% elif transaction.payment_status != 'succeeded' %}
      -
    • Title: - Failed Payment -
    • -
    • Transaction ID: {{ transaction.uuid }}
    • -
    • Date: {{ transaction.created_at.strftime('%Y-%m-%d') }}
    • + {{ plan_template.subscriber_plan_title("Failed Payment", "failed-payment-title") }} + {{ plan_template.subscriber_plan_subscription_id("Transaction ID", transaction.uuid) }} + {{ plan_template.subscriber_plan_started_date(transaction.created_at) }}
    • Price: {{ transaction.showSellPrice() }} + {{ plan_template.subscriber_plan_sell_price("Price", "failed-payment-amount", subscription | default(null), transaction) }}
    • - -
    • Status: - {{ transaction.payment_status }} -
    • + {{ plan_template.subscriber_plan_status("failed-payment-status",subscription | default(null), transaction.payment_status) }}
    {% endif %} diff --git a/subscribie/blueprints/admin/templates/macros/subscribers-plan-macro.html b/subscribie/blueprints/admin/templates/macros/subscribers-plan-macro.html new file mode 100644 index 00000000..b6d5cd67 --- /dev/null +++ b/subscribie/blueprints/admin/templates/macros/subscribers-plan-macro.html @@ -0,0 +1,183 @@ + {% macro subscriber_plan_title(title, class_name) -%} +
  • Title: + {{ title }} +
  • + {%- endmacro %} + + + {% macro subscriber_plan_interval(plan) -%} +
  • + Interval: + {% if plan is not sameas None and plan.interval_unit is not sameas None %} + {{ plan.interval_unit.capitalize() }} + {% else %} + {{ plan.interval_unit }} + {% endif %} +
  • + {%- endmacro %} + + + {% macro subsciber_plan_chosen_options(chosen_options) -%} +
  • +
    + Chosen Options +
      + {% for choice in chosen_options %} +
    • {{ choice.choice_group_title }}: {{ choice.option_title }}
    • + {% endfor %} +
    +
    +
  • + {%- endmacro %} + + + {% macro subscriber_plan_subscription_id(title, uuid) -%} +
  • {{ title }}: {{ uuid }}
  • + {%- endmacro %} + + {% macro subscriber_plan_started_date(created_at) -%} +
  • Date started: {{ created_at.strftime('%Y-%m-%d') }}
  • + {%- endmacro %} + + {% macro subscriber_plan_interval_amount(plan_requirements, subscription) -%} +
  • Price: + + {% if plan_requirements and plan_requirements.subscription %} + {{ subscription.showIntervalAmount() }} + {% else %} + (One-off. Not a subscription) + {% endif %} + +
  • + {%- endmacro %} + + {% macro subscriber_plan_sell_price(title, class_name, subscription, transaction) -%} +
  • {{ title }}: + + {% if subscription.plan.requirements and subscription.plan.requirements.instant_payment %} + {{ subscription.showSellPrice() }} + {% elif transaction %} + {{ transaction.showSellPrice() }} + {% else %} + (No up-front fee) + {% endif %} + +
  • + {%- endmacro %} + + + {% macro subscriber_plan_status(class_name, subscription, transaction) -%} +
  • Status: + {% if subscription.plan.requirements and subscription.plan.requirements.subscription %} + {% if subscription.stripe_pause_collection == "void" %} + Paused + {% else %} + {{ subscription.stripe_status }} + {% endif %} + {% elif transaction %} + transaction.status + {% else %} + Paid + {% endif %} +
  • + {%- endmacro %} + + + + {% macro subscriber_plan_cancel_at(cancel_at) -%} +
  • + Automatically Cancels at: + {{ cancel_at | timestampToDate }} +
  • + {%- endmacro %} + + + {% macro subscriber_plan_order_note(plan_requirements, subscription) -%} +
  • + {% if plan_requirements and plan_requirements.note_to_seller_required %} +
    + Order Note + {% if subscription.note %} + {{ subscription.note.note }} + {% else %} + No note was given. + {% endif %} +
    + {% endif %} +
  • + {%- endmacro %} + + + {% macro subscriber_plan_actions(plan_requirements, subscription) -%} +
  • Actions: + {% if plan_requirements and plan_requirements.subscription %} + {% if subscription.stripe_status|lower in ['active', 'trialing', 'past_due', 'unpaid'] %} + {% if subscription.stripe_status|lower != 'trialing' and subscription.stripe_pause_collection != 'void' %} + + Pause + | + + Cancel + | + {% endif %} + {% endif %} + {% if subscription.stripe_pause_collection|lower == 'void' %} + + Resume + | + + Cancel + | + {% endif %} + {% endif %}
  • + {%- endmacro %} + + + {% macro subscriber_plan_history(person_uuid)-%} +
  • History: + View Transactions + +
  • + {%- endmacro %} + + + {% macro subscriber_plan_documents(documents)-%} +
  • Documents: + {% if documents|length == 0 %} + None + {% else %} +
      + {% for document in documents %} + {# Show documents assocated with subscription (if any) #} +
    • + {{ document.name }} | + {{ document.created_at.strftime('%Y-%m-%d') }}
    • + {% endfor %} +
    + {% endif %} +
  • + {%- endmacro %} + + + {% macro subscriber_transaction_comment(title, comment) -%} +
  • + {% if comment %} +
    + {{ title }} + {{ comment }} + {% else %} + No note was given. +
    + {% endif %} +
  • + + {%- endmacro %}