def get(self): # HANDLE PARAM : search_stirng - Any search string. An empty string (or None) will just return everything! search_string = request.args.get('search_string') or '' # HANDLE PARAM : params - Standard filter object. Exact same as the ones Metrics uses! encoded_filters = request.args.get('params') filters = process_transfer_filters(encoded_filters) # HANDLE PARAM : order # Valid orders types are: `ASC` and `DESC` # Default: DESC order_arg = request.args.get('order') or 'DESC' if order_arg.upper() not in ['ASC', 'DESC']: return { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\''.format(order_arg)} order = asc if order_arg.upper()=='ASC' else desc # HANDLE PARAM: sort_by # Valid orders types are: first_name, last_name, email, date_account_created, rank, balance, status # Default: rank sort_by_arg = request.args.get('sort_by') or 'rank' final_query = generate_search_query(search_string, filters, order, sort_by_arg) transfer_accounts, total_items, total_pages, _ = paginate_query(final_query, ignore_last_fetched=True) if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_accounts_schema.dump(transfer_accounts) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_accounts_schema.dump(transfer_accounts) return { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'query_time': datetime.datetime.utcnow(), 'data': { 'transfer_accounts': result.data } }
def put(self, user_id): user = User.query.get(user_id) reset_tfa = request.args.get('reset_tfa', False) == 'true' if user is not None: if reset_tfa and AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'sempoadmin'): user.reset_TFA() if reset_tfa and not AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'sempoadmin'): response_object = { 'status': 'fail', 'message': 'Only Sempo admininstrators may reset TFA codes.', } return make_response(jsonify(response_object)), 403 user.reset_password() response_object = { 'status': 'success', 'message': 'Successfully reset password for user.', 'data': { 'user': user_schema.dump(user).data } } return make_response(jsonify(response_object)), 200 else: response_object = { 'message': 'No user to reset password for', } return make_response(jsonify(response_object)), 400
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 post(self): post_data = request.get_json() email = post_data.get('email', '') email = email.lower() if email else '' tier = post_data.get('tier') organisation_id = post_data.get('organisation_id', None) if not (email and tier): response_object = {'message': 'No email or tier provided'} return make_response(jsonify(response_object)), 400 if not AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', tier): return make_response( jsonify({ 'message': f'User does not have permission to invite {tier}' })), 400 if organisation_id and not AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'sempoadmin'): response_object = { 'message': 'Not Authorised to set organisation ID' } return make_response(jsonify(response_object)), 401 target_organisation_id = organisation_id or g.active_organisation.id if not target_organisation_id: response_object = { 'message': 'Must provide an organisation to bind user to' } return make_response(jsonify(response_object)), 400 organisation = Organisation.query.get(target_organisation_id) if not organisation: response_object = {'message': 'Organisation Not Found'} return make_response(jsonify(response_object)), 404 email_exists = EmailWhitelist.query.filter( func.lower(EmailWhitelist.email) == email).first() if email_exists: response_object = {'message': 'Email already on whitelist.'} return make_response(jsonify(response_object)), 400 invite = EmailWhitelist(email=email, tier=tier, organisation_id=target_organisation_id) db.session.add(invite) send_invite_email(invite, organisation) db.session.commit() response_object = { 'message': 'An invite has been sent!', } return make_response(jsonify(attach_host(response_object))), 201
def sempo_admin_involved(credit_transfer): if credit_transfer.recipient_user and AccessControl.has_sufficient_tier( credit_transfer.recipient_user.roles, 'ADMIN', 'sempoadmin'): return True if credit_transfer.sender_user and AccessControl.has_sufficient_tier( credit_transfer.sender_user.roles, 'ADMIN', 'sempoadmin'): return True return False
def get(self, kyc_application_id): user_id = request.args.get('user_id') trulioo_countries = request.args.get('trulioo_countries', None) trulioo_documents = request.args.get('trulioo_documents', None) country = request.args.get('country', None) if trulioo_countries: trulioo_countries = supported_countries return make_response(jsonify({'message': 'Trulioo Countries', 'data': {'kyc_application': {'trulioo_countries': trulioo_countries}}})), 200 if trulioo_documents: trulioo_documents = {country: supported_documents[country]} return make_response(jsonify({'message': 'Trulioo Countries', 'data': {'kyc_application': {'trulioo_documents': trulioo_documents}}})), 200 if AccessControl.has_suffient_role(g.user.roles, {'ADMIN': 'subadmin'}): if user_id: # user account KYC kyc_details = KycApplication.query.filter_by(user_id=user_id).first() else: # main organisation KYC kyc_details = KycApplication.query.filter_by(organisation_id=g.active_organisation.id).first() if kyc_details is None: response_object = { 'message': 'No business verification details found' } return make_response(jsonify(response_object)), 404 if user_id and AccessControl.has_suffient_role(g.user.roles, {'ADMIN': 'admin'}): response_object = { 'message': 'Successfully loaded business verification details', 'data': {'kyc_application': kyc_application_schema.dump(kyc_details).data} } return make_response(jsonify(response_object)), 200 else: # must be an individual (mobile) user account kyc_details = KycApplication.query.filter_by(user_id=g.user.id).first() if kyc_details is None: return make_response(jsonify({'message': 'No KYC object found for user.', 'data': {'kyc_application': {}}})) # displays kyc_status and kyc_actions state only. response_object = { 'message': 'Loaded KYC details', 'data': {'kyc_application': kyc_application_state_schema.dump(kyc_details).data} } return make_response(jsonify(response_object)), 200
def test_update_admin_user_tier(new_sempo_admin_user): """ GIVEN a User model WHEN a user tier is updated to superadmin THEN check that all lower tiers are True """ new_sempo_admin_user.set_held_role('ADMIN', 'view') assert AccessControl.has_any_tier(new_sempo_admin_user.roles, 'ADMIN') assert not AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'subadmin') assert not AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'admin') assert not AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'superadmin') # update user tier to super admin new_sempo_admin_user.set_held_role('ADMIN', 'superadmin') assert AccessControl.has_any_tier(new_sempo_admin_user.roles, 'ADMIN') assert AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'subadmin') assert AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'admin') assert AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'superadmin')
def get(self): user = g.user if AccessControl.has_suffient_role(user.roles, {'ADMIN': 'subadmin', 'VENDOR': 'supervendor'}): # admins and supervendors see all transfers for that transfer account transfer_account = user.transfer_account transfers_query = CreditTransfer.query.filter( or_(CreditTransfer.recipient_transfer_account_id == transfer_account.id, CreditTransfer.sender_transfer_account_id == transfer_account.id)) else: # other users only see transfers involving themselves transfers_query = CreditTransfer.query.filter( or_(CreditTransfer.recipient_user_id == user.id, CreditTransfer.sender_user_id == user.id)) transfers, total_items, total_pages = paginate_query(transfers_query, CreditTransfer) transfer_list = me_credit_transfers_schema.dump(transfers).data response_object = { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'data': { 'credit_transfers': transfer_list, } } return make_response(jsonify(response_object)), 201
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 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 post(self): reference = None kyc_application_id = None if 'kyc_application_id' in request.form: kyc_application_id = request.form['kyc_application_id'] if 'document' not in request.files: return make_response(jsonify({'message': 'No File'})), 400 document = request.files['document'] filename = document.filename if kyc_application_id is None: return make_response(jsonify({'message': 'You must append documents to a business profile'})), 400 business_details = KycApplication.query.filter_by(id=kyc_application_id).first() if not business_details: return make_response(jsonify({'message': 'Cannot find kyc for id {}'.format(kyc_application_id)})), 404 if business_details.organisation_id and AccessControl.has_suffient_role(g.user.roles, {'ADMIN': 'superadmin'}) is not True: return make_response(jsonify({'message': 'Must be a superadmin to edit admin org KYC object'})), 401 if filename == '': return make_response(jsonify({'message': 'No File'})), 400 if not allowed_file(filename): return make_response(jsonify({'message': 'Must be JPG, JPEG, PNG or PDF'})), 400 file_type = filename.rsplit('.', 1)[1].lower() new_filename = generate_new_filename(filename, file_type) saved_document = UploadedResource.query.filter_by(filename=new_filename).first() if saved_document: return make_response(jsonify({'message': 'Document already exists'})), 400 save_to_s3_from_document(document=document, new_filename=new_filename) if 'reference' in request.form: reference = request.form['reference'] uploaded_document = UploadedResource(filename=new_filename, file_type=file_type, reference=reference, user_filename=filename) db.session.add(uploaded_document) # tie document to kyc application uploaded_document.kyc_application_id = business_details.id response_object = { 'message': 'Document uploaded', 'data': {'kyc_application': kyc_application_schema.dump(business_details).data} } return make_response(jsonify(response_object)), 201
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 put(self, bank_account_id): put_data = request.get_json() kyc_application_id = put_data.get('kyc_application_id') bank_country = put_data.get('bank_country') routing_number = put_data.get('routing_number') account_number = put_data.get('account_number') currency = put_data.get('currency') if bank_account_id is None: return make_response( jsonify({'message': 'You need to provide a bank account ID'})), 400 bank_account = BankAccount.query.filter_by(id=bank_account_id).first() if kyc_application_id is None: kyc_application_id = bank_account.kyc_application_id business_details = KycApplication.query.filter_by( id=kyc_application_id).first() if not business_details: return make_response( jsonify({ 'message': 'Cannot find kyc for id {}'.format(kyc_application_id) })), 404 if business_details.organisation_id and AccessControl.has_suffient_role( g.user.roles, {'ADMIN': 'superadmin'}) is not True: return make_response( jsonify({ 'message': 'Must be a superadmin to edit admin org KYC object' })), 401 if bank_account: bank_account.kyc_application_id = kyc_application_id bank_account.bank_country = bank_country bank_account.routing_number = routing_number bank_account.account_number = account_number bank_account.currency = currency response_object = { 'message': 'Bank account edited', 'data': { 'kyc_application': kyc_application_schema.dump(business_details).data } } return make_response(jsonify(response_object)), 200
def post(self, bank_account_id): post_data = request.get_json() kyc_application_id = post_data.get('kyc_application_id') bank_country = post_data.get('bank_country') routing_number = post_data.get('routing_number') account_number = post_data.get('account_number') currency = post_data.get('currency') business_details = KycApplication.query.filter_by(id=kyc_application_id).first() if not business_details: return make_response(jsonify({'message': 'Cannot find kyc for id {}'.format(kyc_application_id)})), 404 if business_details.organisation_id and AccessControl.has_suffient_role(g.user.roles, {'ADMIN': 'superadmin'}) is not True: return make_response(jsonify({'message': 'Must be a superadmin to edit admin org KYC object'})), 401 if routing_number is None or account_number is None or bank_country is None or currency is None or kyc_application_id is None: response_object = { 'message': 'Need routing_number, account_number, bank_country, currency and business profile id', } return make_response(jsonify(response_object)), 400 # can't create a duplicate bank account at present bank_account = BankAccount.query.filter_by(routing_number=routing_number, account_number=account_number).first() if bank_account: response_object = { 'message': 'Bank account already exists', } return make_response(jsonify(response_object)), 400 # create new bank account create_bank_account = BankAccount( bank_country=bank_country, routing_number=routing_number, account_number=account_number, currency=currency, ) create_bank_account.kyc_application = business_details db.session.add(create_bank_account) response_object = { 'message': 'Bank account added', 'data': {'kyc_application': kyc_application_schema.dump(business_details).data} } return make_response(jsonify(response_object)), 201
def test_new_sempo_admin_user(new_sempo_admin_user): """ GIVEN a User model WHEN a new admin User is created THEN check the email, password is hashed, not authenticated, and role fields are defined correctly """ assert new_sempo_admin_user.email == '*****@*****.**' assert new_sempo_admin_user.password_hash is not None assert new_sempo_admin_user.password_hash != 'TestPassword' assert not new_sempo_admin_user.is_activated assert AccessControl.has_any_tier(new_sempo_admin_user.roles, 'ADMIN') assert isinstance(new_sempo_admin_user.secret, str)
def check_if_fully_approved(self): # Checks if the credit transfer is approved and ready to be resolved as complete if current_app.config[ 'REQUIRE_MULTIPLE_APPROVALS'] and not AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'sempoadmin'): if len(self.approvers) <= 1: return False else: # 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: return True
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_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 is_TFA_required(self): for tier in current_app.config['TFA_REQUIRED_ROLES']: if AccessControl.has_exact_role(self.roles, 'ADMIN', tier): return True else: return False
def is_supervendor(self): return AccessControl.has_sufficient_tier(self.roles, 'VENDOR', 'supervendor')
def has_group_account_role(self): return AccessControl.has_any_tier(self.roles, 'GROUP_ACCOUNT')
def has_token_agent_role(self): return AccessControl.has_any_tier(self.roles, 'TOKEN_AGENT')
def has_beneficiary_role(self): return AccessControl.has_any_tier(self.roles, 'BENEFICIARY')
def has_vendor_role(self): return AccessControl.has_any_tier(self.roles, 'VENDOR')
def has_admin_role(self): return AccessControl.has_any_tier(self.roles, 'ADMIN')
def get(self): # HANDLE PARAM : search_stirng - Any search string. An empty string (or None) will just return everything! search_string = request.args.get('search_string') or '' # HANDLE PARAM : params - Standard filter object. Exact same as the ones Metrics uses! encoded_filters = request.args.get('params') filters = process_transfer_filters(encoded_filters) # HANDLE PARAM : order # Valid orders types are: `ASC` and `DESC` # Default: DESC order_arg = request.args.get('order') or 'DESC' if order_arg.upper() not in ['ASC', 'DESC']: return { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\''. format(order_arg) } order = asc if order_arg.upper() == 'ASC' else desc # HANDLE PARAM: sort_by # Valid orders types are: first_name, last_name, email, date_account_created, rank, balance, status # Default: rank sort_types_to_database_types = { 'first_name': User.first_name, 'last_name': User.last_name, 'email': User.email, 'date_account_created': User.created, 'rank': 'rank', 'balance': TransferAccount._balance_wei, 'status': TransferAccount.is_approved, } sort_by_arg = request.args.get('sort_by') or 'rank' if sort_by_arg not in sort_types_to_database_types: return { 'message': f'Invalid sort_by value {sort_by_arg}. Please use one of the following: {sort_types_to_database_types.keys()}'\ } # To add new searchable column, simply add a new SearchableColumn object! # And don't forget to add a trigram index on that column too-- see migration 33df5e72fca4 for reference user_search_columns = [ SearchableColumn('first_name', User.first_name, rank=1.5), SearchableColumn('last_name', User.last_name, rank=1.5), SearchableColumn('phone', User.phone, rank=2), SearchableColumn('public_serial_number', User.public_serial_number, rank=2), SearchableColumn('location', User.location, rank=1), SearchableColumn('primary_blockchain_address', User.primary_blockchain_address, rank=2), ] sum_search = reduce(lambda x, y: x + y, [ sc.get_similarity_query(search_string) for sc in user_search_columns ]) sort_by = sum_search if sort_by_arg == 'rank' else sort_types_to_database_types[ sort_by_arg] # If there's no search string, the process is the same, just sort by account creation date sort_by = sort_types_to_database_types[ 'date_account_created'] if sort_by == 'rank' and not search_string else sort_by final_query = db.session.query(TransferAccount, User, sum_search)\ .with_entities(TransferAccount, sum_search)\ .outerjoin(TransferAccount, User.default_transfer_account_id == TransferAccount.id)\ .filter(TransferAccount.is_ghost != True)\ .order_by(order(sort_by)) # If there is a search string, we only want to return ranked results! final_query = final_query.filter( sum_search != 0) if search_string else final_query final_query = apply_filters(final_query, filters, User) transfer_accounts, total_items, total_pages, _ = paginate_query( final_query, ignore_last_fetched=True) accounts = [resultTuple[0] for resultTuple in transfer_accounts] if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_accounts_schema.dump(accounts) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_accounts_schema.dump(accounts) return { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'query_time': datetime.datetime.utcnow(), 'data': { 'transfer_accounts': result.data } }
def get(self, credit_transfer_id): transfer_account_ids = request.args.get('transfer_account_ids') transfer_type = request.args.get('transfer_type', 'ALL') get_transfer_stats = request.args.get('get_stats', False) transfer_list = None if transfer_type: transfer_type = transfer_type.upper() if credit_transfer_id: credit_transfer = CreditTransfer.query.get(credit_transfer_id) if credit_transfer is None: return make_response( jsonify({'message': 'Credit transfer not found'})), 404 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): transfer_list = credit_transfers_schema.dump([credit_transfer ]).data elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): transfer_list = view_credit_transfers_schema.dump( [credit_transfer]).data transfer_stats = [] response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'data': { 'credit_transfer': transfer_list, 'transfer_stats': transfer_stats } } return make_response(jsonify(response_object)), 200 else: query = CreditTransfer.query transfer_list = None if transfer_type != 'ALL': try: transfer_type_enum = TransferTypeEnum[transfer_type] query = query.filter( CreditTransfer.transfer_type == transfer_type_enum) except KeyError: response_object = { 'message': 'Invalid Filter: Transfer Type ', } return make_response(jsonify(response_object)), 400 if transfer_account_ids: # We're getting a list of transfer accounts - parse try: parsed_transfer_account_ids = list( map(lambda x: int(x), filter(None, transfer_account_ids.split(',')))) except ValueError: response_object = { 'message': 'Invalid Filter: Transfer Account IDs ', } return make_response(jsonify(response_object)), 400 if parsed_transfer_account_ids: query = query.filter( or_( CreditTransfer.recipient_transfer_account_id.in_( parsed_transfer_account_ids), CreditTransfer.sender_transfer_account_id.in_( parsed_transfer_account_ids))) transfers, total_items, total_pages = paginate_query( query, CreditTransfer) if get_transfer_stats: transfer_stats = calculate_transfer_stats( total_time_series=True) else: transfer_stats = None if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): transfer_list = credit_transfers_schema.dump(transfers).data elif AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'view'): transfer_list = view_credit_transfers_schema.dump( transfers).data response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'data': { 'credit_transfers': transfer_list, 'transfer_stats': transfer_stats } } return make_response(jsonify(response_object)), 200
def get(self, transfer_account_id): account_type_filter = request.args.get('account_type') result = None if transfer_account_id: transfer_account = TransferAccount.query.get(transfer_account_id) if transfer_account is None: response_object = { 'message': 'No such transfer account: {}'.format(transfer_account_id), } return make_response(jsonify(response_object)), 400 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_account_schema.dump(transfer_account) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_account_schema.dump(transfer_account) response_object = { 'message': 'Successfully Loaded.', 'data': { 'transfer_account': result.data, } } return make_response(jsonify(response_object)), 201 else: search_string = request.args.get('search_string') or '' # HANDLE PARAM : params - Standard filter object. Exact same as the ones Metrics uses! encoded_filters = request.args.get('params') filters = process_transfer_filters(encoded_filters) # HANDLE PARAM : order # Valid orders types are: `ASC` and `DESC` # Default: DESC order_arg = request.args.get('order') or 'DESC' if order_arg.upper() not in ['ASC', 'DESC']: return { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\'' .format(order_arg) } order = asc if order_arg.upper() == 'ASC' else desc # HANDLE PARAM: sort_by # Valid orders types are: first_name, last_name, email, date_account_created, rank, balance, status # Default: rank sort_by_arg = request.args.get('sort_by') or 'rank' base_query = generate_search_query( search_string, filters, order, sort_by_arg).filter(TransferAccount.is_ghost != True) if account_type_filter == 'vendor': transfer_accounts_query = base_query.filter_by(is_vendor=True) elif account_type_filter == 'beneficiary': transfer_accounts_query = base_query.filter_by( is_beneficiary=True) else: pass # Filter Contract, Float and Organisation Transfer Accounts transfer_accounts_query = (base_query.filter( TransferAccount.account_type == TransferAccountType.USER)) transfer_accounts, total_items, total_pages, new_last_fetched = paginate_query( transfer_accounts_query) if transfer_accounts is None: response_object = { 'message': 'No transfer accounts', } return make_response(jsonify(response_object)), 400 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_accounts_schema.dump(transfer_accounts) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_accounts_schema.dump(transfer_accounts) response_object = { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'last_fetched': new_last_fetched, 'query_time': datetime.datetime.utcnow().strftime("%m/%d/%Y, %H:%M:%S"), 'data': { 'transfer_accounts': result.data } } return make_response(json.dumps(response_object), 200)
def get(self, credit_transfer_id): transfer_account_ids = request.args.get('transfer_account_ids') # Handle search parameters # HANDLE PARAM : search_string - Any search string. An empty string (or None) will just return everything! search_string = request.args.get('search_string') or '' # HANDLE PARAM : params - Standard filter object. Exact same as the ones Metrics uses! encoded_filters = request.args.get('params') filters = process_transfer_filters(encoded_filters) # HANDLE PARAM : order # Valid orders types are: `ASC` and `DESC` # Default: DESC order_arg = request.args.get('order') or 'DESC' if order_arg.upper() not in ['ASC', 'DESC']: return { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\''. format(order_arg) } order = asc if order_arg.upper() == 'ASC' else desc # HANDLE PARAM: sort_by # Valid orders types are: first_name, last_name, email, date_account_created, rank, balance, status # Default: rank sort_by_arg = request.args.get('sort_by') or 'rank' if credit_transfer_id: credit_transfer = CreditTransfer.query.get(credit_transfer_id) if credit_transfer is None: return make_response( jsonify({'message': 'Credit transfer not found'})), 404 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = credit_transfer_schema.dump(credit_transfer).data elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_credit_transfer_schema.dump(credit_transfer).data transfer_stats = [] response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'data': { 'credit_transfer': result, 'transfer_stats': transfer_stats } } return make_response(jsonify(response_object)), 200 else: if transfer_account_ids: # We're getting a list of transfer accounts - parse try: parsed_transfer_account_ids = list( map(lambda x: int(x), filter(None, transfer_account_ids.split(',')))) except Exception as e: response_object = {'status': 'fail', 'message': str(e)} return make_response(jsonify(response_object)), 400 if parsed_transfer_account_ids: try: query = generate_search_query( search_string, filters, order, sort_by_arg, search_type=CREDIT_TRANSFER) except Exception as e: response_object = {'status': 'fail', 'message': str(e)} return make_response(jsonify(response_object)), 200 final_query = query.filter( or_( CreditTransfer.recipient_transfer_account_id.in_( parsed_transfer_account_ids), CreditTransfer.sender_transfer_account_id.in_( parsed_transfer_account_ids))) credit_transfers, total_items, total_pages, new_last_fetched = paginate_query( final_query, ignore_last_fetched=True) if AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'admin'): result = credit_transfers_schema.dump(credit_transfers) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_credit_transfers_schema.dump( credit_transfers) return { 'status': 'success', 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'last_fetched': new_last_fetched, 'query_time': datetime.datetime.utcnow(), 'data': { 'credit_transfers': result.data } } else: try: final_query = generate_search_query( search_string, filters, order, sort_by_arg, search_type=CREDIT_TRANSFER) except Exception as e: response_object = {'status': 'fail', 'message': str(e)} return make_response(jsonify(response_object)), 200 credit_transfers, total_items, total_pages, new_last_fetched = paginate_query( final_query, ignore_last_fetched=True) if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = credit_transfers_schema.dump(credit_transfers) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_credit_transfers_schema.dump(credit_transfers) return { 'status': 'success', 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'last_fetched': new_last_fetched, 'query_time': datetime.datetime.utcnow(), 'data': { 'credit_transfers': result.data } }
def post(self, credit_transfer_id): auto_resolve = AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'superadmin') post_data = request.get_json() uuid = post_data.get('uuid') queue = 'low-priority' transfer_type = post_data.get('transfer_type') transfer_amount = abs( round(Decimal(post_data.get('transfer_amount') or 0), 6)) token_id = post_data.get('token_id') target_balance = post_data.get('target_balance') transfer_use = post_data.get('transfer_use') try: use_ids = transfer_use.split(',') # passed as '3,4' etc. except AttributeError: use_ids = transfer_use sender_user_id = post_data.get('sender_user_id') recipient_user_id = post_data.get('recipient_user_id') # These could be phone numbers, email, nfc serial numbers, card numbers etc sender_public_identifier = post_data.get('sender_public_identifier') recipient_public_identifier = post_data.get( 'recipient_public_identifier') sender_transfer_account_id = post_data.get( 'sender_transfer_account_id') recipient_transfer_account_id = post_data.get( 'recipient_transfer_account_id') recipient_transfer_accounts_ids = post_data.get( 'recipient_transfer_accounts_ids') # invert_recipient_list will send to everyone _except_ for the users in recipient_transfer_accounts_ids invert_recipient_list = post_data.get('invert_recipient_list', False) invert_recipient_list = False if invert_recipient_list == False else True credit_transfers = [] response_list = [] is_bulk = False transfer_card = None if uuid: existing_transfer = CreditTransfer.query.filter_by( uuid=uuid).first() # We return a 201 here so that the client removes the uuid from the cache response_object = { 'message': 'Transfer Successful', 'data': { 'credit_transfer': credit_transfer_schema.dump(existing_transfer).data, } } return make_response(jsonify(response_object)), 201 if transfer_amount <= 0 and not target_balance and not ( transfer_amount == 0 and transfer_type == "BALANCE"): response_object = { 'message': 'Transfer amount must be positive', } return make_response(jsonify(response_object)), 400 if recipient_transfer_accounts_ids: is_bulk = True batch_uuid = str(uuid4()) if transfer_type not in ["DISBURSEMENT", "BALANCE"]: response_object = { 'message': 'Bulk transfer must be either disbursement or balance', } return make_response(jsonify(response_object)), 400 transfer_user_list = [] individual_sender_user = None if invert_recipient_list: all_accounts_query = TransferAccount.query.filter( TransferAccount.is_ghost != True).filter_by( organisation_id=g.active_organisation.id) all_user_accounts_query = (all_accounts_query.filter( TransferAccount.account_type == TransferAccountType.USER)) all_accounts_except_selected_query = all_user_accounts_query.filter( not_( TransferAccount.id.in_( recipient_transfer_accounts_ids))) for individual_recipient_user in all_accounts_except_selected_query.all( ): transfer_user_list.append( (individual_sender_user, individual_recipient_user.primary_user, None)) else: for transfer_account_id in recipient_transfer_accounts_ids: try: individual_recipient_user, transfer_card = find_user_with_transfer_account_from_identifiers( None, None, transfer_account_id) transfer_user_list.append( (individual_sender_user, individual_recipient_user, transfer_card)) except (NoTransferAccountError, UserNotFoundError) as e: response_list.append({ 'status': 400, 'message': str(e) }) else: batch_uuid = None try: individual_sender_user, transfer_card = find_user_with_transfer_account_from_identifiers( sender_user_id, sender_public_identifier, sender_transfer_account_id) individual_recipient_user, _ = find_user_with_transfer_account_from_identifiers( recipient_user_id, recipient_public_identifier, recipient_transfer_account_id) transfer_user_list = [ (individual_sender_user, individual_recipient_user, transfer_card) ] except Exception as e: response_object = { 'message': str(e), } return make_response(jsonify(response_object)), 400 if token_id: token = Token.query.get(token_id) if not token: response_object = {'message': 'Token not found'} return make_response(jsonify(response_object)), 404 else: active_organisation = g.active_organisation if active_organisation is None: response_object = {'message': 'Must provide token_id'} return make_response(jsonify(response_object)), 400 else: token = active_organisation.token for sender_user, recipient_user, transfer_card in transfer_user_list: try: if transfer_type == 'PAYMENT': transfer = make_payment_transfer( transfer_amount, token=token, send_user=sender_user, receive_user=recipient_user, transfer_use=transfer_use, transfer_mode=TransferModeEnum.WEB, uuid=uuid, automatically_resolve_complete=False, queue=queue, batch_uuid=batch_uuid, transfer_card=transfer_card) elif transfer_type == 'RECLAMATION': transfer = make_payment_transfer( transfer_amount, token=token, send_user=sender_user, uuid=uuid, transfer_subtype=TransferSubTypeEnum.RECLAMATION, transfer_mode=TransferModeEnum.WEB, require_recipient_approved=False, automatically_resolve_complete=False, queue=queue, batch_uuid=batch_uuid) elif transfer_type == 'DISBURSEMENT': transfer = make_payment_transfer( transfer_amount, token=token, send_user=g.user, receive_user=recipient_user, uuid=uuid, transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, transfer_mode=TransferModeEnum.WEB, automatically_resolve_complete=False, queue=queue, batch_uuid=batch_uuid) elif transfer_type == 'BALANCE': transfer = make_target_balance_transfer( target_balance, recipient_user, uuid=uuid, automatically_resolve_complete=False, transfer_mode=TransferModeEnum.WEB, queue=queue, ) if auto_resolve: transfer.add_approver_and_resolve_as_completed() except (InsufficientBalanceError, AccountNotApprovedError, InvalidTargetBalanceError, BlockchainError, Exception) as e: if is_bulk: response_list.append({'status': 400, 'message': str(e)}) else: db.session.commit() response_object = {'message': str(e)} return make_response(jsonify(response_object)), 400 else: message = 'Transfer Successful' if auto_resolve else 'Transfer Pending. Must be approved.' if is_bulk: credit_transfers.append(transfer) response_list.append({'status': 201, 'message': message}) else: db.session.flush() credit_transfer = credit_transfer_schema.dump( transfer).data response_object = { 'message': message, 'is_create': True, 'data': { 'credit_transfer': credit_transfer, } } return make_response(jsonify(response_object)), 201 db.session.flush() message = 'Bulk Transfer Creation Successful' if auto_resolve else 'Bulk Transfer Pending. Must be approved.' response_object = { 'message': message, 'bulk_responses': response_list, 'data': { 'credit_transfers': credit_transfers_schema.dump(credit_transfers).data } } return make_response(jsonify(response_object)), 201