HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux spn-python 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 x86_64
User: arjun (1000)
PHP: 8.1.2-1ubuntu2.20
Disabled: NONE
Upload Files
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()