add /coinbase to handle Coinbase callback
This commit is contained in:
parent
fbe48b7b3e
commit
b00841f679
115
server.py
115
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)
|
||||
|
|
|
@ -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 %}
|
|
@ -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.
|
|
@ -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 %}
|
|
@ -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.
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue