Skip to content

Commit

Permalink
wip Fix #1133 I can configure plan prorations as a shop owner
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsimpson committed Aug 1, 2024
1 parent 3ee15c4 commit 754c8ac
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""add proration_behavior stripe_proration_behavior plan subscription
Revision ID: d8c120e8212e
Revises: a4d35e9917f7
Create Date: 2024-07-27 22:21:26.823294
"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "d8c120e8212e"
down_revision = "a4d35e9917f7"
branch_labels = None
depends_on = None


def upgrade():
# "none" not to be confused with "None"
# See
# https://docs.stripe.com/api/subscriptions/create#create_subscription-proration_behavior
with op.batch_alter_table("plan", schema=None) as batch_op:
batch_op.add_column(
sa.Column("proration_behavior", sa.String(), nullable=True, default="none")
)

# "none" not to be confused with "None"
# See
# https://docs.stripe.com/api/subscriptions/create#create_subscription-proration_behavior
with op.batch_alter_table("subscription", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"stripe_proration_behavior", sa.String(), nullable=True, default="none"
)
)


def downgrade():
pass
1 change: 0 additions & 1 deletion subscribie/anti_spam_subscribie_shop_names/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

# Preprocess the data: character-level features
char_vectorizer = CountVectorizer(analyzer="char", ngram_range=(2, 5))
breakpoint()
X_train_char = char_vectorizer.fit_transform(X_train)
X_test_char = char_vectorizer.transform(X_test)

