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 %}
<p>
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()}}
({{ sub.next_bill_date | dt }}).
You have canceled your subscription and it will end on {{ current_user.next_bill_date() }}
</p>
<hr>
@ -33,23 +32,47 @@
{% else %}
<p>
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>
<div class="mt-3">
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>
<hr>
<div>
<h4>Cancel subscription</h4>
Don't want to protect your inbox anymore? <br>
<form method="post">
<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>
</span>
</form>

View File

@ -27,7 +27,7 @@
<div class="card-body">
<div class="card-title">
Change Email Address
Email Address
</div>
<div class="form-group">
<label class="form-label">Email</label>
@ -60,9 +60,12 @@
<div class="card-body">
<div class="card-title">
Change Profile
Profile
</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>
{{ form.name(class="form-control", value=current_user.name) }}
{{ render_field_errors(form.name) }}

View File

@ -1,11 +1,12 @@
from flask import render_template, flash, redirect, url_for, request
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.log import LOG
from app.models import Subscription
from app.models import Subscription, PlanEnum
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"])
@ -29,10 +30,43 @@ def billing():
flash("Your subscription has been canceled successfully", "success")
else:
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",
)
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
def lifetime_or_active_subscription(self) -> bool:
def _lifetime_or_active_subscription(self) -> bool:
"""True if user has lifetime licence or active subscription"""
if self.lifetime:
return True
@ -219,7 +219,7 @@ class User(db.Model, ModelMixin, UserMixin):
def in_trial(self):
"""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
if self.trial_end and arrow.now() < self.trial_end:
@ -228,7 +228,7 @@ class User(db.Model, ModelMixin, UserMixin):
return False
def should_upgrade(self):
if self.lifetime_or_active_subscription():
if self._lifetime_or_active_subscription():
# user who has canceled can also re-subscribe
sub: Subscription = self.get_subscription()
if sub and sub.cancelled:
@ -264,7 +264,7 @@ class User(db.Model, ModelMixin, UserMixin):
- in trial period or
- active subscription
"""
if self.lifetime_or_active_subscription():
if self._lifetime_or_active_subscription():
return True
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"]
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",
)
db.session.commit()
user.trial_end = None
LifetimeCoupon.create(code="coupon", nb_used=10)
db.session.commit()
@ -385,12 +386,15 @@ def setup_paddle_callback(app: Flask):
LOG.debug("Update subscription %s", subscription_id)
sub: Subscription = Subscription.get_by(subscription_id=subscription_id)
sub.event_time = arrow.now()
sub.next_bill_date = arrow.get(
request.form.get("next_bill_date"), "YYYY-MM-DD"
).date()
# 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()
db.session.commit()
db.session.commit()
elif request.form.get("alert_name") == "subscription_cancelled":
subscription_id = request.form.get("subscription_id")
@ -411,7 +415,40 @@ def setup_paddle_callback(app: Flask):
db.session.commit()
else:
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"

View File

@ -69,4 +69,17 @@ em {
.cursor {
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() %}
<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
{% if current_user.is_cancel() %}
until {{ current_user.next_bill_date() }}