add /coinbase to handle Coinbase callback

This commit is contained in:
Son NK 2020-12-13 19:16:32 +01:00
parent fbe48b7b3e
commit b00841f679
6 changed files with 229 additions and 11 deletions

115
server.py
View File

@ -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)

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block content %}
{% call text() %}
<h1>
Your subscription has been extended!
</h1>
{% endcall %}
{% call text() %}
Your payment with cryptocurrency has been successfully processed. <br>
Your subscription has been extended to
<b>{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}</b>
{% endcall %}
{% call text() %}
Thank you a lot for your support!
{% endcall %}
{{ render_text('Best, <br />SimpleLogin Team.') }}
{% endblock %}

View File

@ -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.

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block content %}
{% call text() %}
<h1>
Your account has been upgraded!
</h1>
{% endcall %}
{% call text() %}
Your payment with cryptocurrency has been successfully processed. <br>
Your account has been upgraded to the premium plan until
<b>{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}</b>
{% endcall %}
{% call text() %}
Thank you a lot for your support!
{% endcall %}
{{ render_text('Best, <br />SimpleLogin Team.') }}
{% endblock %}

View File

@ -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.

View File

@ -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