Expand Down
48 changes: 45 additions & 3 deletions subscribie/blueprints/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
from .getLoadedModules import getLoadedModules
import uuid
from sqlalchemy import desc
from datetime import datetime
from datetime import datetime, time
from subscribie.models import (
Transaction,
EmailTemplate,
Expand Down Expand Up @@ -125,10 +125,10 @@ def ordinal(n):


@admin.app_template_filter()
def timestampToDate(timestamp: str):
def timestampToDate(timestamp: str, date_format="%d-%m-%Y"):
if timestamp is None:
return None
return datetime.fromtimestamp(int(timestamp)).strftime("%d-%m-%Y")
return datetime.fromtimestamp(int(timestamp)).strftime(date_format)


def dtStylish(dt, f):
Expand Down Expand Up @@ -542,6 +542,16 @@ def edit():
database.session.add(draftPlan)
plan_requirements = PlanRequirements()
draftPlan.cancel_at = cancel_at
proration_behavior = getPlan(form.proration_behavior.data, index)
if proration_behavior == "on":
proration_behavior = "create_prorations"
msg = f"Updating plan proration_behavior to '{proration_behavior}'"
log.debug(msg)
else:
proration_behavior = "none"
msg = f"Updating plan proration_behavior to '{proration_behavior}'"
log.debug(msg)
draftPlan.proration_behavior = proration_behavior
draftPlan.uuid = str(uuid.uuid4())
draftPlan.parent_plan_revision_uuid = plan.uuid
draftPlan.requirements = plan_requirements
Expand Down Expand Up @@ -652,6 +662,22 @@ def edit():
new_plan_question_assoc.plan_id = draftPlan.id
database.session.add(new_plan_question_assoc)

# If cancel_at is not set, ensure plan cancel_at is not set
if request.form.get(f"cancel_at_set-{index}") is None:
draftPlan.cancel_at = None
else:
# If cancel_at_set is set get date and time and convert to
# timestamp
if getPlan(form.cancel_at.data, index) != "":
cancel_at_date = datetime.strptime(
getPlan(form.cancel_at.data, index), "%Y-%m-%d"
)
cancel_at_time = getPlan(form.cancel_at_time.data, index) or time(
hour=0, minute=0, second=0, microsecond=0
) # noqa: E501
cancel_at = datetime.combine(cancel_at_date.date(), cancel_at_time)
draftPlan.cancel_at = int(float(cancel_at.timestamp()))

database.session.commit() # Save
flash("Plan(s) updated.")
return redirect(url_for("admin.edit"))
Expand Down Expand Up @@ -762,6 +788,22 @@ def add_plan():
cancel_at = datetime.combine(cancel_at_date.date(), cancel_at_time.time())
draftPlan.cancel_at = int(float(cancel_at.timestamp()))

# Set proration_behavior
# See:
# - https://github.com/Subscribie/subscribie/issues/1133
# - https://docs.stripe.com/api/subscriptions/create#create_subscription-proration_behavior # noqa: E501
proration_behavior = request.form.get("proration_behavior-0")
if proration_behavior == "on":
proration_behavior = "create_prorations"
msg = f"Setting plan proration_behavior to '{proration_behavior}'"
log.debug(msg)
else:
proration_behavior = "none"
msg = f"Setting plan proration_behavior to '{proration_behavior}'"
log.debug(msg)

draftPlan.proration_behavior = proration_behavior

# filling plan price_list with existing price_lists
draftPlan.assignDefaultPriceLists()

Expand Down
23 changes: 23 additions & 0 deletions subscribie/blueprints/admin/templates/admin/add_plan.html
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,29 @@ <h2 class="text-center text-dark mb-3">Create a new plan</h2>
<small class="form-text text-muted">
Time (e.g. midnight)
</small>
<br />
<div class="form-check">
<input class="form-check-input" type="radio" name="proration_behavior-0" value="off" id="prorate_option2" checked>
<label class="form-check-label" for="prorate_option2">
Do not prorate
</label>
<small class="form-text text-muted">
Charge subscribers the full amount of the subscription, even
if it ends before the period is over.
</small>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="proration_behavior-0" value="on" id="prorate_option1">
<label class="form-check-label" for="prorate_option1">
Prorate
</label>
<small class="form-text text-muted">
Don't charge subscribers the full amount if the subscription
ends before the period is over. For example, if a monthly
subscription ends partway through the month, the final charge
will be less since it is prorated based on the time used.
</small>
</div>
</div>
</fieldset>

Expand Down
45 changes: 45 additions & 0 deletions subscribie/blueprints/admin/templates/admin/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,51 @@ <h2 class="text-center text-dark mb-3">Edit Plans</h2>
Then, you can send a link to your customer(s) to sign-up to the private plan.
</small>
</div>
<div class="form-group ">
<div class="form-check">
<input type="checkbox" value="yes" class="form-check-input toggle" name="cancel_at_set-{{ loop.index0 }}" id="cancel_at_set-{{ loop.index0 }}" {% if plan.cancel_at and plan.cancel_at != 0 %} checked {% endif %}>
<label class="form-check-label font-weight-bolder" for="cancel_at_set-{{ loop.index0 }}">Cancel at</label>
</div>
<small class="form-text text-muted">
Cancel all subscriptions to this plan at this date / time. This is useful if you run a
membership style organisation with seasons (such as a Football club).
</small>
</div>
<div class="form-group extra_fields" id="cancel_at" >
<label for="cancel_at_date-{{ loop.index0 }}" class="col-form-label font-weight-bolder">Date &amp; time to Cancel at</label>
<input type="date" class="form-control" name="cancel_at-{{ loop.index0 }}" {% if plan.cancel_at != 0 %} value="{{ plan.cancel_at|timestampToDate("%Y-%m-%d") }}" {% endif %} id="cancel_at_date-{{ loop.index0 }}">
<small class="form-text text-muted mb-2">
Date (e.g. 30th June 2022) {{ plan.cancel_at|timestampToDate("%H:%M") }}
</small>
<input type="time" class="form-control" name="cancel_at_time-{{ loop.index0 }}" {% if plan.cancel_at != 0 %} value="{{ plan.cancel_at|timestampToDate("%H:%M") }}" {% endif %} id="cancel_at_time-{{ loop.index0 }}">
<small class="form-text text-muted">
Time (e.g. midnight) {{ plan.proration_behavior }}
</small>
<br />
<div class="form-check">
<input class="form-check-input" type="radio" name="proration_behavior-{{ loop.index0 }}" value="off" id="prorate_option2-{{ loop.index0 }}" checked>
<label class="form-check-label" for="prorate_option2-{{ loop.index0 }}">
Do not prorate
</label>
<small class="form-text text-muted">
Charge subscribers the full amount of the subscription, even
if it ends before the period is over.
</small>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="proration_behavior-{{ loop.index0 }}" value="on" id="prorate_option1-{{ loop.index0 }}" {% if plan.proration_behavior == "create_prorations" %} checked {% endif %} />
<label class="form-check-label" for="prorate_option1-{{ loop.index0 }}">
Prorate
</label>
<small class="form-text text-muted">
Don't charge subscribers the full amount if the subscription
ends before the period is over. For example, if a monthly
subscription ends partway through the month, the final charge
will be less since it is prorated based on the time used.
</small>
</div>
</div>

<a href="{{url_for("views.view_plan", uuid=plan.uuid, plan_title=plan.title)}}" target="_blank">Share URL</a>
</fieldset>
<!-- end plan description & settings -->
Expand Down
9 changes: 9 additions & 0 deletions subscribie/blueprints/admin/templates/admin/subscribers.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ <h4>Search...</h4>
(No up-front fee)
{% endif %}
</span>
{% if subscription.plan.requirements and subscription.plan.requirements.subscription %}
<li><strong>Proration mode: </strong>
{% if subscription.stripe_proration_behavior == "create_prorations" %}
Apply Pro Rata Charges
{% elif subscription.stripe_proration_behavior == "none" %}
No Pro Rata Charges
{% endif %}
</li>
{% endif %}
<li><strong>Status:</strong>
{% if subscription.plan.requirements and subscription.plan.requirements.subscription %}
{% if subscription.stripe_pause_collection == "void" %}
Expand Down
34 changes: 33 additions & 1 deletion subscribie/blueprints/checkout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
signal_payment_failed,
signal_new_donation,
)
from subscribie.notifications import newSubscriberEmailNotification
import stripe
import backoff
import os
Expand Down Expand Up @@ -1091,6 +1090,39 @@ def stripe_webhook():
except KeyError:
chosen_option_ids = None

