File: //home/arjun/projects/buyercall_new/buyercall/buyercall/blueprints/billing/models/subscription.py
import datetime
import logging
import pytz
import traceback
from flask import current_app
from flask_login import current_user
from twilio.rest import Client
from collections import OrderedDict
from config import settings
from buyercall.lib.util_sqlalchemy import ResourceMixin, AwareDateTime
from buyercall.extensions import db
from sqlalchemy import orm
from sqlalchemy.orm import load_only
from buyercall.blueprints.billing.models.credit_card import CreditCard
from buyercall.blueprints.billing.models.coupon import Coupon
from buyercall.blueprints.billing.gateways.stripecom import Card as PaymentCard
from buyercall.blueprints.billing.gateways.stripecom import \
Subscription as PaymentSubscription
log = logging.getLogger(__name__)
class Subscription(ResourceMixin, db.Model):
STATUS = OrderedDict([
('inactive', 'Inactive'),
('active', 'Active'),
('suspended', 'Suspended'),
('closed', 'Closed')
])
__tablename__ = 'subscriptions'
id = db.Column(db.Integer, primary_key=True)
# Relationships.
credit_card_id = db.Column(db.Integer, db.ForeignKey('credit_cards.id',
onupdate='CASCADE',
ondelete='CASCADE'),
index=True, nullable=True)
partnership = db.relationship('Partnership', backref='subscription', uselist=False, passive_deletes=True)
partnership_account = db.relationship('PartnershipAccount', backref='subscription',
uselist=False, passive_deletes=True)
# Subscription details.
plan = db.Column(db.String(128))
coupon = db.Column(db.String(32))
# Billing.
payment_id = db.Column(db.String(128), index=True)
cancelled_subscription_on = db.Column(AwareDateTime())
# Usage tracking
usage_current_month = db.Column(
db.Integer(), nullable=False, default=0, server_default='0'
)
# Number of warnings sent this month
limit_warning_count = db.Column(
db.Integer(), nullable=False, default=0, server_default='0'
)
# Twilio Sub Account id
twilio_subaccount_sid = db.Column(db.String(128))
status = db.Column(db.Enum(*STATUS, name='subscription_status'), index=True,
nullable=False, server_default='inactive')
def __init__(self, **kwargs):
# Call Flask-SQLAlchemy's constructor.
super(Subscription, self).__init__(**kwargs)
@orm.reconstructor
def init_fields(self):
self.__usage_over_limit = False
self.__usage_close_to_limit = False
@classmethod
def get_plan_by_id(cls, plan):
"""
Pick the plan based on the plan identifier.
:param plan: Plan identifier
:type plan: str
:return: dict or None
"""
for key, value in settings.STRIPE_PLANS.items():
if value.get('id') == plan:
return settings.STRIPE_PLANS[key]
return None
@classmethod
def get_new_plan(cls, keys):
"""
Pick the plan based on the plan identifier.
:param keys: Keys to look through
:type keys: list
:return: str or None
"""
for key in keys:
split_key = key.split('submit_')
if isinstance(split_key, list) and len(split_key) == 2:
if Subscription.get_plan_by_id(split_key[1]):
return split_key[1]
return None
@classmethod
def reset_monthly_usage(cls):
"""
Reset an account monthly usage in subscription table.
:return: Result of updating the records
"""
from buyercall.app import create_app
# Create a context for the database connection.
app = create_app()
db.app = app
with app.app_context():
Subscription.query.update({
Subscription.usage_current_month: 0,
Subscription.limit_warning_count: 0
})
db.session.commit()
subscriptions = Subscription.query.options(
load_only('twilio_subaccount_sid')
).filter(
Subscription.plan is not None,
Subscription.cancelled_subscription_on.is_(None),
Subscription.twilio_subaccount_sid.isnot(None)
).all()
for subscription in subscriptions:
try:
subscription.activate_twilio_account()
except Exception as e:
log.error(traceback.format_exc())
pass
def create(self, user=None, name=None, plan=None, coupon=None, token=None, quantity=None):
"""
Create a recurring subscription.
:param user: User to apply the subscription to
:type user: User instance
:param name: User's billing name
:type name: str
:param plan: Plan identifier
:type plan: str
:param coupon: Coupon code to apply
:type coupon: str
:param token: Token returned by javascript
:type token: str
:param quantity: Quantity identifier
:type token: str
:return: bool
"""
if token is None:
return False
if coupon:
self.coupon = coupon.upper()
try:
customer = PaymentSubscription.create(
token=token,
email=user.email,
plan=plan,
quantity=quantity,
coupon=self.coupon
)
self._activate_twilio(user)
# Update the user account.
self.payment_id = customer.id
self.cancelled_subscription_on = None
# Set the subscription details.
if user.role == 'admin':
self.partnership_account = user.partnership_account
if user.role == 'partner':
self.partnership = user.partnership
self.plan = plan
# Redeem the coupon.
if coupon:
coupon = Coupon.query.filter(Coupon.code == self.coupon).first()
coupon.redeem()
# Create the credit card.
credit_card = CreditCard(cc_name=name,
**CreditCard.extract_card_params(customer))
self.credit_card = credit_card
self.status = 'active'
db.session.add(user)
db.session.add(credit_card)
db.session.add(self)
db.session.commit()
return True
except Exception as e:
log.error(traceback.format_exc())
db.session.rollback()
return False
def create_invoice_subscription(self, user=None, plan=None):
"""
Create a recurring subscription.
:param user: User to apply the subscription to
:type user: User instance
:param plan: Plan identifier
:type plan: str
"""
try:
customer = PaymentSubscription.create_invoice_subscription(
email=user.email,
plan=plan
)
self._activate_twilio(user)
# Update the user account.
self.payment_id = customer.id
self.cancelled_subscription_on = None
# Set the subscription details.
if user.role == 'admin':
self.partnership_account = user.partnership_account
if user.role == 'partner':
self.partnership = user.partnership
self.plan = plan
self.status = 'active'
db.session.add(user)
db.session.add(self)
db.session.commit()
return True
except Exception as e:
log.error(traceback.format_exc())
db.session.rollback()
return False
def update(self, user=None, coupon=None, plan=None):
"""
Update an existing subscription.
:param user: User to apply the subscription to
:type user: User instance
:param coupon: Coupon code to apply
:type coupon: str
:param plan: Plan identifier
:type plan: str
:return: bool
"""
PaymentSubscription.update(self.payment_id, coupon, plan)
self.plan = plan
if coupon:
self.coupon = coupon
coupon = Coupon.query.filter(Coupon.code == coupon).first()
if coupon:
coupon.redeem()
db.session.add(self)
db.session.commit()
return True
def cancel(self, user=None, discard_credit_card=True):
"""
Cancel an existing subscription.
:param user: User to apply the subscription to
:type user: User instance
:param discard_credit_card: Delete the user's credit card
:type discard_credit_card: bool
:return: bool
"""
PaymentSubscription.cancel(self.payment_id)
self.payment_id = None
self.cancelled_subscription_on = datetime.datetime.now(pytz.utc)
self.status = 'closed'
db.session.add(self)
self.close_twilio_account()
# Explicitly delete the credit card because the FK is on the
# user, not subscription so we can't depend on cascading deletes.
# This is for cases where you may want to keep a user's card
# on file even if they cancelled.
# if discard_credit_card and current_user.subscription.plan != 'invoice':
# # db.session.delete(self.credit_card)
# pass
db.session.commit()
return True
def update_payment_method(self, user=None, name=None, token=None):
"""
Update the subscription.
:param user: User to apply the subscription to
:type user: User instance
:param name: User's billing name
:type name: str
:param token: Token returned by javascript
:type token: str
:return: bool
"""
if token is None:
return False
customer = PaymentCard.update(self.payment_id, token)
old_card = self.credit_card
old_card.is_deleted = True
# Create the new credit card.
credit_card = CreditCard(cc_name=name,
**CreditCard.extract_card_params(customer))
self.credit_card = credit_card
db.session.add(old_card)
db.session.add(user)
db.session.add(credit_card)
db.session.commit()
return True
def _activate_twilio(self, user):
twilio_client = Client(
current_app.config['TWILIO_ACCOUNT_SID'],
current_app.config['TWILIO_AUTH_TOKEN'])
# check if user already has twilio sub account
twilio_sub_sid = self.twilio_subaccount_sid
if not twilio_sub_sid:
# Create a Twilio sub account for the user
subaccount = twilio_client.api.accounts.create(
friendly_name=user.company
)
self.twilio_subaccount_sid = subaccount.sid
else:
account = twilio_client.api.accounts(self.twilio_subaccount_sid).fetch()
if account.status == 'suspended':
account.update(status='active')
db.session.add(self)
db.session.commit()
def _twilio_account_action(self, status):
if not self.twilio_subaccount_sid:
return
twilio_client = Client(
current_app.config['TWILIO_ACCOUNT_SID'],
current_app.config['TWILIO_AUTH_TOKEN']
)
try:
sub_account = twilio_client.api.accounts(self.twilio_subaccount_sid).fetch()
if sub_account:
try:
sub_account.update(status=status)
self.status = status
db.session.add(self)
db.session.commit()
except Exception as e:
log.info('Unable to update twilio account with active status.Most likely due to the account being'
' a master account and not a twilio sub-account.')
else:
log.info('No sub-account was found for twilio sid {}'.format(self.twilio_subaccount_sid))
except Exception as e:
log.info(e)
log.info('No connection to twilio was made get the correct sub account')
def suspend_twilio_account(self):
self._twilio_account_action(status="suspended")
def close_twilio_account(self):
self._twilio_account_action(status="closed")
from buyercall.blueprints.phonenumbers.models import Phone
from buyercall.blueprints.partnership.models import PartnershipAccount
import pytz
from datetime import datetime
if self.partnership:
ids = db.session.query(PartnershipAccount.id).\
filter(PartnershipAccount.partnership_id == self.partnership.id).\
all()
db.session.query(Phone) \
.filter(Phone.partnership_account_id.in_(ids)) \
.update({"active": False, "deactivated_on": datetime.now(pytz.utc)}, synchronize_session='fetch')
if self.partnership_account:
db.session.query(Phone) \
.filter(Phone.partnership_account_id == self.partnership_account.id) \
.update({"active": False, "deactivated_on": datetime.now(pytz.utc)}, synchronize_session='fetch')
db.session.commit()
def activate_twilio_account(self):
self._twilio_account_action(status="active")
@property
def usage_limit(self):
plan = Subscription.get_plan_by_id(self.plan)
if not plan or ('metadata' not in plan):
return None
return plan['metadata'].get('minutes')
@property
def agent_limit(self):
plan = Subscription.get_plan_by_id(self.plan)
if not plan or ('metadata' not in plan):
return None
return plan['metadata'].get('agents')
@property
def phonenumber_limit(self):
plan = Subscription.get_plan_by_id(self.plan)
if not plan or ('metadata' not in plan):
return None
return plan['metadata'].get('phonenumbers')
@property
def priority_number_limit(self):
plan = Subscription.get_plan_by_id(self.plan)
if not plan or ('metadata' not in plan):
return None
return plan['metadata'].get('priority_number')
@property
def usage_over_limit(self):
usage_limit = self.usage_limit
if not usage_limit:
return False
result = self.__usage_over_limit or (
self.usage_current_month > usage_limit
)
self.__usage_over_limit = result
return result
@property
def usage_close_to_limit(self):
usage_limit = self.usage_limit
if not usage_limit:
return False
result = self.__usage_close_to_limit or \
(self.usage_current_month > 0.9 * usage_limit and self.usage_current_month <= self.usage_limit)
self.__usage_close_to_limit = result
return result
@classmethod
def update_usage(cls, partnership_account_id, minutes=0, seconds=0):
# Round up the seconds value to the nearest minute
if seconds:
minutes += (seconds + 59) / 60
if not minutes:
log.debug('No usage incurred for call.')
return
log.debug('Updating usage: {} minutes'.format(minutes))
from buyercall.blueprints.user.models import User
from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
partnership_account = PartnershipAccount.query.join(
PartnershipAccount.partnership
).filter(PartnershipAccount.id == partnership_account_id).one()
partnership = partnership_account.partnership
subscription = partnership_account.subscription
if not subscription:
subscription = partnership.subscription
db.session.begin_nested()
subscription.usage_current_month += minutes
db.session.add(subscription)
db.session.commit()
# Send warning email, if we're close to the limit
if partnership_account.billing_type == 'account':
ids = User.query.filter(User.partnership_account_id == partnership_account.id,
User.is_deactivated.is_(False),
User.active,
User.role == 'admin')\
.all()
elif partnership.billing_type == 'partnership':
ids = User.query.filter(User.partnership_id == partnership.id,
User.is_deactivated.is_(False),
User.active,
User.role == 'partner') \
.all()
else:
"""
if billing_type is set to invoice
"""
ids = User.query.filter(User.active,
User.role == 'sysadmin') \
.all()
if subscription.usage_close_to_limit and subscription.limit_warning_count == 0:
from buyercall.blueprints.billing.tasks import send_limit_warning_email
for user_id in ids:
partner_account = PartnershipAccount.query.\
filter(PartnershipAccount.id == user_id.partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
send_limit_warning_email.delay(user_id, partner.name, partner.logo)
# Send second warning email if we're over the limit
if subscription.usage_over_limit and subscription.limit_warning_count < 2:
from buyercall.blueprints.billing.tasks import send_second_limit_warning_email
for user_id in ids:
partner_account = PartnershipAccount.query. \
filter(PartnershipAccount.id == user_id.partnership_account_id).first()
partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
send_second_limit_warning_email.delay(user_id, partner.name, partner.logo)
subscription.suspend_twilio_account()