class SubscriptionTable(BaseTable): """ Houses storing information about a customer account. """ __tablename__ = 'braintree_subscription' braintree_sub_id = db.Column(db.String, nullable=False) plan_id = db.Column(db.String, nullable=False, default='Basic0x01') date_started = db.Column(db.DateTime, nullable=False) date_ended = db.Column(db.DateTime, nullable=True) user_id = db.Column( db.String(36), db.ForeignKey('users.public_id'), nullable=False, ) braintree_customer_id = db.Column( db.String(36), db.ForeignKey('braintree_customer.public_id'), nullable=False, ) # House keeping stuff. is_deleted = db.Column(db.BOOLEAN, nullable=False, default=False) def __init__(self, bt_customer_id, user_id, sub_id, *, plan_id='Basic0x01'): super().__init__() self.braintree_customer_id = bt_customer_id self.braintree_sub_id = sub_id self.user_id = user_id self.plan_id = plan_id self.date_started = datetime.utcnow() def __repr__(self): return 'User: {0} -- PlanID: {1} -- CustomerID: {2}'.format( self.user_id, self.plan_id, self.braintree_customer_id, )
class ScheduleTable(BaseTable): """ Houses the schedules of users. """ __tablename__ = 'schedules' # Foreign keys. user_id = db.Column( db.String(36), db.ForeignKey('users.public_id'), nullable=False ) # Actual schedule stuff. utc_duration = db.Column(TSRANGE, nullable=False) local_duration = db.Column(TSTZRANGE, nullable=False) day_number = db.Column(db.SmallInteger, nullable=False) month_number = db.Column(db.SmallInteger, nullable=False) # tz stuff local_tz = db.Column(db.String, nullable=False) ExcludeConstraint(('utc_duration', '&&')) ExcludeConstraint(('local_duration', '&&')) @property def local_tz_open(self): return self.local_duration.lower @property def local_tz_end(self): return self.local_duration.upper @property def utc_open(self): return self.utc_duration.lower @property def utc_end(self): return self.utc_duration.upper def __init__(self, open_date, end_date, user_id, local_tz): super().__init__() self.utc_duration = DateTimeRange(open_date, end_date) self.local_duration = DateTimeTZRange( self._localize(self.utc_duration.lower, local_tz), self._localize(self.utc_duration.upper, local_tz), ) self.day_number = open_date.day self.month_number = open_date.month self.local_tz = local_tz self.user_id = str(user_id) def __repr__(self): return 'Open: {0} -> End: {1} -- For User: {2}'.format( self.utc_open, self.utc_end, self.user_id )
class PaymentTable(BaseTable): """ Houses storing information pertinent to the master merchant. """ __tablename__ = 'payments' submerchant_id = db.Column( db.Integer, db.ForeignKey('braintree_sub_merchant.id') ) event_id = db.Column( db.Integer, db.ForeignKey('events.id') ) base_amount = db.Column(db.DECIMAL, nullable=False) service_fee = db.Column(db.DECIMAL, nullable=False) total_price = db.Column(db.DECIMAL, nullable=False) def __init__(self, amount, service_fee, submerchant, event): super().__init__() self.base_amount = amount self.service_fee = service_fee self.total_price = event.total_price self.submerchant_id = submerchant.public_id self.event_id = event.public_id def __repr__(self): return 'Transaction {0} for submerchant {1} and event {2}'.format( self.public_id, self.submerchant_id, self.event_id )
class CustomerTable(BaseTable): """ Houses storing information about a customer account. """ __tablename__ = 'braintree_customer' braintree_customer_id = db.Column(db.String, nullable=False, unique=True) credit_card_token = db.Column(db.String, nullable=False, unique=True) first_name = db.Column(db.String(64), nullable=False) last_name = db.Column(db.String(64), nullable=False) user_id = db.Column( db.String(36), db.ForeignKey('users.public_id'), nullable=False, ) # House keeping stuff. is_default = db.Column(db.BOOLEAN, nullable=False, default=False) is_deleted = db.Column(db.BOOLEAN, nullable=False, default=False) def __init__(self, bt_customer_id, cc_token, first_name, last_name, user_id, *, is_default=False): super().__init__() self.braintree_customer_id = bt_customer_id self.credit_card_token = cc_token self.first_name = first_name self.last_name = last_name self.user_id = user_id self.is_default = is_default def __repr__(self): return 'User: {0} -- Default: {1} -- CustomerID: {2}'.format( self.user_id, self.is_default, self.braintree_customer_id, )
class SubmerchantTable(BaseTable): """ Houses storing information relevant for submerchants. """ __tablename__ = 'braintree_sub_merchant' master_merchant_id = db.Column( db.Integer, db.ForeignKey('braintree_master_merchant.id')) user_id = db.Column( db.String(36), db.ForeignKey('users.public_id'), nullable=False, ) # House keeping stuff. is_deleted = db.Column(db.BOOLEAN, nullable=False, default=False) is_approved = db.Column(db.BOOLEAN, nullable=False, default=False) is_rejected = db.Column(db.Boolean, nullable=False, default=False) # Actual shit being put into braintree upon submerchant account creation. braintree_account_id = db.Column(db.String(24), nullable=False) service_fee_percent = db.Column(db.DECIMAL, nullable=False, default=.025) # Individual first_name = db.Column(db.String(64), nullable=False) last_name = db.Column(db.String(64), nullable=False) email = db.Column(db.String(64), nullable=False) date_of_birth = db.Column(db.DateTime, nullable=False) address_street_address = db.Column(db.String(128), nullable=False) address_locality = db.Column(db.String(32), nullable=False) address_region = db.Column(db.String(32), nullable=False) address_zip = db.Column(db.String(12), nullable=False) # Business stuff. Only required if user is registering as a business register_as_business = db.Column(db.Boolean, nullable=False, default=False) legal_name = db.Column(db.String(64)) # NOTE: We do NOT store the tax_id. dba_name = db.Column(db.String(64)) bus_address_street_address = db.Column(db.String(128)) bus_address_locality = db.Column(db.String(32)) bus_address_region = db.Column(db.String(32)) bus_address_zip = db.Column(db.String(12)) def __init__(self, user_id, account_id, first_name, last_name, email, date_of_birth, address_street_address, address_locality, address_region, address_zip, register_as_business=False, legal_name=None, dba_name=None, bus_address_street_address=None, bus_address_locality=None, bus_address_region=None, bus_address_zip=None): super().__init__() self.user_id = user_id self.braintree_account_id = account_id self.first_name = first_name self.last_name = last_name self.email = email self.date_of_birth = date_of_birth self.address_street_address = address_street_address self.address_locality = address_locality self.address_region = address_region self.address_zip = address_zip if register_as_business: self.register_as_business = register_as_business self.legal_name = legal_name self.dba_name = dba_name self.bus_address_street_address = bus_address_street_address self.bus_address_locality = bus_address_locality self.bus_address_region = bus_address_region self.bus_address_zip = bus_address_zip def __repr__(self): return 'Account ID: {0} -- User Public ID: {1}'.format( self.braintree_account_id, self.user_id)
class AddressTable(BaseTable): """ Houses the schedules of contact form submissions. """ __tablename__ = 'addresses' first_name = db.Column(db.String, nullable=False) last_name = db.Column(db.String, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) street_address = db.Column(db.String, nullable=False) extended_address = db.Column(db.String, nullable=True) # City locality = db.Column(db.String, nullable=False) # State region = db.Column(db.String, nullable=False) postal_code = db.Column(db.String, nullable=False) country_code_alpha2 = db.Column(db.String(2), nullable=False) is_default = db.Column(db.Boolean, nullable=False, default=False) is_deleted = db.Column(db.Boolean, nullable=False, default=False) # Indexes db.Index('idx_default_addresses', is_default) db.Index('idx_locality', locality) db.Index('idx_region', region) db.Index('idx_postal_code', postal_code) db.Index('idx_country_code', country_code_alpha2) def __init__(self, user_id, first_name, last_name, street_address, locality, region, postal_code, country_code_alpha2, is_default=False, extended_address=None): super().__init__() try: self.user_id = UserDAO().get(user_id).id except DAOException as e: logging.error( 'Failed to get user by pub ID {0} w/ exc of {1}'.format( user_id, e, )) raise TableException('Failed to find requested user.') except AttributeError as e: logging.error('Requested user ({0}) does not exist when ' 'creating new address.'.format(user_id, )) raise TableException(e) self.first_name = first_name self.last_name = last_name self.street_address = street_address self.locality = locality self.region = region self.postal_code = postal_code self.country_code_alpha2 = country_code_alpha2 self.is_default = is_default if extended_address is not None and extended_address != '': self.extended_address = extended_address def __repr__(self): return """ First name: {0} Last name: {1} Default: {2} Locality: {3} Region: {4} Country2: {5} """.format( self.first_name, self.last_name, self.is_default, self.locality, self.region, self.country_code_alpha2, )
class EventTable(BaseTable): """ Houses the schedules of users. """ __tablename__ = 'events' # Foreign keys. scheduling_user_id = db.Column(db.String(36), db.ForeignKey('users.public_id'), nullable=False, index=True) scheduled_user_id = db.Column(db.String(36), db.ForeignKey('users.public_id'), nullable=False, index=True) utc_duration = db.Column(TSTZRANGE, nullable=False) scheduled_tz_duration = db.Column(TSTZRANGE, nullable=False) scheduling_tz_duration = db.Column(TSTZRANGE, nullable=False) day_number = db.Column(db.SmallInteger, nullable=False) month_number = db.Column(db.SmallInteger, nullable=False) duration = db.Column(db.Integer, nullable=False) total_price = db.Column(db.DECIMAL, nullable=False) service_fee = db.Column(db.DECIMAL, nullable=False) # TODO(ian): Mark this with a payment transaction ID. transaction_id = db.Column(db.Integer, nullable=True) notes = db.Column(db.String(512), nullable=True) ExcludeConstraint(('utc_duration', '&&')) ExcludeConstraint(('scheduled_tz_duration', '&&')) ExcludeConstraint(('scheduling_tz_duration', '&&')) @property def utc_start(self): return self.utc_duration.lower @property def utc_end(self): return self.utc_duration.upper @property def scheduled_tz_start(self): return self.scheduled_tz_duration.lower @property def scheduled_tz_end(self): return self.scheduled_tz_duration.upper @property def scheduling_tz_start(self): return self.scheduling_tz_duration.lower @property def scheduling_tz_end(self): return self.scheduling_tz_duration.upper def __init__(self, start_time, end_time, scheduling, scheduled, notes=None): super().__init__() scheduling_user_info = db.session.query(User).filter_by( public_id=scheduling).first() scheduled_user_info = db.session.query(User).filter_by( public_id=scheduled).first() self.utc_duration = DateTimeTZRange(start_time, end_time) self._set_duration_for_user(scheduling_user_info, is_scheduling=True) self._set_duration_for_user(scheduled_user_info, is_scheduling=False) if scheduling_user_info is None or scheduled_user_info is None: raise ModelException('Invalid requested users.') self.scheduling_user_id = str(scheduling) self.scheduled_user_id = str(scheduled) self.day_number = start_time.day self.month_number = self.utc_duration.lower.month self.duration = ((end_time - start_time).seconds // 60) if self.duration != 60: if self.duration >= 60 and self.duration % 60 == 0: pass else: self.duration %= 60 self.total_price = self.calculate_total_price( self.duration, scheduled_user_info, ) submerchant_info = db.session.query(SubmerchantTable).filter_by( user_id=scheduled).first() self.service_fee = self.calculate_service_fee(submerchant_info) if notes is not None: self.notes = notes def __repr__(self): return 'Start: {0} - End: {1} - Duration (minutes): {2} - Price {3}'.\ format( self.utc_start, self.utc_end, self.duration, self.total_price ) def calculate_total_price(self, duration, scheduled_user): """ Calculates the total price for the event. :param int duration: :param UserTable scheduled_user: The user which si being scheduled. :rtype: float :return: The total price for the duration. """ if scheduled_user is None: raise TableException("Invalid user to schedule.") if scheduled_user.is_premium: if duration not in [5, 15, 30, 45, 60]: if duration >= 60 and duration % 60 == 0: pass else: raise TableException( "Invalid event duration. Premium users can only accept" " durations of 5, 15, 30, or 60 minutes.") else: if duration not in [60]: if duration >= 60 and duration % 60 == 0: pass else: raise TableException( "Invalid event duration. Non-premium users can only " "accept durations of 60 minutes.") if scheduled_user.is_premium: price_lookup = { 5: scheduled_user.five_min_price, 15: scheduled_user.fifteen_min_price, 30: scheduled_user.thirty_min_price, 65: scheduled_user.sixty_min_price, } return price_lookup[duration] + decimal.Decimal(price_lookup[ duration]) * decimal.Decimal(0.026) + decimal.Decimal(0.2) else: if scheduled_user.sixty_min_price is None: logging.error('Attempted to create event for user {0}' ', but failed because no sixty_min_price'.format( scheduled_user.public_id, )) raise TableException( 'Invalid user to be scheduled. User has no price set' 'for 60 minutes.') return scheduled_user.sixty_min_price + decimal.Decimal( scheduled_user.sixty_min_price) * decimal.Decimal( 0.026) + decimal.Decimal(0.2) def calculate_service_fee(self, submerchant): return self.total_price * submerchant.service_fee_percent def _set_duration_for_user(self, user_info, is_scheduling=True): localized_start = self._localize(self.utc_duration.lower, user_info.local_tz) localized_end = self._localize(self.utc_duration.upper, user_info.local_tz) if is_scheduling: self.scheduling_tz_duration = DateTimeTZRange( localized_start, localized_end, ) else: self.scheduled_tz_duration = DateTimeTZRange( localized_start, localized_end, )