# Set proration_behavior if is a subscription
# https://github.com/Subscribie/subscribie/issues/1133
# TODO proration_behavior should be set regarless of
# payment provider.
if stripe_subscription_id:
subscription = (
Subscription.query.filter_by(
subscribie_checkout_session_id=subscribie_checkout_session_id
)
.filter(Subscription.person.has(email=email))
.first()
)
if subscription is not None:
stripe.api_key = get_stripe_secret_key()
connect_account_id = get_stripe_connect_account_id()
proration_behavior = subscription.plan.proration_behavior or "none"
log.info(
f"Updating stripe proration_behavior to {proration_behavior}"
) # noqa: E501
try:
stripe.Subscription.modify(
stripe_subscription_id,
stripe_account=connect_account_id,
proration_behavior=proration_behavior,
)
# Set the local db model stripe_proration_behavior
subscription.stripe_proration_behavior = proration_behavior
database.session.commit()
except Exception as e: # noqa
log.error(
"Could not update proration_behavior: '{e}' for stripe_subscription_id {stripe_subscription_id}" # noqa: E501
)

"""
We treat Stripe checkout session.mode equally because
a subscribie plan may either be a one-off plan or a
Expand Down
10 changes: 10 additions & 0 deletions subscribie/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
TextAreaField,
IntegerField,
MultipleFileField,
TimeField,
)
from wtforms.validators import Optional, DataRequired, Email as EmailValid
from flask_wtf.file import FileField, FileAllowed
from flask_uploads import UploadSet, IMAGES
from datetime import time


class StripWhitespaceForm(FlaskForm):
Expand Down Expand Up @@ -76,6 +78,14 @@ class PlansForm(StripWhitespaceForm):
cancel_at = FieldList(
StringField("Cancel at", [validators.optional()], default=False)
)
cancel_at_time = FieldList(
TimeField(
"Cancel at time",
[validators.optional()],
default=time(hour=0, minute=0, second=0, microsecond=0),
)
) # noqa: E501
proration_behavior = FieldList(StringField("proration_behavior"))
description = FieldList(
StringField("Description", [validators.optional()], default=False)
)
Expand Down
14 changes: 14 additions & 0 deletions subscribie/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,16 @@ class Subscription(database.Model):
# which creates subscriptions.
stripe_cancel_at = database.Column(database.Integer(), default=0)
stripe_pause_collection = database.Column(database.String())
# By default Subscribie chooses to set proration_behavior to none (off) since
# https://github.com/Subscribie/subscribie/issues/1133
# See
# - https://github.com/Subscribie/subscribie/issues/1133
# - https://docs.stripe.com/api/subscriptions/create#create_subscription-proration_behavior # noqa: E501
# - https://docs.stripe.com/api/subscriptions/update?lang=python
# proration_behavior indentionally string "none" not to be confused with None
# See
# https://docs.stripe.com/api/subscriptions/create#create_subscription-proration_behavior
stripe_proration_behavior = database.Column(database.String(), default="none")

def stripe_subscription_active(self):
if self.stripe_subscription_id is not None:
Expand Down Expand Up @@ -701,6 +711,10 @@ class Plan(database.Model, HasArchived):
category = relationship("Category", back_populates="plans")
private = database.Column(database.Boolean(), default=0)
cancel_at = database.Column(database.Integer(), default=0)
# proration_behavior indentionally string "none" not to be confused with None
# See
# https://docs.stripe.com/api/subscriptions/create#create_subscription-proration_behavior
proration_behavior = database.Column(database.String(), default="none")
price_lists = relationship(
"PriceList", secondary=association_table_plan_to_price_lists
)
Expand Down

0 comments on commit 754c8ac

Please sign in to comment.