Пример #1
0
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)
Пример #2
0
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
Пример #3
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}>'
Пример #4
0
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)
Пример #5
0
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)
Пример #6
0
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
Пример #7
0
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'))
Пример #8
0
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'
Пример #9
0
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))
Пример #10
0
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
Пример #11
0
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
Пример #12
0
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
Пример #13
0
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}>"
Пример #14
0
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
Пример #15
0
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)
Пример #16
0
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
Пример #17
0
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
Пример #18
0
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)
Пример #19
0
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)
Пример #20
0
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()
Пример #21
0
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))