import arrow import json from dateutil.relativedelta import relativedelta from flask import Flask, request from app import paddle_utils, paddle_callback from app.config import ( PADDLE_MONTHLY_PRODUCT_ID, PADDLE_MONTHLY_PRODUCT_IDS, PADDLE_YEARLY_PRODUCT_IDS, PADDLE_COUPON_ID, ) from app.db import Session from app.email_utils import send_email, render from app.log import LOG from app.models import Subscription, PlanEnum, User, Coupon from app.subscription_webhook import execute_subscription_webhook from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction from app.utils import random_string def setup_paddle_callback(app: Flask): @app.route("/paddle", methods=["GET", "POST"]) def paddle(): LOG.d(f"paddle callback {request.form.get('alert_name')} {request.form}") # make sure the request comes from Paddle if not paddle_utils.verify_incoming_request(dict(request.form)): LOG.e("request not coming from paddle. Request data:%s", dict(request.form)) return "KO", 400 if ( request.form.get("alert_name") == "subscription_created" ): # new user subscribes # the passthrough is json encoded, e.g. # request.form.get("passthrough") = '{"user_id": 88 }' passthrough = json.loads(request.form.get("passthrough")) user_id = passthrough.get("user_id") user = User.get(user_id) subscription_plan_id = int(request.form.get("subscription_plan_id")) if subscription_plan_id in PADDLE_MONTHLY_PRODUCT_IDS: plan = PlanEnum.monthly elif subscription_plan_id in PADDLE_YEARLY_PRODUCT_IDS: plan = PlanEnum.yearly else: LOG.e( "Unknown subscription_plan_id %s %s", subscription_plan_id, request.form, ) return "No such subscription", 400 sub = Subscription.get_by(user_id=user.id) if not sub: LOG.d(f"create a new Subscription for user {user}") Subscription.create( user_id=user.id, cancel_url=request.form.get("cancel_url"), update_url=request.form.get("update_url"), subscription_id=request.form.get("subscription_id"), event_time=arrow.now(), next_bill_date=arrow.get( request.form.get("next_bill_date"), "YYYY-MM-DD" ).date(), plan=plan, ) emit_user_audit_log( user=user, action=UserAuditLogAction.Upgrade, message="Upgraded through Paddle", ) else: LOG.d(f"Update an existing Subscription for user {user}") sub.cancel_url = request.form.get("cancel_url") sub.update_url = request.form.get("update_url") sub.subscription_id = request.form.get("subscription_id") sub.event_time = arrow.now() sub.next_bill_date = arrow.get( request.form.get("next_bill_date"), "YYYY-MM-DD" ).date() sub.plan = plan # make sure to set the new plan as not-cancelled # in case user cancels a plan and subscribes a new plan sub.cancelled = False emit_user_audit_log( user=user, action=UserAuditLogAction.SubscriptionExtended, message="Extended Paddle subscription", ) execute_subscription_webhook(user) LOG.d("User %s upgrades!", user) Session.commit() elif request.form.get("alert_name") == "subscription_payment_succeeded": subscription_id = request.form.get("subscription_id") LOG.d("Update subscription %s", subscription_id) sub: Subscription = Subscription.get_by(subscription_id=subscription_id) # when user subscribes, the "subscription_payment_succeeded" can arrive BEFORE "subscription_created" # at that time, subscription object does not exist yet if sub: sub.event_time = arrow.now() sub.next_bill_date = arrow.get( request.form.get("next_bill_date"), "YYYY-MM-DD" ).date() Session.commit() execute_subscription_webhook(sub.user) elif request.form.get("alert_name") == "subscription_cancelled": subscription_id = request.form.get("subscription_id") sub: Subscription = Subscription.get_by(subscription_id=subscription_id) if sub: # cancellation_effective_date should be the same as next_bill_date LOG.w( "Cancel subscription %s %s on %s, next bill date %s", subscription_id, sub.user, request.form.get("cancellation_effective_date"), sub.next_bill_date, ) sub.event_time = arrow.now() sub.cancelled = True emit_user_audit_log( user=sub.user, action=UserAuditLogAction.SubscriptionCancelled, message="Cancelled Paddle subscription", ) Session.commit() user = sub.user send_email( user.email, "SimpleLogin - your subscription is canceled", render( "transactional/subscription-cancel.txt", user=user, end_date=request.form.get("cancellation_effective_date"), ), ) execute_subscription_webhook(sub.user) else: # user might have deleted their account LOG.i(f"Cancel non-exist subscription {subscription_id}") return "OK" elif request.form.get("alert_name") == "subscription_updated": subscription_id = request.form.get("subscription_id") sub: Subscription = Subscription.get_by(subscription_id=subscription_id) if sub: next_bill_date = request.form.get("next_bill_date") if not next_bill_date: paddle_callback.failed_payment(sub, subscription_id) return "OK" LOG.d( "Update subscription %s %s on %s, next bill date %s", subscription_id, sub.user, request.form.get("cancellation_effective_date"), sub.next_bill_date, ) if ( int(request.form.get("subscription_plan_id")) == PADDLE_MONTHLY_PRODUCT_ID ): plan = PlanEnum.monthly else: plan = PlanEnum.yearly sub.cancel_url = request.form.get("cancel_url") sub.update_url = request.form.get("update_url") sub.event_time = arrow.now() sub.next_bill_date = arrow.get( request.form.get("next_bill_date"), "YYYY-MM-DD" ).date() sub.plan = plan # make sure to set the new plan as not-cancelled sub.cancelled = False emit_user_audit_log( user=sub.user, action=UserAuditLogAction.SubscriptionExtended, message="Extended Paddle subscription", ) Session.commit() execute_subscription_webhook(sub.user) else: LOG.w( f"update non-exist subscription {subscription_id}. {request.form}" ) return "No such subscription", 400 elif request.form.get("alert_name") == "payment_refunded": subscription_id = request.form.get("subscription_id") LOG.d("Refund request for subscription %s", subscription_id) sub: Subscription = Subscription.get_by(subscription_id=subscription_id) if sub: user = sub.user Subscription.delete(sub.id) emit_user_audit_log( user=user, action=UserAuditLogAction.SubscriptionCancelled, message="Paddle subscription cancelled as user requested a refund", ) Session.commit() LOG.e("%s requests a refund", user) execute_subscription_webhook(sub.user) elif request.form.get("alert_name") == "subscription_payment_refunded": subscription_id = request.form.get("subscription_id") sub: Subscription = Subscription.get_by(subscription_id=subscription_id) LOG.d( "Handle subscription_payment_refunded for subscription %s", subscription_id, ) if not sub: LOG.w( "No such subscription for %s, payload %s", subscription_id, request.form, ) return "No such subscription" plan_id = int(request.form["subscription_plan_id"]) if request.form["refund_type"] == "full": if plan_id in PADDLE_MONTHLY_PRODUCT_IDS: LOG.d("subtract 1 month from next_bill_date %s", sub.next_bill_date) sub.next_bill_date = sub.next_bill_date - relativedelta(months=1) LOG.d("next_bill_date is %s", sub.next_bill_date) Session.commit() elif plan_id in PADDLE_YEARLY_PRODUCT_IDS: LOG.d("subtract 1 year from next_bill_date %s", sub.next_bill_date) sub.next_bill_date = sub.next_bill_date - relativedelta(years=1) LOG.d("next_bill_date is %s", sub.next_bill_date) Session.commit() else: LOG.e("Unknown plan_id %s", plan_id) else: LOG.w("partial subscription_payment_refunded, not handled") execute_subscription_webhook(sub.user) return "OK" @app.route("/paddle_coupon", methods=["GET", "POST"]) def paddle_coupon(): LOG.d("paddle coupon callback %s", request.form) if not paddle_utils.verify_incoming_request(dict(request.form)): LOG.e("request not coming from paddle. Request data:%s", dict(request.form)) return "KO", 400 product_id = request.form.get("p_product_id") if product_id != PADDLE_COUPON_ID: LOG.e("product_id %s not match with %s", product_id, PADDLE_COUPON_ID) return "KO", 400 email = request.form.get("email") LOG.d("Paddle coupon request for %s", email) coupon = Coupon.create( code=random_string(30), comment="For 1-year coupon", expires_date=arrow.now().shift(years=1, days=-1), commit=True, ) return ( f"Your 1-year coupon is {coupon.code}
" f"It's valid until {coupon.expires_date.date().isoformat()}" )