class Coupon(ResourceMixin, db.Model): DURATION = OrderedDict([('forever', 'Forever'), ('once', 'Once'), ('repeating', 'Repeating')]) __tablename__ = 'coupons' id = db.Column(db.Integer, primary_key=True) # Coupon details. code = db.Column(db.String(128), index=True, unique=True) duration = db.Column(db.Enum(*DURATION, name='duration_types'), index=True, nullable=False, server_default='forever') amount_off = db.Column(db.Integer()) percent_off = db.Column(db.Integer()) currency = db.Column(db.String(8)) duration_in_months = db.Column(db.Integer()) max_redemptions = db.Column(db.Integer(), index=True) redeem_by = db.Column(AwareDateTime(), index=True) times_redeemed = db.Column(db.Integer(), index=True, nullable=False, default=0) valid = db.Column(db.Boolean(), nullable=False, server_default='1') def __init__(self, **kwargs): if self.code: self.code = self.code.upper() else: self.code = Coupon.random_coupon_code() # Call Flask-SQLAlchemy's constructor. super(Coupon, self).__init__(**kwargs) @hybrid_property def redeemable(self): """ Return coupons that are still redeemable. Coupons will become invalid once they run out on save. We want to explicitly do a date check to avoid having to hit Stripe's API to get back potentially valid codes. :return: SQLAlchemy query object """ is_redeemable = or_(self.redeem_by.is_(None), self.redeem_by >= datetime.datetime.now(pytz.utc)) return and_(self.valid, is_redeemable) @classmethod def search(cls, query): """ Search a resource by 1 or more fields. :param query: Search query :type query: str :return: SQLAlchemy filter """ if query == '': return '' #if not query: # return '' search_query = f'%{query}%' return or_(Coupon.code.ilike(search_query)) @classmethod def random_coupon_code(cls): """ Create a human readable random coupon code. :return: str """ charset = string.digits + string.ascii_uppercase charset = charset.replace('B', '').replace('I', '') charset = charset.replace('O', '').replace('S', '') charset = charset.replace('0', '').replace('1', '') random_chars = ''.join(choice(charset) for _ in range(14)) coupon_code = f'{random_chars[0:4]}-{random_chars[5:9]}-{random_chars[10:14]}' return coupon_code @classmethod def expire_old_coupons(cls, compare_datetime=None): """ Invalidate coupons that are past their redeem date. :param compare_datetime: Time to compare at :type compare_datetime: date :return: The result of updating the records """ if compare_datetime is None: compare_datetime = datetime.datetime.now(pytz.utc) Coupon.query.filter(Coupon.redeem_by <= compare_datetime) \ .update({Coupon.valid: not Coupon.valid}) return db.session.commit() @classmethod def create(cls, params): """ Return whether or not the coupon was created successfully. :return: bool """ payment_params = params payment_params['code'] = payment_params['code'].upper() if payment_params.get('amount_off'): payment_params['amount_off'] = \ dollars_to_cents(payment_params['amount_off']) PaymentCoupon.create(**payment_params) if 'id' in payment_params: payment_params['code'] = payment_params['id'] del payment_params['id'] if 'redeem_by' in payment_params: if payment_params.get('redeem_by') is not None: params['redeem_by'] = payment_params.get('redeem_by').replace( tzinfo=pytz.UTC) coupon = Coupon(**payment_params) db.session.add(coupon) db.session.commit() return True @classmethod def bulk_delete(cls, ids): """ Override the general bulk_delete method because we need to delete them one at a time while also deleting them on Stripe. :param ids: List of ids to be deleted :type ids: list :return: int """ delete_count = 0 for id in ids: coupon = Coupon.query.get(id) if coupon is None: continue stripe_response = PaymentCoupon.delete(coupon.code) if stripe_response.get('deleted'): coupon.delete() delete_count += 1 return delete_count @classmethod def find_by_code(cls, code): """ Find a coupon by its code. :param code: Coupon code to find :type code: str :return: Coupon instance """ formatted_code = code.upper() coupon = Coupon.query.filter(Coupon.redeemable, Coupon.code == formatted_code).first() return coupon def redeem(self): """ Update the redeem stats for this coupon. :return: Result of saving the record """ self.times_redeemed += 1 if self.max_redemptions: if self.times_redeemed >= self.max_redemptions: self.valid = False return db.session.commit() def apply_discount_to(self, amount): """ Apply the discount to an amount. :param amount: Amount in cents :type amount: int :return: int """ if self.amount_off: amount -= self.amount_off elif self.percent_off: amount *= (1 - (self.percent_off * 0.01)) return int(amount) def to_json(self): """ Return JSON fields to represent a coupon. :return: dict """ params = { 'duration': self.duration, 'duration_in_months': self.duration_in_months } if self.amount_off: params['amount_off'] = cents_to_dollars(self.amount_off) if self.percent_off: params['percent_off'] = self.percent_off return params
class Bet(ResourceMixin, db.Model): __tablename__ = 'bets' id = db.Column(db.Integer, primary_key=True) # Relationships. user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Bet details. guess = db.Column(db.Integer()) die_1 = db.Column(db.Integer()) die_2 = db.Column(db.Integer()) roll = db.Column(db.Integer()) wagered = db.Column(db.BigInteger()) payout = db.Column(db.Float()) net = db.Column(db.BigInteger()) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Bet, self).__init__(**kwargs) @classmethod def is_winner(cls, guess, roll): """ Determine if the result is a win or loss. :param guess: Dice guess :type guess: int :param roll: Dice roll :type roll: int :return: bool """ if guess == roll: return True return False @classmethod def determine_payout(cls, payout, is_winner): """ Determine the payout. :param payout: Dice guess :type payout: float :param is_winner: Was the bet won or lost :type is_winner: bool :return: int """ if is_winner: return payout return 1.0 @classmethod def calculate_net(cls, wagered, payout, is_winner): """ Calculate the net won or lost. :param wagered: Dice guess :type wagered: int :param payout: Dice roll :type payout: float :param is_winner: Was the bet won or lost :type is_winner: bool :return: int """ if is_winner: return int(wagered * payout) return -wagered def save_and_update_user(self, user): """ Commit the bet and update the user's information. :return: SQLAlchemy save result """ self.save() user.coins += self.net user.last_bet_on = tzware_datetime() return user.save() def to_json(self): """ Return JSON fields to represent a bet. :return: dict """ params = { 'guess': self.guess, 'die_1': self.die_1, 'die_2': self.die_2, 'roll': self.roll, 'wagered': self.wagered, 'payout': self.payout, 'net': self.net, 'is_winner': Bet.is_winner(self.guess, self.roll) } return params
class Invoice(ResourceMixin, db.Model): __tablename__ = 'invoices' id = db.Column(db.Integer, primary_key=True) # Relationships. user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Invoice details. plan = db.Column(db.String(128), index=True) receipt_number = db.Column(db.String(128), index=True) description = db.Column(db.String(128)) period_start_on = db.Column(db.Date) period_end_on = db.Column(db.Date) currency = db.Column(db.String(8)) tax = db.Column(db.Integer()) tax_percent = db.Column(db.Float()) total = db.Column(db.Integer()) # De-normalize the card details so we can render a user's history properly # even if they have no active subscription or changed cards at some point. brand = db.Column(db.String(32)) last4 = db.Column(db.Integer) exp_date = db.Column(db.Date, index=True) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Invoice, self).__init__(**kwargs) @classmethod def billing_history(cls, user=None): """ Return the billing history for a specific user. :param user: User whose billing history will be retrieved :type user: User instance :return: Invoices """ invoices = Invoice.query.filter(Invoice.user_id == user.id) \ .order_by(Invoice.created_on.desc()).limit(12) return invoices @classmethod def parse_from_event(cls, payload): """ Parse and return the invoice information that will get saved locally. API Documentation: https://stripe.com/docs/api/invoices/object :return: dict """ data = payload['data']['object'] plan_info = data['lines']['data'][0]['plan'] period_start_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['start']).date() period_end_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['end']).date() invoice = { 'payment_id': data['customer'], 'plan': plan_info['name'], 'receipt_number': data['receipt_number'], 'description': plan_info['statement_descriptor'], 'period_start_on': period_start_on, 'period_end_on': period_end_on, 'currency': data['currency'], 'tax': data['tax'], 'tax_percent': data['tax_percent'], 'total': data['total'] } return invoice @classmethod def parse_from_api(cls, payload): """ Parse and return the invoice information we are interested in. API Documentation: https://stripe.com/docs/api/invoices/object :return: dict """ plan_info = payload['lines']['data'][0]['plan'] date = datetime.datetime.utcfromtimestamp(payload['created']) plan = plan_info['id'].upper() invoice = { 'plan': plan, 'description': '{} MONTHLY'.format(plan), 'next_bill_on': date, 'amount_due': payload['amount_due'], 'interval': plan_info['interval'] } return invoice @classmethod def prepare_and_save(cls, parsed_event): """ Potentially save the invoice after argument the event fields. :param parsed_event: Event params to be saved :type parsed_event: dict :return: User instance """ # Avoid circular imports. from snakeeyes.blueprints.user.models import User # Only save the invoice if the user is valid at this point. id = parsed_event.get('payment_id') user = User.query.filter((User.payment_id == id)).first() if user and user.credit_card: parsed_event['user_id'] = user.id parsed_event['brand'] = user.credit_card.brand parsed_event['last4'] = user.credit_card.last4 parsed_event['exp_date'] = user.credit_card.exp_date del parsed_event['payment_id'] invoice = Invoice(**parsed_event) invoice.save() return user @classmethod def upcoming(cls, customer_id): """ Return the upcoming invoice item. :param customer_id: Stripe customer id :type customer_id: int :return: Stripe invoice object """ invoice = PaymentInvoice.upcoming(customer_id) return Invoice.parse_from_api(invoice)
class Invoice(db.Model): __tablename__ = 'invoices' # Unique ID id = db.Column(db.Integer, primary_key=True) # Relationships user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Invoices details plan = db.Column(db.String(128), index=True) receipt_number = db.Column(db.String(128), index=True) description = db.Column(db.String(128)) period_start_on = db.Column(db.Date()) period_end_on = db.Column(db.Date()) currency = db.Column(db.String(12)) tax = db.Column(db.Integer()) tax_percent = db.Column(db.Float()) total = db.Column(db.Integer()) # De-normalize the card details so we can render a user's history properly even if they have no active subscription or change cards at some point brand = db.Column(db.String(32)) last4 = db.Column(db.Integer) exp_date = db.Column(db.Date, index=True) # Invoice runtime created_on = db.Column(db.DateTime(), default=datetime.datetime.utcnow) def __init__(self, **kwargs): super(Invoice, self).__init__(**kwargs) @classmethod def billing_history(cls, user=None): """ Return a billing history for a particular user """ invoices = cls.query.filter(cls.user_id == user.id).order_by( cls.created_on.desc()).limit(12) return invoices @classmethod def parse_from_event(): """ Parse and return the invoice information that will be saved. :return : dict """ data = payload['data']['object'] plan_info = data['lines']['data'][0]['plan'] period_start_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['start']).date() period_end_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['end']).date() invoice = { 'payment_id': data['customer'], 'plan': plan_info['name'], 'receipt_number': data['receipt_number'], 'description': plan_info['statement_descriptor'], 'period_start_on': period_start_on, 'period_end_on': period_end_on, 'currency': data['currency'], 'tax': data['tax'], 'tax_percent': data['tax_percent'], 'total': data['total'] } return invoice @classmethod def parse_from_api(cls, payload): """ Parse and return invoice information we are interested in. """ plan_info = payload['lines']['data'][0]['plan'] date = datetime.datetime.utcfromtimestamp(payload['date']) invoice = { 'plan': plan_info['name'], 'description': plan_info['statement_descriptor'], 'next_bill_on': date, 'amount_due': payload['amount_due'], 'interval': plan_info['interval'] } return invoice @classmethod def prepare_and_save(cls, parsed_event): """ Potentially save the invoice after argument the event fileds. :param parsed_event: Event params to be saved :type parsed_event: dict :return: User instance """ # Avoid circular import from snakeeyes.blueprints.user.models import User id = parsed_event.get('payment_id') user = User.query.filter((User.payment_id == id)).first() if user and user.credit_card: parsed_event['user_id'] = user.id parsed_event['brand'] = user.credit_card.brand parsed_event['last4'] = user.credit_card.last4 parsed_event['exp_date'] = user.credit_card.exp_date del parsed_event['payment_id'] invoice = Invoice(**parsed_event) db.session.add(invoice) db.session.commit() return user @classmethod def upcoming(cls, customer_id): """ Return the upcoming invoice """ invoice = PaymentInvoice.upcoming(customer_id=customer_id) return Invoice.parse_from_api(invoice)
class Invoice(ResourceMixin, db.Model): __tablename__ = 'invoices' id = db.Column(db.Integer, primary_key=True) # Relationships. user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate='CASCADE', ondelete='CASCADE'), index=True, nullable=False) # Invoice details. plan = db.Column(db.String(128), index=True) receipt_number = db.Column(db.String(128), index=True) description = db.Column(db.String(128)) period_start_on = db.Column(db.Date) period_end_on = db.Column(db.Date) currency = db.Column(db.String(8)) tax = db.Column(db.Integer()) tax_percent = db.Column(db.Float()) total = db.Column(db.Integer()) # De-normalize the card details so we can render a user's history properly # even if they have no active subscription or changed cards at some point. brand = db.Column(db.String(32)) last4 = db.Column(db.Integer) exp_date = db.Column(db.Date, index=True) def __init__(self, **kwargs): # Call Flask-SQLAlchemy's constructor. super(Invoice, self).__init__(**kwargs) @classmethod def search(cls, query): """ Search a resource by 1 or more fields. :param query: Search query :type query: str :return: SQLAlchemy filter """ from snakeeyes.blueprints.user.models import User if not query: return '' search_query = '%{0}%'.format(query) search_chain = (User.email.ilike(search_query), User.username.ilike(search_query)) return or_(*search_chain) @classmethod def billing_history(cls, user=None): """ Return the billing history for a specific user. :param user: User whose billing history will be retrieved :type user: User instance :return: Invoices """ invoices = Invoice.query.filter(Invoice.user_id == user.id) \ .order_by(Invoice.created_on.desc()).limit(12) return invoices @classmethod def parse_from_event(cls, payload): """ Parse and return the invoice information that will get saved locally. :return: dict """ data = payload['data']['object'] plan_info = data['lines']['data'][0]['plan'] period_start_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['start']).date() period_end_on = datetime.datetime.utcfromtimestamp( data['lines']['data'][0]['period']['end']).date() invoice = { 'payment_id': data['customer'], 'plan': plan_info['name'], 'receipt_number': data['receipt_number'], 'description': plan_info['statement_descriptor'], 'period_start_on': period_start_on, 'period_end_on': period_end_on, 'currency': data['currency'], 'tax': data['tax'], 'tax_percent': data['tax_percent'], 'total': data['total'] } return invoice @classmethod def parse_from_api(cls, payload): """ Parse and return the invoice information we are interested in. :return: dict """ plan_info = payload['lines']['data'][0]['plan'] date = datetime.datetime.utcfromtimestamp(payload['date']) invoice = { 'plan': plan_info['name'], 'description': plan_info['statement_descriptor'], 'next_bill_on': date, 'amount_due': payload['amount_due'], 'interval': plan_info['interval'] } return invoice @classmethod def prepare_and_save(cls, parsed_event): """ Potentially save the invoice after argument the event fields. :param parsed_event: Event params to be saved :type parsed_event: dict :return: User instance """ # Avoid circular imports. from snakeeyes.blueprints.user.models import User # Only save the invoice if the user is valid at this point. id = parsed_event.get('payment_id') user = User.query.filter((User.payment_id == id)).first() if user and user.credit_card: parsed_event['user_id'] = user.id parsed_event['brand'] = user.credit_card.brand parsed_event['last4'] = user.credit_card.last4 parsed_event['exp_date'] = user.credit_card.exp_date return user @classmethod def upcoming(cls, customer_id): """ Return the upcoming invoice item. :param customer_id: Stripe customer id :type customer_id: int :return: Stripe invoice object """ invoice = PaymentInvoice.upcoming(customer_id) return Invoice.parse_from_api(invoice) def create(self, user=None, currency=None, amount=None, coins=None, coupon=None, token=None): """ Create an invoice item. :param user: User to apply the subscription to :type user: User instance :param amount: Stripe currency :type amount: str :param amount: Amount in cents :type amount: int :param coins: Amount of coins :type coins: int :param coupon: Coupon code to apply :type coupon: str :param token: Token returned by JavaScript :type token: str :return: bool """ if token is None: return False customer = PaymentCustomer.create(token=token, email=user.email) if coupon: self.coupon = coupon.upper() coupon = Coupon.query.filter(Coupon.code == self.coupon).first() amount = coupon.apply_discount_to(amount) charge = PaymentCharge.create(customer.id, currency, amount) # Redeem the coupon. if coupon: coupon.redeem() # Add the coins to the user. user.coins += coins # Create the invoice item. period_on = datetime.datetime.utcfromtimestamp(charge.get('created')) card_params = CreditCard.extract_card_params(customer) self.user_id = user.id self.plan = '—' self.receipt_number = charge.get('receipt_number') self.description = charge.get('statement_descriptor') self.period_start_on = period_on self.period_end_on = period_on self.currency = charge.get('currency') self.tax = None self.tax_percent = None self.total = charge.get('amount') self.brand = card_params.get('brand') self.last4 = card_params.get('last4') self.exp_date = card_params.get('exp_date') db.session.add(user) db.session.add(self) db.session.commit() return True
class Coupon(db.Model): DURATION = OrderedDict([('once', 'Once'), ('repeating', 'Repeating'), ('forever', 'Forever')]) __tablename__ = 'coupons' id = db.Column(db.Integer, primary_key=True) # Coupon details. code = db.Column(db.String(128), index=True, unique=True) duration = db.Column(db.Enum(*DURATION, name='duration_types'), index=True, nullable=False, default='forever') amount_off = db.Column(db.Integer()) percent_off = db.Column(db.Integer()) currency = db.Column(db.String(8)) duration_in_months = db.Column(db.Integer()) max_redemptions = db.Column(db.Integer(), index=True) redeem_by = db.Column(db.DateTime(), default=datetime.datetime.utcnow) times_redeemed = db.Column(db.Integer(), index=True, nullable=False, default=0) valid = db.Column(db.Boolean(), nullable=False, server_default='1') created_on = db.Column(db.DateTime(), default=datetime.datetime.utcnow) def __init__(self, **kwargs): if self.code: self.code = code.upper() else: self.code = Coupon.random_coupon_code() super(Coupon, self).__init__(**kwargs) @hybrid_property def redeemable(self): """ Returns coupon code that are still valid. """ is_redeemable = or_(self.redeem_by.is_(None), self.redeem_by >= datetime.datetime.now) return and_(self.valid, is_redeemable) @classmethod def search(cls, query): """ search resources by one or more filed """ if not query: return '' search_query = '%{0}%'.format(query) return or_(cls.code.ilike(search_query)) # search_query = '%{0}%'.format(query) # search_chain = (User.email.ilike(search_query), # User.username.ilike(search_query)) # return or_(*search_chain) @classmethod def random_coupon_code(cls): """ Create a human readable random code. """ charset = string.digits + string.ascii_uppercase charset = charset.replace('B', '').replace('I', '') charset = charset.replace('O', '').replace('S', '') charset = charset.replace('0', '').replace('1', '') random_chars = ''.join(choice(charset) for _ in range(0, 14)) coupon_code = '{}-{}-{}'.format(random_chars[0:4], random_chars[5:9], random_chars[10:14]) return coupon_code @classmethod def expire_old_coupons(cls, compare_date=None): """ Invalidate coupon that has pass thier expire date """ if compare_date is None: compare_date = datetime.datetime.now(pytz.utc) cls.query.filter(cls.redeem_by < +compare_date).update( {cls.valid: not cls.valid}) return db.session.commit() @classmethod def create(cls, params): """ Create a coupon code and return true is successful """ payment_params = params payment_params['code'] = payment_params['code'].upper() if payment_params.get('amount_off'): payment_params['amount_off'] = dollars_to_cents( payment_params['amount_off']) PaymentCoupon.create(**payment_params) # Stripe will save the coupon to id field on stripe while on our database, we want it to save on code field if 'id' in payment_params: payment_params['code'] = payment_params['id'] del payment_params['id'] # Converting th eunix time to day stim stamp that is acceptable by the databse if 'redeem_by' in payment_params: if payment_params.get('redeem_by') is not None: params['redeem_by'] = payment_params.get('redeem_by').replace( datetime.datetime.utcnow) coupon = Coupon(**payment_params) db.session.add(coupon) db.session.commit() return True @classmethod def bulk_delete(cls, ids): """ Override the general bulk delete method to delete coupon from application and stripe """ delete_count = 0 for id in ids: coupon = Coupon.query.get(id) print(coupon) if coupon is None: continue # Delete on stripe stripe_delete = PaymentCoupon.delete(coupon) # If successful, delete it locally if stripe_delete.get('deleted'): db.session.delete(coupon) delete_count += 1 return delete_count @classmethod def find_by_code(cls, code): """ Find a coupon by its code """ formatted_code = code.upper() coupon = Coupon.query.filter(Coupon.redeemable, Coupon.code == formatted_code).first() return coupon def redeem(self): """ Update redeem stats for this coupon """ self.times_redeemed += 1 if self.max_redemptions: if self.times_redeemed >= self.max_redemptions: self.valid = False return db.session.commit() def to_json(self): """ Retun JSON fields to represent a coupon """ params = { 'duration': self.duration, 'duration_in_months': self.duration_in_months } if self.amount_off: params['amount_off'] = cent_to_dollar(self.amount_off) if self.percent_off: params['percent_off'] = self.percent_off return params @classmethod def sort_by(cls, field, direction): """This help to sort the user base on the field column and direction. """ if field not in cls.__table__.columns: field = "created_on" if direction not in ('asc', 'desc'): direction = 'asc' return field, direction @classmethod def get_bulk_action_ids(cls, scope, ids, omit_ids=[], query=''): """Determine which id to be deleted.""" omit_ids = list(map(str, omit_ids)) if scope == 'all_search_result': ids = cls.query.with_entities(cls.id).filter(cls.search(query)) ids = [str(item[0]) for item in ids] if omit_ids: ids = [id for id in ids if id not in omit_ids] return ids