Merge pull request #126 from simple-login/change-plan

User can change plan
This commit is contained in:
Son Nguyen Kim 2020-04-12 19:45:59 +02:00 committed by GitHub
commit b4f28a5156
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 151 additions and 22 deletions

View file

@ -14,8 +14,7 @@
{% if sub.cancelled %} {% if sub.cancelled %}
<p> <p>
You are on the <b>{{ sub.plan_name() }}</b> plan. <br> You are on the <b>{{ sub.plan_name() }}</b> plan. <br>
You have canceled your subscription and it will end on {{current_user.next_bill_date()}} You have canceled your subscription and it will end on {{ current_user.next_bill_date() }}
({{ sub.next_bill_date | dt }}).
</p> </p>
<hr> <hr>
@ -33,23 +32,47 @@
{% else %} {% else %}
<p> <p>
You are on the <b>{{ sub.plan_name() }}</b> plan. Thank you very much for supporting You are on the <b>{{ sub.plan_name() }}</b> plan. Thank you very much for supporting
SimpleLogin. 🙌 SimpleLogin. 🙌 <br>
The next billing cycle starts at {{ sub.next_bill_date.strftime("%Y-%m-%d") }}.
</p> </p>
<div class="mt-3"> <div class="mt-3">
Click here to update billing information on Paddle, our payment partner: <br> Click here to update billing information on Paddle, our payment partner: <br>
<a class="btn btn-success" href="{{ sub.update_url }}"> Update billing information </a> <a class="btn btn-outline-success mt-2" href="{{ sub.update_url }}"> Update billing information </a>
</div>
<hr>
<div class="mt-6">
<h4>Change Plan</h4>
You can change the plan at any moment. <br>
Please note that the new billing cycle starts instantly
i.e. you will be charged <b>immediately</b> the annual fee when switching from monthly plan or vice-versa
<b>without pro rata computation </b>. <br>
To change the plan you can also cancel the current one and subscribe a new one <b>by the end</b> of this plan.
{% if sub.plan == PlanEnum.yearly %}
<form method="post">
<input type="hidden" name="form-name" value="change-monthly">
<button class="btn btn-outline-primary mt-2">Change to Monthly Plan</button>
</form>
{% else %}
<form method="post">
<input type="hidden" name="form-name" value="change-yearly">
<button class="btn btn-outline-primary mt-2">Change to Yearly Plan</button>
</form>
{% endif %}
</div> </div>
<hr> <hr>
<div> <div>
<h4>Cancel subscription</h4>
Don't want to protect your inbox anymore? <br> Don't want to protect your inbox anymore? <br>
<form method="post"> <form method="post">
<input type="hidden" name="form-name" value="cancel"> <input type="hidden" name="form-name" value="cancel">
<span class="cancel btn btn-warning"> <span class="cancel btn btn-outline-danger mt-2">
Cancel subscription <i class="fe fe-alert-triangle text-danger"></i> Cancel subscription <i class="fe fe-alert-triangle text-danger"></i>
</span> </span>
</form> </form>

View file

@ -27,7 +27,7 @@
<div class="card-body"> <div class="card-body">
<div class="card-title"> <div class="card-title">
Change Email Address Email Address
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Email</label> <label class="form-label">Email</label>
@ -60,9 +60,12 @@
<div class="card-body"> <div class="card-body">
<div class="card-title"> <div class="card-title">
Change Profile Profile
</div> </div>
<div class="form-group"> <div>
These informations will be filled up automatically when you use "Sign in with SimpleLogin" button
</div>
<div class="form-group mt-3">
<label class="form-label">Name</label> <label class="form-label">Name</label>
{{ form.name(class="form-control", value=current_user.name) }} {{ form.name(class="form-control", value=current_user.name) }}
{{ render_field_errors(form.name) }} {{ render_field_errors(form.name) }}

View file

@ -1,11 +1,12 @@
from flask import render_template, flash, redirect, url_for, request from flask import render_template, flash, redirect, url_for, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from app.config import PADDLE_MONTHLY_PRODUCT_ID, PADDLE_YEARLY_PRODUCT_ID
from app.dashboard.base import dashboard_bp from app.dashboard.base import dashboard_bp
from app.log import LOG from app.log import LOG
from app.models import Subscription from app.models import Subscription, PlanEnum
from app.extensions import db from app.extensions import db
from app.paddle_utils import cancel_subscription from app.paddle_utils import cancel_subscription, change_plan
@dashboard_bp.route("/billing", methods=["GET", "POST"]) @dashboard_bp.route("/billing", methods=["GET", "POST"])
@ -29,10 +30,43 @@ def billing():
flash("Your subscription has been canceled successfully", "success") flash("Your subscription has been canceled successfully", "success")
else: else:
flash( flash(
"Something went wrong, sorry for the inconvenience. Please retry. We are already notified and will be on it asap", "Something went wrong, sorry for the inconvenience. Please retry. "
"We are already notified and will be on it asap",
"error",
)
return redirect(url_for("dashboard.billing"))
elif request.form.get("form-name") == "change-monthly":
LOG.debug(f"User {current_user} changes to monthly plan")
success = change_plan(sub.subscription_id, PADDLE_MONTHLY_PRODUCT_ID)
if success:
sub.plan = PlanEnum.monthly
db.session.commit()
flash("Your subscription has been updated", "success")
else:
flash(
"Something went wrong, sorry for the inconvenience. Please retry. "
"We are already notified and will be on it asap",
"error",
)
return redirect(url_for("dashboard.billing"))
elif request.form.get("form-name") == "change-yearly":
LOG.debug(f"User {current_user} changes to yearly plan")
success = change_plan(sub.subscription_id, PADDLE_YEARLY_PRODUCT_ID)
if success:
sub.plan = PlanEnum.yearly
db.session.commit()
flash("Your subscription has been updated", "success")
else:
flash(
"Something went wrong, sorry for the inconvenience. Please retry. "
"We are already notified and will be on it asap",
"error", "error",
) )
return redirect(url_for("dashboard.billing")) return redirect(url_for("dashboard.billing"))
return render_template("dashboard/billing.html", sub=sub) return render_template("dashboard/billing.html", sub=sub, PlanEnum=PlanEnum)

