class Cashin(BankingTransaction): __tablename__ = 'cashin' __mapper_args__ = { 'polymorphic_identity': __tablename__, } id = Field(Integer(), ForeignKey(BankingTransaction.id), primary_key=True) # TODO: Add some salt to prevent man in the middle (Extra field to send on creation and check on verification) transaction_id = Field(Unicode())
class BankAccount(BankingId): __tablename__ = 'bank_account' __mapper_args__ = { 'polymorphic_identity': __tablename__, } id = Field(Integer(), ForeignKey(BankingId.id), primary_key=True) fiat_symbol = Field(Unicode(10), ForeignKey('fiat.symbol')) iban = Field(Unicode(50), pattern=r'^[A-Z]{2}[A-Z0-9]{4,10}[0-9]{5,40}$') # TODO: Should be unique if is_valid owner = Field(Unicode(100)) bic = Field(Unicode(20), pattern=r'^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$', nullable=True)
class BankCard(BankingId): __tablename__ = 'bank_card' __mapper_args__ = { 'polymorphic_identity': __tablename__, } id = Field(Integer(), ForeignKey(BankingId.id), primary_key=True) fiat_symbol = Field(Unicode(10), ForeignKey('fiat.symbol')) pan = Field(Unicode(30), pattern=r'^([0-9]{4}-){3}[0-9]{4}$') # TODO: Should be unique if is_valid holder = Field(Unicode(100)) expiration = Field(Unicode(7), pattern=r'^[0-1]{1}/[0-9]{2,4}$', nullable=True, protected=True) # mm/yy or mm/yyyy
class Cashout(BankingTransaction): __tablename__ = 'cashout' __mapper_args__ = { 'polymorphic_identity': __tablename__, } id = Field(Integer(), ForeignKey(BankingTransaction.id), primary_key=True)
class ActivationMixin: activated_at = Field(DateTime, nullable=True, json='activatedAt', readonly=True, protected=True) @hybrid_property def is_active(self): return self.activated_at is not None @is_active.setter def is_active(self, value): self.activated_at = datetime.now() if value else None @is_active.expression def is_active(self): # noinspection PyUnresolvedReferences return self.activated_at.isnot(None) @classmethod def filter_activated(cls, query=None): # noinspection PyUnresolvedReferences return (query or cls.query).filter(cls.is_active) @classmethod def import_value(cls, column, v): # noinspection PyUnresolvedReferences if column.key == cls.is_active.key and not isinstance(v, bool): return str(v).lower() == 'true' # noinspection PyUnresolvedReferences return super().import_value(column, v)
class OrderableMixin: order = Field("order", Integer, default=0, nullable=False) __mapper_args__ = dict(order_by=order) @classmethod def apply_default_sort(cls, query=None): return (query or cls.query).order_by(cls.order)
class BankingId(TimestampMixin, DeclarativeBase): __tablename__ = 'banking_id' id = Field(Integer(), primary_key=True) client_id = Field(Integer(), ForeignKey('client.id')) is_verified = Field(Boolean(), default=False) error = Field(Unicode(), nullable=True) client = relationship('Client', lazy='select', protected=True) type = Field(Enum('bank_account', 'bank_card', name='banking_id_type')) __mapper_args__ = { 'polymorphic_identity': __tablename__, 'polymorphic_on': type }
class Ticket(ModifiedMixin, PaginationMixin, DeclarativeBase): __tablename__ = 'ticket' id = Field(Integer(), primary_key=True) title = Field(Unicode()) member_id = Field(Integer(), ForeignKey('member.id')) department_id = Field(Integer(), ForeignKey('ticket_department.id')) closed_at = Field(DateTime(), nullable=True) department = relationship('TicketDepartment', uselist=False) @hybrid_property def is_closed(self): return self.closed_at is not None @is_closed.setter def is_closed(self, value): self.closed_at = datetime.now() if value else None @is_closed.expression def is_closed(self): # noinspection PyUnresolvedReferences return self.closed_at.isnot(None) @classmethod def import_value(cls, column, v): # noinspection PyUnresolvedReferences if column.key == cls.is_closed.key and not isinstance(v, bool): return str(v).lower() == 'true' # noinspection PyUnresolvedReferences return super().import_value(column, v) def to_dict(self): result = super().to_dict() first_message = DBSession.query(TicketMessage) \ .filter(TicketMessage.ticket_id == self.id) \ .order_by(TicketMessage.created_at) \ .first() result['firstMessage'] = first_message.to_dict() if (first_message is not None) else None return result
class TicketMessage(ModifiedMixin, PaginationMixin, DeclarativeBase): __tablename__ = 'ticket_message' id = Field(Integer(), primary_key=True) ticket_id = Field(Integer(), ForeignKey(Ticket.id)) member_id = Field(Integer(), ForeignKey('member.id')) text = Field(Unicode()) is_answer = Field(Boolean(), default=False) _attachment = Field(TicketAttachment.as_mutable(JSON), nullable=True, protected=True) ticket = relationship(Ticket, lazy='select', uselist=False, protected=True) @property def attachment(self): return self._attachment.locate() if self._attachment else None @attachment.setter def attachment(self, value): if value is not None: self._attachment = TicketAttachment.create_from(value) else: self._attachment = None def to_dict(self): result = super().to_dict() result['attachment'] = self.attachment return result
class BankingTransaction(ModifiedMixin, OrderingMixin, FilteringMixin, PaginationMixin, DeclarativeBase): __tablename__ = 'banking_transaction' id = Field(Integer(), primary_key=True) fiat_symbol = Field(Unicode(10), ForeignKey('fiat.symbol')) member_id = Field(Integer(), ForeignKey('member.id')) # FIXME: Change the name to `member_id` payment_gateway_name = Field(Unicode(30), ForeignKey('payment_gateway.name')) amount = Field(DECIMAL(18, 8)) # Value without commission commission = Field(DECIMAL(18, 8), default=Decimal(0)) error = Field(Unicode(), nullable=True) reference_id = Field(Unicode(260), nullable=True) banking_id_id = Field(Integer(), ForeignKey('banking_id.id'), protected=True) payment_gateway = relationship('PaymentGateway') banking_id = relationship('BankingId') type = Field(Unicode(50)) member = relationship('Member') __mapper_args__ = { 'polymorphic_identity': __tablename__, 'polymorphic_on': type }
class ModifiedMixin(TimestampMixin): modified_at = Field(DateTime, nullable=True, json='modifiedAt', readonly=True) @property def last_modification_time(self): return self.modified_at or self.created_at # FIXME: rename it to before_update # noinspection PyUnusedLocal @staticmethod def on_update(mapper, connection, target): target.modified_at = datetime.now() @classmethod def __declare_last__(cls): event.listen(cls, 'before_update', cls.on_update)
class SoftDeleteMixin: removed_at = Field(DateTime, nullable=True, json='removedAt', readonly=True) def assert_is_not_deleted(self): if self.is_deleted: raise ValueError('Object is already deleted.') def assert_is_deleted(self): if not self.is_deleted: raise ValueError('Object is not deleted.') @property def is_deleted(self): return self.removed_at is not None def soft_delete(self, ignore_errors=False): if not ignore_errors: self.assert_is_not_deleted() self.removed_at = datetime.now() def soft_undelete(self, ignore_errors=False): if not ignore_errors: self.assert_is_deleted() self.removed_at = None @staticmethod def on_delete(mapper, connection, target): raise HttpConflict('Cannot remove this object: %s' % target) @classmethod def __declare_last__(cls): event.listen(cls, 'before_delete', cls.on_delete) @classmethod def filter_deleted(cls, query=None): # noinspection PyUnresolvedReferences return (query or cls.query).filter(cls.removed_at.isnot(None)) @classmethod def exclude_deleted(cls, query=None): # noinspection PyUnresolvedReferences return (query or cls.query).filter(cls.removed_at.is_(None))
class Market(OrderingMixin, FilteringMixin, DeclarativeBase): """ Currency pairs are sometimes then written by concatenating the ISO currency codes (ISO 4217) of the base currency and the counter currency, separating them with a slash character. Often the slash character is omitted, alternatively the slash may be replaced by and etc. A widely traded currency pair is the relation of the euro against the US dollar, designated as EUR/USD. The quotation EUR/USD 1.2500 means that one euro is exchanged for 1.2500 US dollars. Here, EUR is the base currency and USD is the quote currency(counter currency). This means that 1 Euro can be exchangeable to 1.25 US Dollars. Reference: https://en.wikipedia.org/wiki/Currency_pair """ __tablename__ = 'market' name = Field(Unicode(20), pattern=r'^[A-Z0-9]{1,10}_[A-Z0-9]{1,10}$', primary_key=True) # e.g. btc_usd base_currency_symbol = Field(Unicode(), ForeignKey('currency.symbol'), protected=True) quote_currency_symbol = Field(Unicode(), ForeignKey('currency.symbol'), protected=True) base_currency = relationship('Currency', foreign_keys=[base_currency_symbol]) quote_currency = relationship('Currency', foreign_keys=[quote_currency_symbol]) buy_amount_min = Field(DECIMAL(18, 8), default=Decimal('0.00000100')) buy_amount_max = Field(DECIMAL(18, 8), default=Decimal('100.00000000')) sell_amount_min = Field(DECIMAL(18, 8), default=Decimal('0.00000100')) sell_amount_max = Field(DECIMAL(18, 8), default=Decimal('100.00000000')) taker_commission_rate = Field(Unicode(10), default='0.4') maker_commission_rate = Field(Unicode(10), default='0.1') # taker_static_commission = Field(BigInteger(), default=0) # taker_permille_commission = Field(Integer(), default=0) # taker_max_commission = Field(BigInteger(), default=0) # maker_static_commission = Field(BigInteger(), default=0) # maker_permille_commission = Field(Integer(), default=0) # maker_max_commission = Field(BigInteger(), default=0) def to_dict(self): result = super().to_dict() # TODO: Get the current user's wallet_tier_policy about this currency # result['tirePolicy'] = {} result['buyAmountMin'] = self.base_currency.normalized_to_output( self.buy_amount_min) result['buyAmountMax'] = self.base_currency.normalized_to_output( self.buy_amount_max) result['sellAmountMin'] = self.base_currency.normalized_to_output( self.sell_amount_min) result['sellAmountMax'] = self.base_currency.normalized_to_output( self.sell_amount_max) return result def get_last_price(self): try: return Decimal(stexchange_client.market_last(self.name)) except StexchangeException as e: raise stexchange_http_exception_handler(e) def validate_ranges(self, type_, total_amount, price=None): # TODO: Review and rewrite the price threshold validator # threshold = Decimal(settings.trader.price_threshold_permille) # price_rate = Decimal(1000 * (price or self.get_last_price()) / self.get_last_price()) - Decimal(1000) if type_ == 'buy': # if price_rate > threshold: # raise HttpBadRequest('Price not in valid range', 'price-not-in-range') if total_amount < self.buy_amount_min or \ (self.buy_amount_max != 0 and total_amount > self.buy_amount_max): raise HttpBadRequest('Amount not in range', 'amount-not-in-range') elif type_ == 'sell': # if price_rate < -threshold: # raise HttpBadRequest('Price not in valid range', 'price-not-in-range') if total_amount < self.sell_amount_min or \ (self.sell_amount_max != 0 and total_amount > self.sell_amount_max): raise HttpBadRequest('Amount not in range', 'amount-not-in-range')
class TicketDepartment(DeclarativeBase): __tablename__ = 'ticket_department' id = Field(Integer(), primary_key=True) title = Field(Unicode(50))
class AutoActivationMixin(ActivationMixin): activated_at = Field( DateTime, nullable=True, json='activatedAt', readonly=True, protected=True, default=datetime.now )
class TimestampMixin: created_at = Field(DateTime, default=datetime.now, nullable=False, json='createdAt', readonly=True)
class PaymentGateway(DeclarativeBase): __tablename__ = 'payment_gateway' name = Field(Unicode(30), primary_key=True) fiat_symbol = Field(Unicode(10), ForeignKey('fiat.symbol')) # # TODO: Will be deprecated and replaced by tiers cashin_min = Field(DECIMAL(18, 8), default=Decimal('0.00001000')) cashin_max = Field(DECIMAL(18, 8), default=Decimal('100.00000000')) cashin_static_commission = Field(DECIMAL(18, 8), default=Decimal('0.00000000')) cashin_commission_rate = Field(Unicode(10), default="0.000") cashin_max_commission = Field(DECIMAL(18, 8), default=Decimal('0.00000000')) # # TODO: Will be deprecated and replaced by tiers cashout_min = Field(DECIMAL(18, 8), default=Decimal('0.00010000')) cashout_max = Field(DECIMAL(18, 8), default=Decimal('100.00000000')) cashout_static_commission = Field(DECIMAL(18, 8), default=Decimal('0.00000000')) cashout_commission_rate = Field(Unicode(10), default="0.005") cashout_max_commission = Field(DECIMAL(18, 8), default=Decimal('0.00000000')) fiat = relationship('Fiat') def to_dict(self): result = super().to_dict() # TODO: Get the current user's fiat_tier_policy about this currency # result['tirePolicy'] = {} result['cashoutMin'] = self.fiat.normalized_to_output(self.cashout_min) result['cashoutMax'] = self.fiat.normalized_to_output(self.cashout_max) result['cashoutStaticCommission'] = self.fiat.normalized_to_output(self.cashout_static_commission) result['cashoutMaxCommission'] = self.fiat.normalized_to_output(self.cashout_max_commission) result['cashinMin'] = self.fiat.normalized_to_output(self.cashin_min) result['cashinMax'] = self.fiat.normalized_to_output(self.cashin_max) result['cashinStaticCommission'] = self.fiat.normalized_to_output(self.cashin_static_commission) result['cashinMaxCommission'] = self.fiat.normalized_to_output(self.cashin_max_commission) return result def calculate_cashout_commission(self, amount: Decimal) -> Decimal: commission = self.cashout_static_commission if self.cashout_commission_rate != Decimal(0): commission += amount * Decimal(self.cashout_commission_rate) return min( commission, self.cashout_max_commission ) if self.cashout_max_commission != Decimal(0) else commission def calculate_cashin_commission(self, amount: Decimal) -> Decimal: commission = self.cashin_static_commission if self.cashin_commission_rate != Decimal(0): commission += amount * Decimal(self.cashin_commission_rate) return min( commission, self.cashin_max_commission ) if self.cashin_max_commission != Decimal(0) else commission