From b00841f679d9513098cc18f04d6b5fa68b9763f1 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Sun, 13 Dec 2020 19:16:32 +0100 Subject: [PATCH] add /coinbase to handle Coinbase callback --- server.py | 115 ++++++++++++++++-- .../coinbase/extend-subscription.html | 23 ++++ .../coinbase/extend-subscription.txt | 11 ++ .../coinbase/new-subscription.html | 22 ++++ .../coinbase/new-subscription.txt | 11 ++ tests/test_server.py | 58 +++++++++ 6 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 templates/emails/transactional/coinbase/extend-subscription.html create mode 100644 templates/emails/transactional/coinbase/extend-subscription.txt create mode 100644 templates/emails/transactional/coinbase/new-subscription.html create mode 100644 templates/emails/transactional/coinbase/new-subscription.txt diff --git a/server.py b/server.py index 1b4c039c..0669f882 100644 --- a/server.py +++ b/server.py @@ -6,6 +6,8 @@ from datetime import timedelta import arrow import flask_profiler import sentry_sdk +from coinbase_commerce.error import WebhookInvalidPayload, SignatureVerificationError +from coinbase_commerce.webhook import Webhook from flask import ( Flask, redirect, @@ -53,6 +55,7 @@ from app.config import ( PADDLE_MONTHLY_PRODUCT_IDS, PADDLE_YEARLY_PRODUCT_IDS, PGP_SIGNER, + COINBASE_WEBHOOK_SECRET, ) from app.dashboard.base import dashboard_bp from app.developer.base import developer_bp @@ -77,6 +80,7 @@ from app.models import ( Referral, AliasMailbox, Notification, + CoinbaseSubscription, ) from app.monitor.base import monitor_bp from app.oauth.base import oauth_bp @@ -142,6 +146,7 @@ def create_app() -> Flask: init_admin(app) setup_paddle_callback(app) + setup_coinbase_commerce(app) setup_do_not_track(app) if FLASK_PROFILER_PATH: @@ -198,20 +203,23 @@ def fake_data(): user.trial_end = None - LifetimeCoupon.create(code="coupon", nb_used=10) - db.session.commit() + LifetimeCoupon.create(code="coupon", nb_used=10, commit=True) # Create a subscription for user - Subscription.create( - user_id=user.id, - cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234", - update_url="https://checkout.paddle.com/subscription/update?user=1234", - subscription_id="123", - event_time=arrow.now(), - next_bill_date=arrow.now().shift(days=10).date(), - plan=PlanEnum.monthly, + # Subscription.create( + # user_id=user.id, + # cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234", + # update_url="https://checkout.paddle.com/subscription/update?user=1234", + # subscription_id="123", + # event_time=arrow.now(), + # next_bill_date=arrow.now().shift(days=10).date(), + # plan=PlanEnum.monthly, + # ) + # db.session.commit() + + CoinbaseSubscription.create( + user_id=user.id, end_at=arrow.now().shift(days=10), commit=True ) - db.session.commit() api_key = ApiKey.create(user_id=user.id, name="Chrome") api_key.code = "code" @@ -634,6 +642,91 @@ def setup_paddle_callback(app: Flask): return "OK" +def setup_coinbase_commerce(app): + @app.route("/coinbase", methods=["POST"]) + def coinbase_webhook(): + # event payload + request_data = request.data.decode("utf-8") + # webhook signature + request_sig = request.headers.get("X-CC-Webhook-Signature", None) + + try: + # signature verification and event object construction + event = Webhook.construct_event( + request_data, request_sig, COINBASE_WEBHOOK_SECRET + ) + except (WebhookInvalidPayload, SignatureVerificationError) as e: + LOG.exception("Invalid Coinbase webhook") + return str(e), 400 + + LOG.d("Coinbase event %s", event) + + if event["type"] == "charge:confirmed": + if handle_coinbase_event(event): + return "success", 200 + else: + return "error", 400 + + return "success", 200 + + +def handle_coinbase_event(event) -> bool: + user_id = int(event["data"]["metadata"]["custom"]) + code = event["data"]["code"] + user = User.get(user_id) + if not user: + LOG.exception("User not found %s", user_id) + return False + + coinbase_subscription: CoinbaseSubscription = CoinbaseSubscription.get_by( + user_id=user_id + ) + + if not coinbase_subscription: + LOG.d("Create a coinbase subscription for %s", user) + coinbase_subscription = CoinbaseSubscription.create( + user_id=user_id, end_at=arrow.now().shift(years=1), code=code, commit=True + ) + send_email( + user.email, + "Your SimpleLogin account has been upgraded", + render( + "transactional/coinbase/new-subscription.txt", + coinbase_subscription=coinbase_subscription, + ), + render( + "transactional/coinbase/new-subscription.html", + coinbase_subscription=coinbase_subscription, + ), + ) + else: + if coinbase_subscription.code != code: + LOG.d("Update code from %s to %s", coinbase_subscription.code, code) + coinbase_subscription.code = code + + if coinbase_subscription.is_active(): + coinbase_subscription.end_at = coinbase_subscription.end_at.shift(years=1) + else: # already expired subscription + coinbase_subscription.end_at = arrow.now().shift(years=1) + + db.session.commit() + + send_email( + user.email, + "Your SimpleLogin account has been extended", + render( + "transactional/coinbase/extend-subscription.txt", + coinbase_subscription=coinbase_subscription, + ), + render( + "transactional/coinbase/extend-subscription.html", + coinbase_subscription=coinbase_subscription, + ), + ) + + return True + + def init_extensions(app: Flask): login_manager.init_app(app) db.init_app(app) diff --git a/templates/emails/transactional/coinbase/extend-subscription.html b/templates/emails/transactional/coinbase/extend-subscription.html new file mode 100644 index 00000000..7f593b6e --- /dev/null +++ b/templates/emails/transactional/coinbase/extend-subscription.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block content %} + {% call text() %} +

+ Your subscription has been extended! +

+ {% endcall %} + + {% call text() %} + Your payment with cryptocurrency has been successfully processed.
+ Your subscription has been extended to + {{ coinbase_subscription.end_at.format("YYYY-MM-DD") }} + {% endcall %} + + + {% call text() %} + Thank you a lot for your support! + {% endcall %} + + + {{ render_text('Best,
SimpleLogin Team.') }} +{% endblock %} diff --git a/templates/emails/transactional/coinbase/extend-subscription.txt b/templates/emails/transactional/coinbase/extend-subscription.txt new file mode 100644 index 00000000..c1739dae --- /dev/null +++ b/templates/emails/transactional/coinbase/extend-subscription.txt @@ -0,0 +1,11 @@ +Your subscription has been extended! + +Your payment with cryptocurrency has been successfully processed. + +Your subscription has been extended to +{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }} + +Thank you a lot for your support! + +Best, +SimpleLogin team. diff --git a/templates/emails/transactional/coinbase/new-subscription.html b/templates/emails/transactional/coinbase/new-subscription.html new file mode 100644 index 00000000..e34dbd6f --- /dev/null +++ b/templates/emails/transactional/coinbase/new-subscription.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block content %} + {% call text() %} +

+ Your account has been upgraded! +

+ {% endcall %} + + {% call text() %} + Your payment with cryptocurrency has been successfully processed.
+ Your account has been upgraded to the premium plan until + {{ coinbase_subscription.end_at.format("YYYY-MM-DD") }} + {% endcall %} + + {% call text() %} + Thank you a lot for your support! + {% endcall %} + + + {{ render_text('Best,
SimpleLogin Team.') }} +{% endblock %} diff --git a/templates/emails/transactional/coinbase/new-subscription.txt b/templates/emails/transactional/coinbase/new-subscription.txt new file mode 100644 index 00000000..613ad5c0 --- /dev/null +++ b/templates/emails/transactional/coinbase/new-subscription.txt @@ -0,0 +1,11 @@ +Your account has been upgraded! + +Your payment with cryptocurrency has been successfully processed. + +Your account has been upgraded to premium plan until +{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }} + +Thank you a lot for your support! + +Best, +SimpleLogin team. diff --git a/tests/test_server.py b/tests/test_server.py index 30bcc8e4..46ceaac8 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,6 +1,64 @@ +import arrow + +from app.extensions import db +from app.models import User, CoinbaseSubscription +from server import handle_coinbase_event + + def test_redirect_login_page(flask_client): """Start with a blank database.""" rv = flask_client.get("/") assert rv.status_code == 302 assert rv.location == "http://sl.test/auth/login" + + +def test_coinbase_webhook(flask_client): + r = flask_client.post("/coinbase") + assert r.status_code == 400 + + +def test_handle_coinbase_event_new_subscription(flask_client): + user = User.create( + email="a@b.c", + password="password", + name="Test User", + activated=True, + commit=True, + ) + handle_coinbase_event( + {"data": {"code": "AAAAAA", "metadata": {"custom": str(user.id)}}} + ) + + assert user.is_paid() + assert user.is_premium() + + assert CoinbaseSubscription.get_by(user_id=user.id) is not None + + +def test_handle_coinbase_event_extend_subscription(flask_client): + user = User.create( + email="a@b.c", + password="password", + name="Test User", + activated=True, + ) + user.trial_end = None + db.session.commit() + + cb = CoinbaseSubscription.create( + user_id=user.id, end_at=arrow.now().shift(days=-400), commit=True + ) + assert not cb.is_active() + + assert not user.is_paid() + assert not user.is_premium() + + handle_coinbase_event( + {"data": {"code": "AAAAAA", "metadata": {"custom": str(user.id)}}} + ) + + assert user.is_paid() + assert user.is_premium() + + assert CoinbaseSubscription.get_by(user_id=user.id) is not None