View file

@ -202,7 +202,7 @@ class User(db.Model, ModelMixin, UserMixin):
return user return user
def lifetime_or_active_subscription(self) -> bool: def _lifetime_or_active_subscription(self) -> bool:
"""True if user has lifetime licence or active subscription""" """True if user has lifetime licence or active subscription"""
if self.lifetime: if self.lifetime:
return True return True
@ -219,7 +219,7 @@ class User(db.Model, ModelMixin, UserMixin):
def in_trial(self): def in_trial(self):
"""return True if user does not have lifetime licence or an active subscription AND is in trial period""" """return True if user does not have lifetime licence or an active subscription AND is in trial period"""
if self.lifetime_or_active_subscription(): if self._lifetime_or_active_subscription():
return False return False
if self.trial_end and arrow.now() < self.trial_end: if self.trial_end and arrow.now() < self.trial_end:
@ -228,7 +228,7 @@ class User(db.Model, ModelMixin, UserMixin):
return False return False
def should_upgrade(self): def should_upgrade(self):
if self.lifetime_or_active_subscription(): if self._lifetime_or_active_subscription():
# user who has canceled can also re-subscribe # user who has canceled can also re-subscribe
sub: Subscription = self.get_subscription() sub: Subscription = self.get_subscription()
if sub and sub.cancelled: if sub and sub.cancelled:
@ -264,7 +264,7 @@ class User(db.Model, ModelMixin, UserMixin):
- in trial period or - in trial period or
- active subscription - active subscription
""" """
if self.lifetime_or_active_subscription(): if self._lifetime_or_active_subscription():
return True return True
if self.trial_end and arrow.now() < self.trial_end: if self.trial_end and arrow.now() < self.trial_end:

View file

@ -76,3 +76,22 @@ def cancel_subscription(subscription_id: int) -> bool:
) )
return res["success"] return res["success"]
def change_plan(subscription_id: int, plan_id) -> bool:
r = requests.post(
"https://vendors.paddle.com/api/2.0/subscription/users/update",
data={
"vendor_id": PADDLE_VENDOR_ID,
"vendor_auth_code": PADDLE_AUTH_CODE,
"subscription_id": subscription_id,
"plan_id": plan_id,
},
)
res = r.json()
if not res["success"]:
LOG.error(
f"cannot change subscription {subscription_id} to {plan_id}, paddle response: {res}"
)
return res["success"]

View file

@ -133,6 +133,7 @@ def fake_data():
otp_secret="base32secret3232", otp_secret="base32secret3232",
) )
db.session.commit() db.session.commit()
user.trial_end = None
LifetimeCoupon.create(code="coupon", nb_used=10) LifetimeCoupon.create(code="coupon", nb_used=10)
db.session.commit() db.session.commit()
@ -385,12 +386,15 @@ def setup_paddle_callback(app: Flask):
LOG.debug("Update subscription %s", subscription_id) LOG.debug("Update subscription %s", subscription_id)
sub: Subscription = Subscription.get_by(subscription_id=subscription_id) sub: Subscription = Subscription.get_by(subscription_id=subscription_id)
sub.event_time = arrow.now() # when user subscribes, the "subscription_payment_succeeded" can arrive BEFORE "subscription_created"
sub.next_bill_date = arrow.get( # at that time, subscription object does not exist yet
request.form.get("next_bill_date"), "YYYY-MM-DD" if sub:
).date() sub.event_time = arrow.now()
sub.next_bill_date = arrow.get(
request.form.get("next_bill_date"), "YYYY-MM-DD"
).date()
db.session.commit() db.session.commit()
elif request.form.get("alert_name") == "subscription_cancelled": elif request.form.get("alert_name") == "subscription_cancelled":
subscription_id = request.form.get("subscription_id") subscription_id = request.form.get("subscription_id")
@ -411,7 +415,40 @@ def setup_paddle_callback(app: Flask):
db.session.commit() db.session.commit()
else: else:
return "No such subscription", 400 return "No such subscription", 400
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:
LOG.debug(
"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
db.session.commit()
else:
return "No such subscription", 400
return "OK" return "OK"

View file

@ -69,4 +69,17 @@ em {
.cursor { .cursor {
cursor: pointer; cursor: pointer;
}
/*Left border for alert zone*/
.alert-primary{
border-left: 5px #467fcf solid;
}
.alert-danger{
border-left: 5px #6b1110 solid;
}
.alert-danger::before {
content: "⚠️";
} }

View file

@ -27,7 +27,7 @@
{% if current_user.in_trial() %} {% if current_user.in_trial() %}
<small class="text-success d-block mt-1">Trial ends {{ current_user.trial_end|dt }}</small> <small class="text-success d-block mt-1">Trial ends {{ current_user.trial_end|dt }}</small>
{% elif current_user.lifetime_or_active_subscription() %} {% elif current_user.is_premium() %}
<small class="text-success d-block mt-1">Premium <small class="text-success d-block mt-1">Premium
{% if current_user.is_cancel() %} {% if current_user.is_cancel() %}
until {{ current_user.next_bill_date() }} until {{ current_user.next_bill_date() }}