class Table(db.Model): __table_args__ = (db.UniqueConstraint('name_id', 'ver'), ) id = db.Column(db.Integer, autoincrement=True, primary_key=True) name_id = db.Column(db.Integer, db.ForeignKey('table_name.id'), nullable=False) ver = db.Column(db.Integer, nullable=False, default=1) time = db.Column(db.DateTime, default=datetime.datetime.utcnow) id_col = db.Column(db.String(10), default='id') y_col = db.Column(db.String(10)) y_col_id = db.Column(db.Integer, default=-1) memo = db.Column(db.String(320), default='') price0 = db.Column(db.Numeric(10, 2), default=.01) price1 = db.Column(db.Numeric(10, 2), default=.01) price2 = db.Column(db.Numeric(10, 2), default=.01) score = db.Column(db.Float) # name backref from TableName def get_fate_path(self): return 't%d_v%d' % (self.name_id, self.ver) def get_local_path(self): path = 'u%d_t%d_v%d' % (self.name.uid, self.name_id, self.ver) return os.path.join(app.config['UPLOAD_FOLDER'], path)
class Properties(db.Model): __tablename__ = 'Properties' property_id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(200)) address_l1 = db.Column(db.String(200)) address_l2 = db.Column(db.String(200)) city = db.Column(db.String(200)) state = db.Column(db.String(2)) zipcode = db.Column(db.String(20)) type = db.Column(db.String(50)) beds = db.Column(db.Numeric()) baths = db.Column(db.Numeric()) sale_price = db.Column(db.Integer) rent_price = db.Column(db.Integer) for_sale = db.Column(db.Boolean()) for_rent = db.Column(db.Boolean()) is_public = db.Column(db.Boolean(), default=False) status = db.Column(db.String(100)) area = db.Column(db.Integer) notes = db.Column(db.String(2000)) date_posted = db.Column(db.DateTime(), onupdate=datetime.utcnow, default=datetime.utcnow) images = db.relationship('PropertyImgs', backref='property', lazy='dynamic', order_by='PropertyImgs.date_added') history = db.relationship('History', backref='property', lazy='dynamic', order_by='History.date.desc()') def __init__(self, input): columns = Properties.__table__.columns for c in columns: if c.key in input: setattr(self, c.key, input[c.key]) def add_image(self, image): if not self.has_image(image): self.images.append(image) def remove_image(self, image): if self.has_image(image): self.images.remove(image) def has_image(self, image): return self.images.filter( PropertyImgs.img_id == image.img_id).count() > 0
class Story(Base): __tablename__ = 'stories' title = db.Column(Text(), nullable=False) author = db.Column(Text()) display_image = db.Column(UploadedFileField( \ upload_type=UploadedImageWithThumb)) description = db.Column(Text(), nullable=False) text = db.Column(Text(), nullable=False) latitude = db.Column(db.Numeric(10, 7)) longitude = db.Column(db.Numeric(10, 7)) def __repr__(self): return f'<Story "{self.title}" {self.id}>'
class Transaction(db.Model): id = db.Column(db.Integer, autoincrement=True, primary_key=True) time = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) gid = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) hid = db.Column(db.Integer, db.ForeignKey('user.id')) type = db.Column(db.Integer, nullable=False) amount = db.Column(db.Numeric(10, 2), nullable=False)
class Gasto(db.Model): id = db.Column(db.Integer, primary_key=True) valor = db.Column(db.Numeric(precision=2)) __data = db.Column("data", db.Date, default=db.func.today()) motivo = db.Column(db.Unicode) tipo_id = db.Column(db.Integer, db.ForeignKey('tipo_do_gasto.id')) tipo = db.relationship('TipoDoGasto', backref=db.backref('gastos', lazy='dynamic')) pessoa_id = db.Column(db.Integer, db.ForeignKey('pessoa.id')) pessoa = db.relationship('Pessoa', backref=db.backref('gastos', lazy='dynamic')) def __init__(self, valor, data, motivo, cod_tipo, cod_pessoa): self.valor = valor self.data = data self.motivo = motivo self.tipo_id = cod_tipo self.pessoa_id = cod_pessoa @property def data(self): return self.__data @data.setter def data(self, valor): import datetime print valor dia = int(valor.split("/")[0]) mes = int(valor.split("/")[1]) ano = int(valor.split("/")[2]) self.__data = datetime.date(ano, mes, dia)
class Pensioenen(db.Model): __tablename__ = "pensioenen" id = db.Column(db.Integer, primary_key=True) pensioen_id = db.Column(db.Integer, db.ForeignKey('pensioengegevens.pensioen_id'), nullable=False) pensioen = db.relationship('Pensioengegevens', foreign_keys=pensioen_id) pensioen_uitvoerder = db.Column(db.String(80)) bruto_per_jaar = db.Column(db.Numeric(50)) investment_percentage = db.Column(db.String(25)) polisnummer = db.Column(db.String(40)) begintijd = db.Column(db.String(40)) eindtijd = db.Column(db.String(40)) def __init__(self, Pensioen_id, Pensioen_uitvoerder, Bruto_per_jaar, Investment_percentage, Polisnummer, Begintijd, Eindtijd): self.pensioen_id = Pensioen_id self.pensioen_uitvoerder = Pensioen_uitvoerder self.bruto_per_jaar = Bruto_per_jaar self.investment_percentage = Investment_percentage self.polisnummer = Polisnummer self.begintijd = Begintijd self.eindtijd = Eindtijd
class Route(db.Model): __tablename__ = 'routes' id = db.Column(db.Integer, primary_key=True) race_id = db.Column(db.Integer, ForeignKey('races.id')) name = db.Column(db.String(500)) description = db.Column(db.String) elevation_gain = db.Column(db.Numeric(precision=3)) geom = db.Column(Geometry('GEOMETRY'))
class Disbursement(ModelBase): __tablename__ = 'disbursement' creator_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) label = db.Column(db.String) search_string = db.Column(db.String) search_filter_params = db.Column(db.String) include_accounts = db.Column(db.ARRAY(db.Integer)) exclude_accounts = db.Column(db.ARRAY(db.Integer)) state = db.Column(db.String) transfer_type = db.Column(db.String) _disbursement_amount_wei = db.Column(db.Numeric(27), default=0) creator_user = db.relationship('User', primaryjoin='User.id == Disbursement.creator_user_id') transfer_accounts = db.relationship( "TransferAccount", secondary=disbursement_transfer_account_association_table, back_populates="disbursements") credit_transfers = db.relationship( "CreditTransfer", secondary=disbursement_credit_transfer_association_table, back_populates="disbursement") @hybrid_property def disbursement_amount(self): return (self._disbursement_amount_wei or 0) / int(1e16) @disbursement_amount.setter def disbursement_amount(self, val): self._disbursement_amount_wei = val * int(1e16) def _transition_state(self, new_state): if new_state not in ALLOWED_STATES: raise Exception(f'{new_state} is not an allowed state, must be one of f{ALLOWED_STATES}') allowed_transitions = ALLOWED_STATE_TRANSITIONS.get(self.state, []) if new_state not in allowed_transitions: raise Exception(f'{new_state} is not an allowed transition, must be one of f{allowed_transitions}') self.state = new_state def approve(self): self._transition_state('APPROVED') def reject(self): self._transition_state('REJECTED') def __init__(self, *args, **kwargs): super(Disbursement, self).__init__(*args, **kwargs) self.state = 'PENDING'
class User(db.Model): id = db.Column(db.Integer, autoincrement=True, primary_key=True) name = db.Column(db.String(80), unique=True, nullable=False) pw_hash = db.Column(db.String(80), nullable=False) balance = db.Column(db.Numeric(10, 2), default=0) table_names = db.relationship('TableName', backref=db.backref('user', lazy=True)) model_names = db.relationship('ModelName', backref=db.backref('user', lazy=True))
class Products(db.Model): __tablename__ = 'products' id = db.Column(db.Integer, primary_key=True, autoincrement=True) img = db.Column(db.String()) descricao = db.Column(db.String()) valor = db.Column(db.Numeric(10, 2)) def __init__(self, img, descricao, valor): self.img = img self.descricao = descricao self.valor = valor
class Pensioengegevens(db.Model): __tablename__ = "pensioengegevens" id = db.Column(db.Integer, primary_key=True) pensioen_leeftijd_jaar = db.Column(db.Numeric(4)) pensioen_leeftijd_maand = db.Column(db.Numeric(2)) verwacht_inkomen = db.Column(db.Numeric(50)) verwacht_uitgaven = db.Column(db.Numeric(50)) pensioen_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) pensioen = db.relationship('User', foreign_keys=pensioen_id) def __init__(self, Pensioen_leeftijd_jaar, Pensioen_leeftijd_maand, Verwacht_inkomen, Verwacht_uitgaven, Pensioen_id): self.pensioen_leeftijd_jaar = Pensioen_leeftijd_jaar self.pensioen_leeftijd_maand = Pensioen_leeftijd_maand self.verwacht_inkomen = Verwacht_inkomen self.verwacht_uitgaven = Verwacht_uitgaven self.pensioen_id = Pensioen_id
class Persoongegevens(db.Model): __tablename__ = "persoongegevens" id = db.Column(db.Integer, primary_key=True) voornaam = db.Column(db.String(40)) tussenvoegsel = db.Column(db.String(40)) achternaam = db.Column(db.String(40)) persoon_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) telefoonnr = db.Column(db.String(20)) geboortedatum = db.Column(db.String(25)) adres = db.Column(db.String(40)) postcode = db.Column(db.String(6)) provincie = db.Column(db.String(40)) land = db.Column(db.String(40)) emailadres = db.Column(db.String(40)) bsn = db.Column(db.String(40)) brutosalaris = db.Column(db.Numeric(50)) partner = db.Column(db.String(5)) persoon = db.relationship('User', foreign_keys=persoon_id) def __init__(self, Voornaam, Tussenvoegsel, Achternaam, Persoon_id, Telefoonnr, Geboortedatum, Adres, Postcode, Provincie, Land, Emailadres, Bsn, Brutosalaris, Partner): self.voornaam = Voornaam self.tussenvoegsel = Tussenvoegsel self.achternaam = Achternaam self.persoon_id = Persoon_id self.telefoonnr = Telefoonnr self.geboortedatum = Geboortedatum self.adres = Adres self.postcode = Postcode self.provincie = Provincie self.land = Land self.emailadres = Emailadres self.bsn = Bsn self.brutosalaris = Brutosalaris self.partner = Partner
class Race(db.Model, CRUD): __tablename__ = 'races' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, ForeignKey('users.id')) name = db.Column(db.String(500)) running_race_type = db.Column(db.Enum(RunningRaceType)) distance = db.Column(db.Numeric(precision=3)) start_date_local = db.Column(db.DateTime) city = db.Column(db.String(256)) country = db.Column(db.String(256)) measurement_preference = db.Column(db.Enum(MeasurementPreference)) url = db.Column(db.String(256)) website_url = db.Column(db.String(255)) start_point = db.Column(Geometry('POINT')) routes = relationship('Route') race_follows = relationship('RaceFollow') def __repr__(self): return f"<Race {self.id}:{self.name!r}>"
class TransferAccount(OneOrgBase, ModelBase): __tablename__ = 'transfer_account' name = db.Column(db.String()) _balance_wei = db.Column(db.Numeric(27), default=0) blockchain_address = db.Column(db.String()) is_approved = db.Column(db.Boolean, default=False) # These are different from the permissions on the user: # is_vendor determines whether the account is allowed to have cash out operations etc # is_beneficiary determines whether the account is included in disbursement lists etc is_vendor = db.Column(db.Boolean, default=False) is_beneficiary = db.Column(db.Boolean, default=False) is_ghost = db.Column(db.Boolean, default=False) account_type = db.Column(db.Enum(TransferAccountType)) payable_period_type = db.Column(db.String(), default='week') payable_period_length = db.Column(db.Integer, default=2) payable_epoch = db.Column(db.DateTime, default=datetime.datetime.utcnow) token_id = db.Column(db.Integer, db.ForeignKey("token.id")) exchange_contract_id = db.Column(db.Integer, db.ForeignKey(ExchangeContract.id)) transfer_card = db.relationship('TransferCard', backref='transfer_account', lazy=True, uselist=False) # users = db.relationship('User', backref='transfer_account', lazy=True) users = db.relationship( "User", secondary=user_transfer_account_association_table, back_populates="transfer_accounts", lazy='joined' ) credit_sends = db.relationship('CreditTransfer', backref='sender_transfer_account', foreign_keys='CreditTransfer.sender_transfer_account_id') credit_receives = db.relationship('CreditTransfer', backref='recipient_transfer_account', foreign_keys='CreditTransfer.recipient_transfer_account_id') spend_approvals_given = db.relationship('SpendApproval', backref='giving_transfer_account', foreign_keys='SpendApproval.giving_transfer_account_id') def get_float_transfer_account(self): for transfer_account in self.organisation.transfer_accounts: if transfer_account.account_type == 'FLOAT': return transfer_account float_wallet = TransferAccount.query.filter(TransferAccount.account_type == TransferAccountType.FLOAT).first() return float_wallet @property def balance(self): # division/multipication by int(1e16) occurs because # the db stores amounts in integer WEI: 1 BASE-UNIT (ETH/USD/ETC) * 10^18 # while the system passes around amounts in float CENTS: 1 BASE-UNIT (ETH/USD/ETC) * 10^2 # Therefore the conversion between db and system is 10^18/10^2c = 10^16 # We use cents for historical reasons, and to enable graceful degredation/rounding on # hardware that can only handle small ints (like the transfer cards and old android devices) # rounded to whole value of balance return float((self._balance_wei or 0) / int(1e16)) @balance.setter def balance(self, val): self._balance_wei = val * int(1e16) def decrement_balance(self, val): self.increment_balance(-1 * val) def increment_balance(self, val): # self.balance += val val_wei = val * int(1e16) if isinstance(val_wei, float): val_wei = Decimal(val_wei).quantize(Decimal('1')) self._balance_wei = (self._balance_wei or 0) + val_wei @hybrid_property def total_sent(self): return int( db.session.query(func.sum(server.models.credit_transfer.CreditTransfer.transfer_amount).label('total')).execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) .filter(server.models.credit_transfer.CreditTransfer.sender_transfer_account_id == self.id).first().total or 0 ) @hybrid_property def total_received(self): return int( db.session.query(func.sum(server.models.credit_transfer.CreditTransfer.transfer_amount).label('total')).execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) .filter(server.models.credit_transfer.CreditTransfer.recipient_transfer_account_id == self.id).first().total or 0 ) @hybrid_property def primary_user(self): if len(self.users) == 0: return None return self.users[0] # users = User.query.execution_options(show_all=True) \ # .filter(User.transfer_accounts.any(TransferAccount.id.in_([self.id]))).all() # if len(users) == 0: # # This only happens when we've unbound a user from a transfer account by manually editing the db # return None # # return sorted(users, key=lambda user: user.created)[0] @hybrid_property def primary_user_id(self): return self.primary_user.id # rounded balance @hybrid_property def rounded_account_balance(self): return (self._balance_wei or 0) / int(1e18) @hybrid_property def master_wallet_approval_status(self): if not current_app.config['USING_EXTERNAL_ERC20']: return 'NOT_REQUIRED' if not self.blockchain_address.encoded_private_key: return 'NOT_REQUIRED' base_query = ( BlockchainTransaction.query .filter(BlockchainTransaction.transaction_type == 'master wallet approval') .filter(BlockchainTransaction.credit_transfer.has(recipient_transfer_account_id=self.id)) ) successful_transactions = base_query.filter(BlockchainTransaction.status == 'SUCCESS').all() if len(successful_transactions) > 0: return 'APPROVED' requested_transactions = base_query.filter(BlockchainTransaction.status == 'PENDING').all() if len(requested_transactions) > 0: return 'REQUESTED' failed_transactions = base_query.filter(BlockchainTransaction.status == 'FAILED').all() if len(failed_transactions) > 0: return 'FAILED' return 'NO_REQUEST' def get_or_create_system_transfer_approval(self): sys_blockchain_address = self.organisation.system_blockchain_address approval = self.get_approval(sys_blockchain_address) if not approval: approval = self.give_approval_to_address(sys_blockchain_address) return approval def give_approval_to_address(self, address_getting_approved): approval = SpendApproval(transfer_account_giving_approval=self, address_getting_approved=address_getting_approved) db.session.add(approval) return approval def get_approval(self, receiving_address): for approval in self.spend_approvals_given: if approval.receiving_address == receiving_address: return approval return None def approve_and_disburse(self, initial_disbursement=None): from server.utils.access_control import AccessControl active_org = getattr(g, 'active_organisation', self.primary_user.default_organisation) admin = getattr(g, 'user', None) auto_resolve = initial_disbursement == active_org.default_disbursement if not self.is_approved and admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'admin'): self.is_approved = True if self.is_beneficiary: # TODO: make this more robust # approve_and_disburse might be called for a second time to disburse # so first check that no credit transfer have already been received if len(self.credit_receives) < 1: # make initial disbursement disbursement = self._make_initial_disbursement(initial_disbursement, auto_resolve) return disbursement elif len(self.credit_receives) == 1: # else likely initial disbursement received, check if DISBURSEMENT and PENDING and resolve if default disbursement = self.credit_receives[0] if disbursement.transfer_subtype == TransferSubTypeEnum.DISBURSEMENT and disbursement.transfer_status == TransferStatusEnum.PENDING and auto_resolve: disbursement.resolve_as_completed() return disbursement def _make_initial_disbursement(self, initial_disbursement, auto_resolve=False): from server.utils.credit_transfer import make_payment_transfer active_org = getattr(g, 'active_organisation', Organisation.master_organisation()) initial_disbursement = initial_disbursement or active_org.default_disbursement if not initial_disbursement: return None user_id = get_authorising_user_id() if user_id is not None: sender = User.query.execution_options(show_all=True).get(user_id) else: sender = self.primary_user disbursement = make_payment_transfer( initial_disbursement, token=self.token, send_user=sender, receive_user=self.primary_user, transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, is_ghost_transfer=False, require_sender_approved=False, require_recipient_approved=False, automatically_resolve_complete=auto_resolve) return disbursement def initialise_withdrawal(self, withdrawal_amount): from server.utils.credit_transfer import make_withdrawal_transfer withdrawal = make_withdrawal_transfer(withdrawal_amount, send_account=self, automatically_resolve_complete=False) return withdrawal def _bind_to_organisation(self, organisation): if not self.organisation: self.organisation = organisation if not self.token: self.token = organisation.token def __init__(self, blockchain_address: Optional[str]=None, bound_entity: Optional[Union[Organisation, User]]=None, account_type: Optional[TransferAccountType]=None, private_key: Optional[str] = None, **kwargs): super(TransferAccount, self).__init__(**kwargs) if bound_entity: bound_entity.transfer_accounts.append(self) if isinstance(bound_entity, Organisation): self.account_type = TransferAccountType.ORGANISATION self.blockchain_address = bound_entity.primary_blockchain_address self._bind_to_organisation(bound_entity) elif isinstance(bound_entity, User): self.account_type = TransferAccountType.USER self.blockchain_address = bound_entity.primary_blockchain_address if bound_entity.default_organisation: self._bind_to_organisation(bound_entity.default_organisation) elif isinstance(bound_entity, ExchangeContract): self.account_type = TransferAccountType.CONTRACT self.blockchain_address = bound_entity.blockchain_address self.is_public = True self.exchange_contact = self if not self.organisation: master_organisation = Organisation.master_organisation() if not master_organisation: raise Exception('master_organisation not found') self._bind_to_organisation(master_organisation) if blockchain_address: self.blockchain_address = blockchain_address if not self.blockchain_address: self.blockchain_address = bt.create_blockchain_wallet(private_key=private_key) if account_type: self.account_type = account_type
class CreditTransfer(ManyOrgBase, BlockchainTaskableBase): __tablename__ = 'credit_transfer' uuid = db.Column(db.String, unique=True) resolved_date = db.Column(db.DateTime) _transfer_amount_wei = db.Column(db.Numeric(27), default=0) transfer_type = db.Column(db.Enum(TransferTypeEnum), index=True) transfer_subtype = db.Column(db.Enum(TransferSubTypeEnum)) transfer_status = db.Column(db.Enum(TransferStatusEnum), default=TransferStatusEnum.PENDING) transfer_mode = db.Column(db.Enum(TransferModeEnum)) transfer_use = db.Column(JSON) transfer_metadata = db.Column(JSONB) exclude_from_limit_calcs = db.Column(db.Boolean, default=False) resolution_message = db.Column(db.String()) token_id = db.Column(db.Integer, db.ForeignKey(Token.id)) sender_transfer_account_id = db.Column( db.Integer, db.ForeignKey("transfer_account.id")) recipient_transfer_account_id = db.Column( db.Integer, db.ForeignKey("transfer_account.id")) sender_blockchain_address_id = db.Column( db.Integer, db.ForeignKey("blockchain_address.id")) recipient_blockchain_address_id = db.Column( db.Integer, db.ForeignKey("blockchain_address.id")) sender_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) recipient_user_id = db.Column(db.Integer, db.ForeignKey("user.id")) attached_images = db.relationship('UploadedResource', backref='credit_transfer', lazy=True) fiat_ramp = db.relationship('FiatRamp', backref='credit_transfer', lazy=True, uselist=False) __table_args__ = (Index('updated_index', "updated"), ) from_exchange = db.relationship('Exchange', backref='from_transfer', lazy=True, uselist=False, foreign_keys='Exchange.from_transfer_id') to_exchange = db.relationship('Exchange', backref='to_transfer', lazy=True, uselist=False, foreign_keys='Exchange.to_transfer_id') # TODO: Apply this to all transfer amounts/balances, work out the correct denominator size @hybrid_property def transfer_amount(self): return (self._transfer_amount_wei or 0) / int(1e16) @transfer_amount.setter def transfer_amount(self, val): self._transfer_amount_wei = val * int(1e16) def send_blockchain_payload_to_worker(self, is_retry=False, queue='high-priority'): sender_approval = self.sender_transfer_account.get_or_create_system_transfer_approval( ) recipient_approval = self.recipient_transfer_account.get_or_create_system_transfer_approval( ) return bt.make_token_transfer( signing_address=self.sender_transfer_account.organisation. system_blockchain_address, token=self.token, from_address=self.sender_transfer_account.blockchain_address, to_address=self.recipient_transfer_account.blockchain_address, amount=self.transfer_amount, prior_tasks=list( filter(lambda x: x is not None, [ sender_approval.eth_send_task_uuid, sender_approval.approval_task_uuid, recipient_approval.eth_send_task_uuid, recipient_approval.approval_task_uuid ])), queue=queue, task_uuid=self.blockchain_task_uuid) def resolve_as_completed(self, existing_blockchain_txn=None, queue='high-priority'): if self.transfer_status not in [None, TransferStatusEnum.PENDING]: raise Exception( f'Transfer resolve function called multiple times for transaciton {self.id}' ) self.check_sender_transfer_limits() self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.COMPLETE self.sender_transfer_account.decrement_balance(self.transfer_amount) self.recipient_transfer_account.increment_balance(self.transfer_amount) if self.transfer_type == TransferTypeEnum.PAYMENT and self.transfer_subtype == TransferSubTypeEnum.DISBURSEMENT: if self.recipient_user and self.recipient_user.transfer_card: self.recipient_user.transfer_card.update_transfer_card() if self.fiat_ramp and self.transfer_type in [ TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL ]: self.fiat_ramp.resolve_as_completed() if not existing_blockchain_txn: self.blockchain_task_uuid = str(uuid4()) g.pending_transactions.append((self, queue)) def resolve_as_rejected(self, message=None): if self.transfer_status not in [None, TransferStatusEnum.PENDING]: raise Exception( f'Transfer resolve function called multiple times for transaciton {self.id}' ) if self.fiat_ramp and self.transfer_type in [ TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL ]: self.fiat_ramp.resolve_as_rejected() self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.REJECTED if message: self.resolution_message = message def get_transfer_limits(self): from server.utils.transfer_limits import ( LIMIT_IMPLEMENTATIONS, get_applicable_transfer_limits) return get_applicable_transfer_limits(LIMIT_IMPLEMENTATIONS, self) def check_sender_transfer_limits(self): if self.sender_user is None: # skip if there is no sender, which implies system send return relevant_transfer_limits = self.get_transfer_limits() for limit in relevant_transfer_limits: try: limit.validate_transfer(self) except (TransferAmountLimitError, TransferCountLimitError, TransferBalanceFractionLimitError, MaximumPerTransferLimitError, MinimumSentLimitError, NoTransferAllowedLimitError) as e: self.resolve_as_rejected(message=e.message) raise e return relevant_transfer_limits def check_sender_has_sufficient_balance(self): return self.sender_user and self.sender_transfer_account.balance - self.transfer_amount >= 0 def check_sender_is_approved(self): return self.sender_user and self.sender_transfer_account.is_approved def check_recipient_is_approved(self): return self.recipient_user and self.recipient_transfer_account.is_approved def _select_transfer_account(self, token, user): if token is None: raise Exception("Token must be specified") return find_transfer_accounts_with_matching_token(user, token) def append_organisation_if_required(self, organisation): if organisation and organisation not in self.organisations: self.organisations.append(organisation) def __init__(self, amount, token=None, sender_user=None, recipient_user=None, sender_transfer_account=None, recipient_transfer_account=None, transfer_type: TransferTypeEnum = None, uuid=None, transfer_metadata=None, fiat_ramp=None, transfer_subtype: TransferSubTypeEnum = None, transfer_mode: TransferModeEnum = None, is_ghost_transfer=False): if amount < 0: raise Exception("Negative amount provided") self.transfer_amount = amount self.sender_user = sender_user self.recipient_user = recipient_user self.sender_transfer_account = sender_transfer_account or self._select_transfer_account( token, sender_user) self.token = token or self.sender_transfer_account.token self.fiat_ramp = fiat_ramp try: self.recipient_transfer_account = recipient_transfer_account or self._select_transfer_account( self.token, recipient_user) if is_ghost_transfer is False: self.recipient_transfer_account.is_ghost = False except NoTransferAccountError: self.recipient_transfer_account = TransferAccount( bound_entity=recipient_user, token=token, is_approved=True, is_ghost=is_ghost_transfer) db.session.add(self.recipient_transfer_account) if transfer_type is TransferTypeEnum.DEPOSIT: self.sender_transfer_account = self.recipient_transfer_account.get_float_transfer_account( ) if transfer_type is TransferTypeEnum.WITHDRAWAL: self.recipient_transfer_account = self.sender_transfer_account.get_float_transfer_account( ) if self.sender_transfer_account.token != self.recipient_transfer_account.token: raise Exception("Tokens do not match") self.transfer_type = transfer_type self.transfer_subtype = transfer_subtype self.transfer_mode = transfer_mode self.transfer_metadata = transfer_metadata if uuid is not None: self.uuid = uuid self.append_organisation_if_required( self.recipient_transfer_account.organisation) self.append_organisation_if_required( self.sender_transfer_account.organisation)
class TransferAccount(OneOrgBase, ModelBase, SoftDelete): __tablename__ = 'transfer_account' name = db.Column(db.String()) _balance_wei = db.Column(db.Numeric(27), default=0, index=True) # override ModelBase deleted to add an index created = db.Column(db.DateTime, default=datetime.datetime.utcnow, index=True) # The purpose of the balance offset is to allow the master wallet to be seeded at # initial deploy time. Since balance is calculated by subtracting total credits from # total debits, without a balance offset we'd be stuck in a zero-sum system with no # mechanism to have initial funds. It's essentially an app-level analogy to minting # which happens on the chain. _balance_offset_wei = db.Column(db.Numeric(27), default=0) blockchain_address = db.Column(db.String()) is_approved = db.Column(db.Boolean, default=False) # These are different from the permissions on the user: # is_vendor determines whether the account is allowed to have cash out operations etc # is_beneficiary determines whether the account is included in disbursement lists etc is_vendor = db.Column(db.Boolean, default=False) is_beneficiary = db.Column(db.Boolean, default=False) is_ghost = db.Column(db.Boolean, default=False) account_type = db.Column(db.Enum(TransferAccountType), index=True) payable_period_type = db.Column(db.String(), default='week') payable_period_length = db.Column(db.Integer, default=2) payable_epoch = db.Column(db.DateTime, default=datetime.datetime.utcnow) token_id = db.Column(db.Integer, db.ForeignKey("token.id"), index=True) exchange_contract_id = db.Column(db.Integer, db.ForeignKey(ExchangeContract.id)) transfer_card = db.relationship('TransferCard', backref='transfer_account', lazy=True, uselist=False) notes = db.Column(db.String(), default='') # users = db.relationship('User', backref='transfer_account', lazy=True) users = db.relationship( "User", secondary=user_transfer_account_association_table, back_populates="transfer_accounts", lazy='joined' ) credit_sends = db.relationship( 'CreditTransfer', foreign_keys='CreditTransfer.sender_transfer_account_id', back_populates='sender_transfer_account', order_by='desc(CreditTransfer.id)' ) credit_receives = db.relationship( 'CreditTransfer', foreign_keys='CreditTransfer.recipient_transfer_account_id', back_populates='recipient_transfer_account', order_by='desc(CreditTransfer.id)' ) spend_approvals_given = db.relationship('SpendApproval', backref='giving_transfer_account', foreign_keys='SpendApproval.giving_transfer_account_id') def delete_transfer_account_from_user(self, user: User): """ Soft deletes a Transfer Account if no other users associated to it. """ try: if self.balance != 0: raise TransferAccountDeletionError('Balance must be zero to delete') if self.total_sent_incl_pending_wei != self.total_sent_complete_only_wei: raise TransferAccountDeletionError('Must resolve pending transactions before account deletion') if len(self.users) > 1: # todo(user): deletion of user from account with multiple users - NOT CURRENTLY SUPPORTED raise TransferAccountDeletionError('More than one user attached to transfer account') if self.primary_user == user: timenow = datetime.datetime.utcnow() self.deleted = timenow else: raise TransferAccountDeletionError('Primary user does not match provided user') except (ResourceAlreadyDeletedError, TransferAccountDeletionError) as e: raise e @property def unrounded_balance(self): return Decimal(self._balance_wei or 0) / Decimal(1e16) @property def balance(self): # division/multipication by int(1e16) occurs because # the db stores amounts in integer WEI: 1 BASE-UNIT (ETH/USD/ETC) * 10^18 # while the system passes around amounts in float CENTS: 1 BASE-UNIT (ETH/USD/ETC) * 10^2 # Therefore the conversion between db and system is 10^18/10^2c = 10^16 # We use cents for historical reasons, and to enable graceful degredation/rounding on # hardware that can only handle small ints (like the transfer cards and old android devices) # rounded to whole value of balance return Decimal((self._balance_wei or 0) / int(1e16)) @property def balance_offset(self): return Decimal((self._balance_offset_wei or 0) / int(1e16)) def set_balance_offset(self, val): self._balance_offset_wei = val * int(1e16) self.update_balance() def update_balance(self): """ Update the balance of the user by calculating the difference between inbound and outbound transfers, plus an offset. For inbound transfers we count ONLY complete, while for outbound we count both COMPLETE and PENDING. This means that users can't spend funds that are potentially: - already spent or - from a transfer that may ultimately be rejected. """ if not self._balance_offset_wei: self._balance_offset_wei = 0 net_credit_transfer_position_wei = ( self.total_received_complete_only_wei - self.total_sent_incl_pending_wei ) self._balance_wei = net_credit_transfer_position_wei + self._balance_offset_wei @hybrid_property def total_sent(self): """ Canonical total sent in cents, helping us to remember that sent amounts should include pending txns """ return Decimal(self.total_sent_incl_pending_wei) / int(1e16) @hybrid_property def total_received(self): """ Canonical total sent in cents, helping us to remember that received amounts should only include complete txns """ return Decimal(self.total_received_complete_only_wei) / int(1e16) @hybrid_property def total_sent_complete_only_wei(self): """ The total sent by an account, counting ONLY transfers that have been resolved as complete locally """ amount = ( db.session.query( func.sum(server.models.credit_transfer.CreditTransfer._transfer_amount_wei).label('total') ) .execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.sender_transfer_account_id == self.id) .filter(server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) .first().total ) return amount or 0 @hybrid_property def total_received_complete_only_wei(self): """ The total received by an account, counting ONLY transfers that have been resolved as complete """ amount = ( db.session.query( func.sum(server.models.credit_transfer.CreditTransfer._transfer_amount_wei).label('total') ) .execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.recipient_transfer_account_id == self.id) .filter(server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) .first().total ) return amount or 0 @hybrid_property def total_sent_incl_pending_wei(self): """ The total sent by an account, counting transfers that are either pending or complete locally """ amount = ( db.session.query( func.sum(server.models.credit_transfer.CreditTransfer._transfer_amount_wei).label('total') ) .execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.sender_transfer_account_id == self.id) .filter(or_( server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE, server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.PENDING)) .first().total ) return amount or 0 @hybrid_property def total_received_incl_pending_wei(self): """ The total received by an account, counting transfers that are either pending or complete locally """ amount = ( db.session.query( func.sum(server.models.credit_transfer.CreditTransfer._transfer_amount_wei).label('total') ) .execution_options(show_all=True) .filter(server.models.credit_transfer.CreditTransfer.recipient_transfer_account_id == self.id) .filter(or_( server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE, server.models.credit_transfer.CreditTransfer.transfer_status == TransferStatusEnum.PENDING)) .first().total ) return amount or 0 @hybrid_property def primary_user(self): if len(self.users) == 0: return None return self.users[0] # users = User.query.execution_options(show_all=True) \ # .filter(User.transfer_accounts.any(TransferAccount.id.in_([self.id]))).all() # if len(users) == 0: # # This only happens when we've unbound a user from a transfer account by manually editing the db # return None # # return sorted(users, key=lambda user: user.created)[0] @hybrid_property def primary_user_id(self): return self.primary_user.id # rounded balance @hybrid_property def rounded_account_balance(self): return (self._balance_wei or 0) / int(1e18) def get_or_create_system_transfer_approval(self): sys_blockchain_address = self.organisation.system_blockchain_address approval = self.get_approval(sys_blockchain_address) if not approval: approval = self.give_approval_to_address(sys_blockchain_address) return approval def give_approval_to_address(self, address_getting_approved): approval = SpendApproval(transfer_account_giving_approval=self, address_getting_approved=address_getting_approved) db.session.add(approval) return approval def get_approval(self, receiving_address): for approval in self.spend_approvals_given: if approval.receiving_address == receiving_address: return approval return None def approve_initial_disbursement(self): from server.utils.access_control import AccessControl admin = getattr(g, 'user', None) active_org = getattr(g, 'active_organisation', Organisation.master_organisation()) initial_disbursement = db.session.query(server.models.credit_transfer.CreditTransfer)\ .filter(server.models.credit_transfer.CreditTransfer.recipient_user == self.primary_user)\ .filter(server.models.credit_transfer.CreditTransfer.is_initial_disbursement == True)\ .first() if initial_disbursement and initial_disbursement.transfer_status == TransferStatusEnum.PENDING: # Must be superadmin to auto-resolve something over default disbursement if initial_disbursement.transfer_amount > active_org.default_disbursement: if admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'superadmin'): return initial_disbursement.resolve_as_complete_and_trigger_blockchain(queue='high-priority') else: return False else: return initial_disbursement.resolve_as_complete_and_trigger_blockchain(queue='high-priority') def approve_and_disburse(self, initial_disbursement=None): from server.utils.access_control import AccessControl admin = getattr(g, 'user', None) active_org = getattr(g, 'active_organisation', Organisation.master_organisation()) if initial_disbursement is None: # initial disbursement defaults to None. If initial_disbursement is set then skip this section. # If none, then we want to see if the active_org has a default disbursement amount initial_disbursement = active_org.default_disbursement # Baseline is NOT is_approved, and do NOT auto_resolve self.is_approved = False auto_resolve = False # If admin role is admin or higher, then auto-approval is contingent on being less than or # equal to the default disbursement if (admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'admin'))or ( g.get('auth_type') == 'external' and active_org.auto_approve_externally_created_users ): self.is_approved = True if initial_disbursement <= active_org.default_disbursement: auto_resolve = True # Accounts created by superadmins are all approved, and their disbursements are # auto-resolved no matter how big they are! if admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'superadmin'): self.is_approved = True auto_resolve = True if self.is_beneficiary: # Initial disbursement should be pending if the account is not approved disbursement = self._make_initial_disbursement(initial_disbursement, auto_resolve=auto_resolve) return disbursement def _make_initial_disbursement(self, initial_disbursement=None, auto_resolve=None): from server.utils.credit_transfer import make_payment_transfer if not initial_disbursement: # if initial_disbursement is still none, then we don't want to create a transfer. return None user_id = get_authorising_user_id() if user_id is not None: sender = User.query.execution_options(show_all=True).get(user_id) else: sender = self.primary_user disbursement = make_payment_transfer( initial_disbursement, token=self.token, send_user=sender, receive_user=self.primary_user, transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, transfer_mode=TransferModeEnum.WEB, is_ghost_transfer=False, require_sender_approved=False, require_recipient_approved=False, automatically_resolve_complete=auto_resolve) disbursement.is_initial_disbursement = True return disbursement def initialise_withdrawal(self, withdrawal_amount, transfer_mode): from server.utils.credit_transfer import make_withdrawal_transfer withdrawal = make_withdrawal_transfer(withdrawal_amount, send_user=self, automatically_resolve_complete=False, transfer_mode=transfer_mode, token=self.token) return withdrawal def _bind_to_organisation(self, organisation): if not self.organisation: self.organisation = organisation if not self.token: self.token = organisation.token def __init__(self, blockchain_address: Optional[str]=None, bound_entity: Optional[Union[Organisation, User]]=None, account_type: Optional[TransferAccountType]=None, private_key: Optional[str] = None, **kwargs): super(TransferAccount, self).__init__(**kwargs) if bound_entity: bound_entity.transfer_accounts.append(self) if isinstance(bound_entity, Organisation): self.account_type = TransferAccountType.ORGANISATION self.blockchain_address = bound_entity.primary_blockchain_address self._bind_to_organisation(bound_entity) elif isinstance(bound_entity, User): self.account_type = TransferAccountType.USER self.blockchain_address = bound_entity.primary_blockchain_address if bound_entity.default_organisation: self._bind_to_organisation(bound_entity.default_organisation) elif isinstance(bound_entity, ExchangeContract): self.account_type = TransferAccountType.CONTRACT self.blockchain_address = bound_entity.blockchain_address self.is_public = True self.exchange_contact = self if not self.organisation: master_organisation = Organisation.master_organisation() if not master_organisation: print('master_organisation not found') if master_organisation: self._bind_to_organisation(master_organisation) if blockchain_address: self.blockchain_address = blockchain_address if not self.blockchain_address: self.blockchain_address = bt.create_blockchain_wallet(private_key=private_key) if account_type: self.account_type = account_type
class Disbursement(ModelBase, OneOrgBase): __tablename__ = 'disbursement' creator_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) label = db.Column(db.String) notes = db.Column(db.String(), default='') errors = db.Column(db.ARRAY(db.String), default=[]) search_string = db.Column(db.String) search_filter_params = db.Column(db.String) include_accounts = db.Column(db.ARRAY(db.Integer)) exclude_accounts = db.Column(db.ARRAY(db.Integer)) state = db.Column(db.String) transfer_type = db.Column(db.String) _disbursement_amount_wei = db.Column(db.Numeric(27), default=0) creator_user = db.relationship( 'User', primaryjoin='User.id == Disbursement.creator_user_id', lazy=True) transfer_accounts = db.relationship( "TransferAccount", secondary=disbursement_transfer_account_association_table, back_populates="disbursements", lazy=True) credit_transfers = db.relationship( "CreditTransfer", secondary=disbursement_credit_transfer_association_table, back_populates="disbursement", lazy=True) approvers = db.relationship( "User", secondary=disbursement_approver_user_association_table, lazy=True) approval_times = db.Column(db.ARRAY(db.DateTime), default=[]) @hybrid_property def recipient_count(self): return db.session.query(func.count(disbursement_transfer_account_association_table.c.disbursement_id))\ .filter(disbursement_transfer_account_association_table.c.disbursement_id==self.id).first()[0] @hybrid_property def total_disbursement_amount(self): return self.recipient_count * self.disbursement_amount @hybrid_property def disbursement_amount(self): return (self._disbursement_amount_wei or 0) / int(1e16) @disbursement_amount.setter def disbursement_amount(self, val): self._disbursement_amount_wei = val * int(1e16) def _transition_state(self, new_state): if new_state not in ALLOWED_STATES: raise Exception( f'{new_state} is not an allowed state, must be one of f{ALLOWED_STATES}' ) allowed_transitions = ALLOWED_STATE_TRANSITIONS.get(self.state, []) if new_state not in allowed_transitions: raise Exception( f'{new_state} is not an allowed transition, must be one of f{allowed_transitions}' ) self.state = new_state def add_approver(self): if g.user not in self.approvers: if not self.approval_times: self.approval_times = [] if len(self.approvers) == len(self.approval_times): self.approval_times = self.approval_times + [ datetime.datetime.utcnow() ] self.approvers.append(g.user) def check_if_approved(self): if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'sempoadmin'): return True if current_app.config['REQUIRE_MULTIPLE_APPROVALS']: # It always has to be approved by at least two people if len(self.approvers) <= 1: return False # If there's an `ALLOWED_APPROVERS` list, one of the approvers has to be in it if current_app.config['ALLOWED_APPROVERS']: # approve if email in list for user in self.approvers: if user.email in current_app.config['ALLOWED_APPROVERS']: return True # If there's not an `ALLOWED_APPROVERS` list, it just has to be approved by more than one person else: return True else: # Multi-approval is off, so it's approved by default return True def approve(self): self.add_approver() if self.check_if_approved(): self._transition_state(APPROVED) else: self._transition_state(PARTIAL) return PARTIAL def reject(self): self.add_approver() self._transition_state(REJECTED) def __init__(self, *args, **kwargs): self.organisation_id = g.active_organisation.id super(Disbursement, self).__init__(*args, **kwargs) self.state = PENDING
class CreditTransfer(ManyOrgBase, BlockchainTaskableBase): __tablename__ = 'credit_transfer' uuid = db.Column(db.String, unique=True) resolved_date = db.Column(db.DateTime) _transfer_amount_wei = db.Column(db.Numeric(27), default=0) transfer_type = db.Column(db.Enum(TransferTypeEnum), index=True) transfer_subtype = db.Column(db.Enum(TransferSubTypeEnum)) transfer_status = db.Column(db.Enum(TransferStatusEnum), default=TransferStatusEnum.PENDING) transfer_mode = db.Column(db.Enum(TransferModeEnum)) transfer_use = db.Column(JSON) transfer_metadata = db.Column(JSONB) exclude_from_limit_calcs = db.Column(db.Boolean, default=False) resolution_message = db.Column(db.String()) token_id = db.Column(db.Integer, db.ForeignKey(Token.id)) sender_transfer_account_id = db.Column( db.Integer, db.ForeignKey("transfer_account.id")) recipient_transfer_account_id = db.Column( db.Integer, db.ForeignKey("transfer_account.id")) sender_blockchain_address_id = db.Column( db.Integer, db.ForeignKey("blockchain_address.id")) recipient_blockchain_address_id = db.Column( db.Integer, db.ForeignKey("blockchain_address.id")) sender_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) recipient_user_id = db.Column(db.Integer, db.ForeignKey("user.id")) attached_images = db.relationship('UploadedResource', backref='credit_transfer', lazy=True) fiat_ramp = db.relationship('FiatRamp', backref='credit_transfer', lazy=True, uselist=False) __table_args__ = (Index('updated_index', "updated"), ) from_exchange = db.relationship('Exchange', backref='from_transfer', lazy=True, uselist=False, foreign_keys='Exchange.from_transfer_id') to_exchange = db.relationship('Exchange', backref='to_transfer', lazy=True, uselist=False, foreign_keys='Exchange.to_transfer_id') # TODO: Apply this to all transfer amounts/balances, work out the correct denominator size @hybrid_property def transfer_amount(self): return (self._transfer_amount_wei or 0) / int(1e16) @transfer_amount.setter def transfer_amount(self, val): self._transfer_amount_wei = val * int(1e16) def send_blockchain_payload_to_worker(self, is_retry=False, queue='high-priority'): sender_approval = self.sender_transfer_account.get_or_create_system_transfer_approval( ) recipient_approval = self.recipient_transfer_account.get_or_create_system_transfer_approval( ) self.blockchain_task_uuid = bt.make_token_transfer( signing_address=self.sender_transfer_account.organisation. system_blockchain_address, token=self.token, from_address=self.sender_transfer_account.blockchain_address, to_address=self.recipient_transfer_account.blockchain_address, amount=self.transfer_amount, prior_tasks=list( filter(lambda x: x is not None, [ sender_approval.eth_send_task_uuid, sender_approval.approval_task_uuid, recipient_approval.eth_send_task_uuid, recipient_approval.approval_task_uuid ])), queue=queue) def resolve_as_completed(self, existing_blockchain_txn=None, queue='high-priority'): self.check_sender_transfer_limits() self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.COMPLETE self.sender_transfer_account.decrement_balance(self.transfer_amount) self.recipient_transfer_account.increment_balance(self.transfer_amount) if self.transfer_type == TransferTypeEnum.PAYMENT and self.transfer_subtype == TransferSubTypeEnum.DISBURSEMENT: if self.recipient_user and self.recipient_user.transfer_card: self.recipient_user.transfer_card.update_transfer_card() if self.fiat_ramp and self.transfer_type in [ TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL ]: self.fiat_ramp.resolve_as_completed() if not existing_blockchain_txn: self.send_blockchain_payload_to_worker(queue=queue) def resolve_as_rejected(self, message=None): if self.fiat_ramp and self.transfer_type in [ TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL ]: self.fiat_ramp.resolve_as_rejected() self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.REJECTED if message: self.resolution_message = message def get_transfer_limits(self): import server.utils.transfer_limits return server.utils.transfer_limits.get_transfer_limits(self) def check_sender_transfer_limits(self): if self.sender_user is None: # skip if there is no sender, which implies system send return relevant_transfer_limits = self.get_transfer_limits() for limit in relevant_transfer_limits: if limit.no_transfer_allowed: raise NoTransferAllowedLimitError(token=self.token.name) if limit.transfer_count is not None: # GE Limits transaction_count = limit.apply_all_filters( self, db.session.query( func.count(CreditTransfer.id).label('count')) ).execution_options(show_all=True).first().count if (transaction_count or 0) > limit.transfer_count: message = 'Account Limit "{}" reached. Allowed {} transaction per {} days'\ .format(limit.name, limit.transfer_count, limit.time_period_days) self.resolve_as_rejected(message=message) raise TransferCountLimitError( transfer_count_limit=limit.transfer_count, limit_time_period_days=limit.time_period_days, token=self.token.name, message=message) if limit.transfer_balance_fraction is not None: allowed_transfer = limit.transfer_balance_fraction * self.sender_transfer_account.balance if self.transfer_amount > allowed_transfer: message = 'Account % Limit "{}" reached. {} available'.format( limit.name, max(allowed_transfer, 0)) self.resolve_as_rejected(message=message) raise TransferBalanceFractionLimitError( transfer_balance_fraction_limit=limit. transfer_balance_fraction, transfer_amount_avail=int(allowed_transfer), limit_time_period_days=limit.time_period_days, token=self.token.name, message=message) if limit.total_amount is not None: # Sempo Compliance Account Limits transaction_volume = limit.apply_all_filters( self, db.session.query( func.sum(CreditTransfer.transfer_amount).label('total') )).execution_options(show_all=True).first().total or 0 if transaction_volume > limit.total_amount: # Don't include the current transaction when reporting amount available amount_avail = limit.total_amount - transaction_volume + int( self.transfer_amount) message = 'Account Limit "{}" reached. {} available'.format( limit.name, max(amount_avail, 0)) self.resolve_as_rejected(message=message) raise TransferAmountLimitError( transfer_amount_limit=limit.total_amount, transfer_amount_avail=amount_avail, limit_time_period_days=limit.time_period_days, token=self.token.name, message=message) return relevant_transfer_limits def check_sender_has_sufficient_balance(self): return self.sender_user and self.sender_transfer_account.balance - self.transfer_amount >= 0 def check_sender_is_approved(self): return self.sender_user and self.sender_transfer_account.is_approved def check_recipient_is_approved(self): return self.recipient_user and self.recipient_transfer_account.is_approved def _select_transfer_account(self, token, user): if token is None: raise Exception("Token must be specified") return find_transfer_accounts_with_matching_token(user, token) def append_organisation_if_required(self, organisation): if organisation and organisation not in self.organisations: self.organisations.append(organisation) def __init__(self, amount, token=None, sender_user=None, recipient_user=None, sender_transfer_account=None, recipient_transfer_account=None, transfer_type: TransferTypeEnum = None, uuid=None, transfer_metadata=None, fiat_ramp=None, transfer_subtype: TransferSubTypeEnum = None, is_ghost_transfer=False): if amount < 0: raise Exception("Negative amount provided") self.transfer_amount = amount self.sender_user = sender_user self.recipient_user = recipient_user self.sender_transfer_account = sender_transfer_account or self._select_transfer_account( token, sender_user) self.token = token or self.sender_transfer_account.token self.fiat_ramp = fiat_ramp try: self.recipient_transfer_account = recipient_transfer_account or self._select_transfer_account( self.token, recipient_user) if is_ghost_transfer is False: self.recipient_transfer_account.is_ghost = False except NoTransferAccountError: self.recipient_transfer_account = TransferAccount( bound_entity=recipient_user, token=token, is_approved=True, is_ghost=is_ghost_transfer) db.session.add(self.recipient_transfer_account) if transfer_type is TransferTypeEnum.DEPOSIT: self.sender_transfer_account = self.recipient_transfer_account.get_float_transfer_account( ) if transfer_type is TransferTypeEnum.WITHDRAWAL: self.recipient_transfer_account = self.sender_transfer_account.get_float_transfer_account( ) if self.sender_transfer_account.token != self.recipient_transfer_account.token: raise Exception("Tokens do not match") self.transfer_type = transfer_type self.transfer_subtype = transfer_subtype self.transfer_metadata = transfer_metadata if uuid is not None: self.uuid = uuid self.append_organisation_if_required( self.recipient_transfer_account.organisation) self.append_organisation_if_required( self.sender_transfer_account.organisation)
class Organisation(ModelBase): """ Establishes organisation object that resources can be associated with. """ __tablename__ = 'organisation' is_master = db.Column(db.Boolean, default=False, index=True) name = db.Column(db.String) external_auth_username = db.Column(db.String) valid_roles = db.Column(ARRAY(db.String, dimensions=1)) _external_auth_password = db.Column(db.String) default_lat = db.Column(db.Float()) default_lng = db.Column(db.Float()) _timezone = db.Column(db.String) _country_code = db.Column(db.String, nullable=False) _default_disbursement_wei = db.Column(db.Numeric(27), default=0) require_transfer_card = db.Column(db.Boolean, default=False) # TODO: Create a mixin so that both user and organisation can use the same definition here # This is the blockchain address used for transfer accounts, unless overridden primary_blockchain_address = db.Column(db.String) # This is the 'behind the scenes' blockchain address used for paying gas fees system_blockchain_address = db.Column(db.String) auto_approve_externally_created_users = db.Column(db.Boolean, default=False) users = db.relationship( "User", secondary=organisation_association_table, back_populates="organisations") token_id = db.Column(db.Integer, db.ForeignKey('token.id')) org_level_transfer_account_id = db.Column(db.Integer, db.ForeignKey('transfer_account.id', name="fk_org_level_account")) # We use this weird join pattern because SQLAlchemy # doesn't play nice when doing multiple joins of the same table over different declerative bases org_level_transfer_account = db.relationship( "TransferAccount", post_update=True, primaryjoin="Organisation.org_level_transfer_account_id==TransferAccount.id", uselist=False) @hybrid_property def timezone(self): return self._timezone @timezone.setter def timezone(self, val): if val is not None and val not in pendulum.timezones: raise Exception(f"{val} is not a valid timezone") self._timezone = val @hybrid_property def country_code(self): return self._country_code @country_code.setter def country_code(self, val): if val is not None: val = val.upper() if len(val) != 2: # will try handle 'AD: Andorra' val = val.split(':')[0] if val not in ISO_COUNTRIES: raise Exception(f"{val} is not a valid country code") self._country_code = val @property def default_disbursement(self): return Decimal((self._default_disbursement_wei or 0) / int(1e16)) @default_disbursement.setter def default_disbursement(self, val): if val is not None: self._default_disbursement_wei = int(val) * int(1e16) # TODO: This is a hack to get around the fact that org level TAs don't always show up. Super not ideal @property def queried_org_level_transfer_account(self): if self.org_level_transfer_account_id: return server.models.transfer_account.TransferAccount\ .query.execution_options(show_all=True).get(self.org_level_transfer_account_id) return None @hybrid_property def external_auth_password(self): return decrypt_string(self._external_auth_password) @external_auth_password.setter def external_auth_password(self, value): self._external_auth_password = encrypt_string(value) credit_transfers = db.relationship("CreditTransfer", secondary=organisation_association_table, back_populates="organisations") transfer_accounts = db.relationship('TransferAccount', backref='organisation', lazy=True, foreign_keys='TransferAccount.organisation_id') blockchain_addresses = db.relationship('BlockchainAddress', backref='organisation', lazy=True, foreign_keys='BlockchainAddress.organisation_id') email_whitelists = db.relationship('EmailWhitelist', backref='organisation', lazy=True, foreign_keys='EmailWhitelist.organisation_id') kyc_applications = db.relationship('KycApplication', backref='organisation', lazy=True, foreign_keys='KycApplication.organisation_id') attribute_maps = db.relationship('AttributeMap', backref='organisation', lazy=True, foreign_keys='AttributeMap.organisation_id') custom_welcome_message_key = db.Column(db.String) @staticmethod def master_organisation() -> "Organisation": return Organisation.query.filter_by(is_master=True).first() def _setup_org_transfer_account(self): transfer_account = server.models.transfer_account.TransferAccount( bound_entity=self, is_approved=True ) db.session.add(transfer_account) self.org_level_transfer_account = transfer_account # Back setup for delayed organisation transfer account instantiation for user in self.users: if AccessControl.has_any_tier(user.roles, 'ADMIN'): user.transfer_accounts.append(self.org_level_transfer_account) def bind_token(self, token): self.token = token self._setup_org_transfer_account() def __init__(self, token=None, is_master=False, valid_roles=None, **kwargs): super(Organisation, self).__init__(**kwargs) self.external_auth_username = '******'+ self.name.lower().replace(' ', '_') self.external_auth_password = secrets.token_hex(16) self.valid_roles = valid_roles or list(ASSIGNABLE_TIERS.keys()) if is_master: if Organisation.query.filter_by(is_master=True).first(): raise Exception("A master organisation already exists") self.is_master = True self.system_blockchain_address = bt.create_blockchain_wallet( private_key=current_app.config['MASTER_WALLET_PRIVATE_KEY'], wei_target_balance=0, wei_topup_threshold=0, ) self.primary_blockchain_address = self.system_blockchain_address or bt.create_blockchain_wallet() else: self.is_master = False self.system_blockchain_address = bt.create_blockchain_wallet( wei_target_balance=current_app.config['SYSTEM_WALLET_TARGET_BALANCE'], wei_topup_threshold=current_app.config['SYSTEM_WALLET_TOPUP_THRESHOLD'], ) self.primary_blockchain_address = bt.create_blockchain_wallet() if token: self.bind_token(token)
class CreditTransfer(ManyOrgBase, BlockchainTaskableBase): __tablename__ = 'credit_transfer' uuid = db.Column(db.String, unique=True) batch_uuid = db.Column(db.String) # override ModelBase deleted to add an index created = db.Column(db.DateTime, default=datetime.datetime.utcnow, index=True) resolved_date = db.Column(db.DateTime) _transfer_amount_wei = db.Column(db.Numeric(27), default=0) transfer_type = db.Column(db.Enum(TransferTypeEnum), index=True) transfer_subtype = db.Column(db.Enum(TransferSubTypeEnum)) transfer_status = db.Column(db.Enum(TransferStatusEnum), default=TransferStatusEnum.PENDING) transfer_mode = db.Column(db.Enum(TransferModeEnum), index=True) transfer_use = db.Column(JSON) # Deprecated transfer_usages = db.relationship( "TransferUsage", secondary=credit_transfer_transfer_usage_association_table, back_populates="credit_transfers", lazy='joined' ) transfer_metadata = db.Column(JSONB) exclude_from_limit_calcs = db.Column(db.Boolean, default=False) resolution_message = db.Column(db.String()) token_id = db.Column(db.Integer, db.ForeignKey(Token.id)) sender_transfer_account_id = db.Column(db.Integer, db.ForeignKey("transfer_account.id"), index=True) sender_transfer_account = db.relationship('TransferAccount', foreign_keys=[sender_transfer_account_id], back_populates='credit_sends', lazy='joined') recipient_transfer_account_id = db.Column(db.Integer, db.ForeignKey("transfer_account.id"), index=True) recipient_transfer_account = db.relationship('TransferAccount', foreign_keys=[recipient_transfer_account_id], back_populates='credit_receives', lazy='joined') sender_blockchain_address_id = db.Column(db.Integer, db.ForeignKey("blockchain_address.id"), index=True) recipient_blockchain_address_id = db.Column(db.Integer, db.ForeignKey("blockchain_address.id"), index=True) sender_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) recipient_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) is_initial_disbursement = db.Column(db.Boolean, default=False) attached_images = db.relationship('UploadedResource', backref='credit_transfer', lazy='joined') fiat_ramp = db.relationship('FiatRamp', backref='credit_transfer', lazy=True, uselist=False) __table_args__ = (Index('updated_index', "updated"), ) from_exchange = db.relationship('Exchange', backref='from_transfer', lazy='joined', uselist=False, foreign_keys='Exchange.from_transfer_id') to_exchange = db.relationship('Exchange', backref='to_transfer', lazy=True, uselist=False, foreign_keys='Exchange.to_transfer_id') def add_message(self, message): dated_message = f"[{datetime.datetime.utcnow()}:: {message}]" self.resolution_message = dated_message # TODO: Apply this to all transfer amounts/balances, work out the correct denominator size @hybrid_property def transfer_amount(self): return (self._transfer_amount_wei or 0) / int(1e16) @transfer_amount.setter def transfer_amount(self, val): self._transfer_amount_wei = val * int(1e16) @hybrid_property def rounded_transfer_amount(self): return (self._transfer_amount_wei or 0) / int(1e18) @hybrid_property def public_transfer_type(self): if self.transfer_type == TransferTypeEnum.PAYMENT: if self.transfer_subtype == TransferSubTypeEnum.STANDARD or None: return TransferTypeEnum.PAYMENT else: return self.transfer_subtype else: return self.transfer_type @public_transfer_type.expression def public_transfer_type(cls): from sqlalchemy import case, cast, String return case([ (cls.transfer_subtype == TransferSubTypeEnum.STANDARD, cast(cls.transfer_type, String)), (cls.transfer_type == TransferTypeEnum.PAYMENT, cast(cls.transfer_subtype, String)), ], else_ = cast(cls.transfer_type, String) ) def send_blockchain_payload_to_worker(self, is_retry=False, queue='high-priority'): sender_approval = self.sender_transfer_account.get_or_create_system_transfer_approval() recipient_approval = self.recipient_transfer_account.get_or_create_system_transfer_approval() # Approval is called so that the master account can make transactions on behalf of the transfer account. # Make sure this approval is done first before making a transaction approval_priors = list( filter(lambda x: x is not None, [ sender_approval.eth_send_task_uuid, sender_approval.approval_task_uuid, recipient_approval.eth_send_task_uuid, recipient_approval.approval_task_uuid ])) # Forces an order on transactions so that if there's an outage somewhere, transactions don't get confirmed # On chain in an order that leads to a unrecoverable state other_priors = [t.blockchain_task_uuid for t in self._get_required_prior_tasks()] all_priors = approval_priors + other_priors return bt.make_token_transfer( signing_address=self.sender_transfer_account.organisation.system_blockchain_address, token=self.token, from_address=self.sender_transfer_account.blockchain_address, to_address=self.recipient_transfer_account.blockchain_address, amount=self.transfer_amount, prior_tasks=all_priors, queue=queue, task_uuid=self.blockchain_task_uuid ) def _get_required_prior_tasks(self): """ Get the tasks involving the sender's account that must complete prior to this task being submitted to chain To calculate the prior tasks for the sender Alice: - Find the most recent credit transfer where Alice was the sender, not including any transfers that have the same batch UUID as this transfer. Call this "most_recent_out_of_batch_send" - Find all credit transfers subsequent to "most_recent_out_of_batch_send" where Alice was the recipient. Call this "more_recent_receives" Required priors are all transfers in "more_recent_receives" and "most_recent_out_of_batch_send". For why this works, see https://github.com/teamsempo/SempoBlockchain/pull/262 """ # We're constantly querying complete transfers here. Lazy and DRY complete_transfer_base_query = ( CreditTransfer.query.filter(CreditTransfer.transfer_status == TransferStatusEnum.COMPLETE) ) # Query for finding the most recent transfer sent by the sending account that isn't from the same batch uuid # that of the transfer in question most_recent_out_of_batch_send = ( complete_transfer_base_query .order_by(CreditTransfer.id.desc()) .filter(CreditTransfer.sender_transfer_account == self.sender_transfer_account) .filter(CreditTransfer.id != self.id) .filter(or_(CreditTransfer.batch_uuid != self.batch_uuid, CreditTransfer.batch_uuid == None # Only exclude matching batch_uuids if they're not null ) ).first() ) # Base query for finding more_recent_receives base_receives_query = ( complete_transfer_base_query .filter(CreditTransfer.recipient_transfer_account == self.sender_transfer_account) ) if most_recent_out_of_batch_send: # If most_recent_out_of_batch_send exists, find all receive transfers since it. more_recent_receives = base_receives_query.filter(CreditTransfer.id > most_recent_out_of_batch_send.id).all() # Required priors are then the out of batch send plus these receive transfers required_priors = more_recent_receives + [most_recent_out_of_batch_send] # Edge case handle: if most_recent_out_of_batch_send is a batch member, the whole batch are priors as well if most_recent_out_of_batch_send.batch_uuid is not None: same_batch_priors = complete_transfer_base_query.filter( CreditTransfer.batch_uuid == most_recent_out_of_batch_send.batch_uuid ).all() required_priors = required_priors + same_batch_priors else: # Otherwise, return all receives, which are all our required priors required_priors = base_receives_query.all() # Filter out any transfers that we already know are complete - there's no reason to create an extra dep # We don't do this inside the Alchemy queries because we need the completed priors to calculate other priors required_priors = [prior for prior in required_priors if prior.blockchain_status != BlockchainStatus.SUCCESS] # Remove any possible duplicates return set(required_priors) def resolve_as_complete_with_existing_blockchain_transaction(self, transaction_hash): self.resolve_as_complete() self.blockchain_status = BlockchainStatus.SUCCESS self.blockchain_hash = transaction_hash def resolve_as_complete_and_trigger_blockchain( self, existing_blockchain_txn=None, queue='high-priority', batch_uuid: str=None ): self.resolve_as_complete(batch_uuid) if not existing_blockchain_txn: self.blockchain_task_uuid = str(uuid4()) g.pending_transactions.append((self, queue)) def resolve_as_complete(self, batch_uuid=None): if self.transfer_status not in [None, TransferStatusEnum.PENDING]: raise Exception(f'Resolve called multiple times for transfer {self.id}') try: self.check_sender_transfer_limits() except TransferLimitError as e: # Sempo admins can always bypass limits, allowing for things like emergency moving of funds etc if hasattr(g, 'user') and AccessControl.has_suffient_role(g.user.roles, {'ADMIN': 'sempoadmin'}): self.add_message(f'Warning: {e}') else: raise e self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.COMPLETE self.update_balances() if self.transfer_type == TransferTypeEnum.PAYMENT and self.transfer_subtype == TransferSubTypeEnum.DISBURSEMENT: if self.recipient_user and self.recipient_user.transfer_card: self.recipient_user.transfer_card.update_transfer_card() if batch_uuid: self.batch_uuid = batch_uuid if self.fiat_ramp and self.transfer_type in [TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL]: self.fiat_ramp.resolve_as_complete() def resolve_as_rejected(self, message=None): if self.transfer_status not in [None, TransferStatusEnum.PENDING]: raise Exception(f'Resolve called multiple times for transfer {self.id}') if self.fiat_ramp and self.transfer_type in [TransferTypeEnum.DEPOSIT, TransferTypeEnum.WITHDRAWAL]: self.fiat_ramp.resolve_as_rejected() self.resolved_date = datetime.datetime.utcnow() self.transfer_status = TransferStatusEnum.REJECTED self.blockchain_status = BlockchainStatus.UNSTARTED self.update_balances() if message: self.add_message(message) def update_balances(self): self.sender_transfer_account.update_balance() self.recipient_transfer_account.update_balance() def get_transfer_limits(self): from server.utils.transfer_limits import (LIMIT_IMPLEMENTATIONS, get_applicable_transfer_limits) return get_applicable_transfer_limits(LIMIT_IMPLEMENTATIONS, self) def check_sender_transfer_limits(self): if self.sender_user is None: # skip if there is no sender, which implies system send return relevant_transfer_limits = self.get_transfer_limits() for limit in relevant_transfer_limits: try: limit.validate_transfer(self) except ( TransferAmountLimitError, TransferCountLimitError, TransferBalanceFractionLimitError, MaximumPerTransferLimitError, MinimumSentLimitError, NoTransferAllowedLimitError ) as e: self.resolve_as_rejected(message=e.message) raise e return relevant_transfer_limits def check_sender_has_sufficient_balance(self): return self.sender_transfer_account.unrounded_balance - Decimal(self.transfer_amount) >= 0 def check_sender_is_approved(self): return self.sender_user and self.sender_transfer_account.is_approved def check_recipient_is_approved(self): return self.recipient_user and self.recipient_transfer_account.is_approved def _select_transfer_account(self, token, user): if token is None: raise Exception("Token must be specified") return find_transfer_accounts_with_matching_token(user, token) def append_organisation_if_required(self, organisation): if organisation and organisation not in self.organisations: self.organisations.append(organisation) def __init__(self, amount, token=None, sender_user=None, recipient_user=None, sender_transfer_account=None, recipient_transfer_account=None, transfer_type: TransferTypeEnum=None, uuid=None, transfer_metadata=None, fiat_ramp=None, transfer_subtype: TransferSubTypeEnum=None, transfer_mode: TransferModeEnum = None, is_ghost_transfer=False, require_sufficient_balance=True): if amount < 0: raise Exception("Negative amount provided") self.transfer_amount = amount self.sender_user = sender_user self.recipient_user = recipient_user self.sender_transfer_account = sender_transfer_account or self._select_transfer_account( token, sender_user ) self.token = token or self.sender_transfer_account.token self.fiat_ramp = fiat_ramp if transfer_type is TransferTypeEnum.DEPOSIT: self.sender_transfer_account = self.recipient_transfer_account.token.float_account if transfer_type is TransferTypeEnum.WITHDRAWAL: self.recipient_transfer_account = self.sender_transfer_account.token.float_account try: self.recipient_transfer_account = recipient_transfer_account or self.recipient_transfer_account or self._select_transfer_account( self.token, recipient_user ) if is_ghost_transfer is False: self.recipient_transfer_account.is_ghost = False except NoTransferAccountError: self.recipient_transfer_account = TransferAccount( bound_entity=recipient_user, token=token, is_approved=True, is_ghost=is_ghost_transfer ) db.session.add(self.recipient_transfer_account) if self.sender_transfer_account.token != self.recipient_transfer_account.token: raise Exception("Tokens do not match") self.transfer_type = transfer_type self.transfer_subtype = transfer_subtype self.transfer_mode = transfer_mode self.transfer_metadata = transfer_metadata if uuid is not None: self.uuid = uuid self.append_organisation_if_required(self.recipient_transfer_account.organisation) self.append_organisation_if_required(self.sender_transfer_account.organisation) if require_sufficient_balance and not self.check_sender_has_sufficient_balance(): message = "Sender {} has insufficient balance. Has {}, needs {}.".format( self.sender_transfer_account, self.sender_transfer_account.balance, self.transfer_amount ) self.resolve_as_rejected(message) raise InsufficientBalanceError(message) self.update_balances()
class Ingredient(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(140)) amount = db.Column(db.Numeric(6, 2)) unit = db.Column(db.String(5))