def post(self): post_data = request.get_json() withdrawal_id_list = post_data.get('withdrawal_id_list') credit_transfers = [] for withdrawal_id_string in withdrawal_id_list: withdrawal_id = int(withdrawal_id_string) withdrawal = CreditTransfer.query.get(withdrawal_id) withdrawal.resolve_as_completed() credit_transfers.append( CreditTransfer.query.get(withdrawal_id_string)) db.session.flush() response_object = { 'message': 'Withdrawal Confirmed', 'data': { 'credit_transfers': credit_transfers_schema.dump(credit_transfers).data, } } return make_response(jsonify(response_object)), 201
def get(self, disbursement_id): transfers = db.session.query(CreditTransfer)\ .filter(CreditTransfer.disbursement.has(id=disbursement_id))\ .options(joinedload(CreditTransfer.disbursement)) transfers, total_items, total_pages, new_last_fetched = paginate_query( transfers) if transfers is None: response_object = { 'message': 'No credit transfers', } return make_response(jsonify(response_object)), 400 transfer_list = credit_transfers_schema.dump(transfers).data d = db.session.query(Disbursement).filter_by( id=disbursement_id).first() disbursement = disbursement_schema.dump(d).data response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'last_fetched': new_last_fetched, 'data': { 'credit_transfers': transfer_list, 'disbursement': disbursement } } return make_response(jsonify(response_object)), 200
def get(self): """ This endpoint searches transfer accounts and credit transfers. It will check first name/last name/phone number/email address Parameters: - search_string: Any string you want to search. When empty or not provided, all results will be returned. - search_type: Valid inputs are transfer_accounts, and credit_transfers. - order: Which order to return results in (ASC or DESC) - sort_by: What to sort by. `rank` works for both, and sorts by search relevance. - Transfer Accounts can be sorted by: 'first_name', 'last_name', 'email', 'date_account_created', 'rank', 'balance', 'status' - Credit Transfers can be sorted by: 'sender_first_name', 'sender_last_name', 'sender_email', 'sender_date_account_created', 'recipient_first_name', 'recipient_last_name', 'recipient_email', 'recipient_date_account_created', 'rank', 'amount', 'transfer_type', 'approval', 'date_transaction_created' Return Value: Results object, similar to the existing transfer_accounts and credit_transfers API return values """ # HANDLE PARAM : search_stirng search_string = request.args.get('search_string') or '' # HANDLE PARAM : search_type # Valid search types are: `transfer_accounts` and `credit_transfers` # Default: transfer_accounts search_type = request.args.get('search_type') or 'transfer_accounts' if search_type not in ['transfer_accounts', 'credit_transfers']: response_object = { 'message': 'Invalid search_type \'{}\'. Please use type \'transfer_accounts\' or \'credit_transfers\'' .format(search_type), } return make_response(jsonify(response_object)), 400 # HANDLE PARAM : sort_by # Valid params differ depending on sort_by. See: sort_by # Default: rank # Aliases used for joining the separate sender and recipient objects to transfers sender = aliased(User) recipient = aliased(User) # Build order by object 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, 'amount': CreditTransfer.transfer_amount, 'transfer_type': CreditTransfer.transfer_type, 'approval': CreditTransfer.transfer_status, 'date_transaction_created': CreditTransfer.resolved_date, 'sender_first_name': sender.first_name, 'sender_last_name': sender.last_name, 'sender_email': sender.email, 'sender_date_account_created': recipient.created, 'recipient_first_name': recipient.first_name, 'recipient_last_name': recipient.last_name, 'recipient_email': recipient.email, 'recipient_date_account_created': recipient.created } # These lists are to validate the user input-- not using sort_types_to_database_types since credit_transfers and transfer_accounts have unique options user_sorting_options = [ 'first_name', 'last_name', 'email', 'date_account_created' ] sender_sorting_options = list( map(lambda s: 'sender_' + s, user_sorting_options) ) # sender_first_name, sender_last_name, etc... recipient_sorting_options = list( map(lambda s: 'recipient_' + s, user_sorting_options) ) # recipient_first_name, recipient_last_name, etc... sorting_options = { 'transfer_accounts': [*user_sorting_options, 'rank', 'balance', 'status'], 'credit_transfers': [ *sender_sorting_options, *recipient_sorting_options, 'rank', 'amount', 'transfer_type', 'approval', 'date_transaction_created' ] } sort_by_arg = request.args.get('sort_by') or 'rank' if sort_by_arg not in sorting_options[search_type]: response_object = { # Example output: # "Invalid sort_by value 'pizza'. Please use one of the following: 'first_name', 'last_name', 'email', 'rank', 'balance', 'status', 'date_account_created'" 'message': 'Invalid sort_by value \'{}\'. Please use one of the following: {}'\ .format(sort_by_arg, ', '.join('\'{}\''.format(a) for a in sorting_options[search_type])), } return make_response(jsonify(response_object)), 400 sort_by = sort_types_to_database_types[sort_by_arg] 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 not in ['ASC', 'DESC']: response_object = { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\''. format(order_arg), } return make_response(jsonify(response_object)), 400 order = desc if order_arg == 'ASC': order = asc # Note: Using tsquery wildcards here. Good docs of them here: # https://www.postgresql.org/docs/current/datatype-textsearch.html#DATATYPE-TSQUERY # 'Fran deRoo' -> 'Fran:* | deRoo:*' # Matches strings like "Francine deRoos" # Will also match "Michiel deRoos" because of the or clause, but this will be ranked lower search_string = re.sub('\s+', ' ', search_string) search_terms = search_string.strip().split(' ') tsquery = ':* | '.join(search_terms) + ':*' # Return everything if the search string is empty if search_string == '': if search_type == 'transfer_accounts': final_query = TransferAccount.query.filter(TransferAccount.is_ghost != True)\ .join(User, User.default_transfer_account_id == TransferAccount.id) final_query = apply_filters(final_query, filters, User) if sort_by_arg == 'rank': # There's no search rank when there's no query string, so do chrono instead final_query = final_query.order_by(order(User.created)) else: final_query = final_query.order_by(order(sort_by)) transfer_accounts, total_items, total_pages = paginate_query( final_query, TransferAccount) result = transfer_accounts_schema.dump(transfer_accounts) data = {'transfer_accounts': result.data} else: final_query = CreditTransfer.query.filter()\ .outerjoin(sender, sender.default_transfer_account_id == CreditTransfer.sender_transfer_account_id)\ .outerjoin(recipient, recipient.default_transfer_account_id == CreditTransfer.recipient_transfer_account_id) if sort_by_arg == 'rank': # There's no search rank when there's no query string, so do chrono instead final_query = final_query.order_by( order(CreditTransfer.created)) else: final_query = final_query.order_by(order(sort_by)) credit_transfers, total_items, total_pages = paginate_query( final_query, CreditTransfer) result = credit_transfers_schema.dump(credit_transfers) data = {'credit_transfers': result.data} else: # First get users who match search string user_search_result = db.session.query( db.distinct(SearchView.id), SearchView, # This ugly (but functional) multi-tscolumn ranking is a modified from Ben Smithgall's blog post # https://www.codeforamerica.org/blog/2015/07/02/multi-table-full-text-search-with-postgres-flask-and-sqlalchemy/ db.func.max(db.func.full_text.ts_rank( db.func.setweight(db.func.coalesce(SearchView.tsv_email, ''), 'D')\ .concat(db.func.setweight(db.func.coalesce(SearchView.tsv_phone, ''), 'A'))\ .concat(db.func.setweight(db.func.coalesce(SearchView.tsv_first_name, ''), 'B'))\ .concat(db.func.setweight(db.func.coalesce(SearchView.tsv_last_name, ''), 'B'))\ .concat(db.func.setweight(db.func.coalesce(SearchView.tsv_public_serial_number, ''), 'A'))\ .concat(db.func.setweight(db.func.coalesce(SearchView.tsv_primary_blockchain_address, ''), 'A'))\ .concat(db.func.setweight(db.func.coalesce(SearchView.tsv_location, ''), 'C'))\ .concat(db.func.setweight(db.func.coalesce(SearchView.tsv_default_transfer_account_id, ''), 'A')), db.func.to_tsquery(tsquery, postgresql_regconfig='english')))\ .label('rank'))\ .group_by(SearchView)\ .subquery() # Then use those results to join aginst TransferAccount or CreditTransfer if search_type == 'transfer_accounts': # TransferAccount Search Logic final_query = db.session.query(TransferAccount)\ .join(user_search_result, user_search_result.c.default_transfer_account_id == TransferAccount.id)\ .join(User, user_search_result.c.default_transfer_account_id == User.default_transfer_account_id)\ .filter(user_search_result.c.rank > 0.0)\ .filter(TransferAccount.is_ghost != True) final_query = apply_filters(final_query, filters, User) if sort_by_arg == 'rank': final_query = final_query.order_by( order(user_search_result.c.rank)) else: final_query = final_query.order_by(order(sort_by)) transfer_accounts, total_items, total_pages = paginate_query( final_query, TransferAccount) result = transfer_accounts_schema.dump(transfer_accounts) data = {'transfer_accounts': result.data} # CreditTransfer Search Logic else: sender_search_result = aliased(user_search_result) recipient_search_result = aliased(user_search_result) # Join the search results objects to sort by rank, as well as aliased user objects (sender/recipient) for other sorting options final_query = db.session.query(CreditTransfer)\ .outerjoin(sender_search_result, sender_search_result.c.default_transfer_account_id == CreditTransfer.sender_transfer_account_id )\ .outerjoin(recipient_search_result, recipient_search_result.c.default_transfer_account_id == CreditTransfer.recipient_transfer_account_id )\ .outerjoin(sender, sender_search_result.c.default_transfer_account_id == sender.default_transfer_account_id )\ .outerjoin(recipient, recipient_search_result.c.default_transfer_account_id == recipient.default_transfer_account_id )\ .filter(or_(recipient_search_result.c.rank > 0.0, sender_search_result.c.rank > 0.0)) if sort_by_arg == 'rank': final_query = final_query.order_by( order(recipient_search_result.c.rank + sender_search_result.c.rank)) else: final_query = final_query.order_by(order(sort_by)) credit_transfers, total_items, total_pages = paginate_query( final_query, CreditTransfer) result = credit_transfers_schema.dump(credit_transfers) data = {'credit_transfers': result.data} response_object = { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'query_time': datetime.datetime.utcnow(), 'data': data } bytes_data = orjson.dumps(response_object) resp = make_response(bytes_data, 200) resp.mimetype = 'application/json' return resp
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
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 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') transfer_type = post_data.get('transfer_type') transfer_amount = abs( round(float(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') 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') credit_transfers = [] response_list = [] is_bulk = False 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: response_object = { 'message': 'Transfer amount must be positive', } return make_response(jsonify(response_object)), 400 if recipient_transfer_accounts_ids: is_bulk = True 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 = [] for transfer_account_id in recipient_transfer_accounts_ids: try: individual_sender_user = None individual_recipient_user = find_user_with_transfer_account_from_identifiers( None, None, transfer_account_id) transfer_user_list.append( (individual_sender_user, individual_recipient_user)) except (NoTransferAccountError, UserNotFoundError) as e: response_list.append({'status': 400, 'message': str(e)}) else: try: individual_sender_user = 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)] 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 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, uuid=uuid, automatically_resolve_complete=auto_resolve) elif transfer_type == 'RECLAMATION': transfer = make_payment_transfer( transfer_amount, token=token, send_user=sender_user, uuid=uuid, transfer_subtype=TransferSubTypeEnum.RECLAMATION, require_recipient_approved=False, automatically_resolve_complete=auto_resolve) 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, automatically_resolve_complete=auto_resolve) elif transfer_type == 'BALANCE': transfer = make_target_balance_transfer( target_balance, recipient_user, uuid=uuid, automatically_resolve_complete=auto_resolve) 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
def make_transfers(disbursement_id, auto_resolve=False): send_transfer_account = g.user.default_organisation.queried_org_level_transfer_account from server.models.user import User from server.models.transfer_account import TransferAccount from server.models.disbursement import Disbursement disbursement = db.session.query(Disbursement).filter( Disbursement.id == disbursement_id).first() for idx, ta in enumerate(disbursement.transfer_accounts): try: user = ta.primary_user if disbursement.transfer_type == 'DISBURSEMENT': transfer = make_payment_transfer( disbursement.disbursement_amount, send_user=g.user, receive_user=db.session.query(User).filter( User.id == user.id).first(), send_transfer_account=send_transfer_account, receive_transfer_account=db.session.query(TransferAccount). filter(TransferAccount.id == ta.id).first(), transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, transfer_mode=TransferModeEnum.WEB, automatically_resolve_complete=False, ) if disbursement.transfer_type == 'RECLAMATION': transfer = make_payment_transfer( disbursement.disbursement_amount, send_user=db.session.query(User).filter( User.id == user.id).first(), send_transfer_account=db.session.query(TransferAccount). filter(TransferAccount.id == ta.id).first(), transfer_subtype=TransferSubTypeEnum.RECLAMATION, transfer_mode=TransferModeEnum.WEB, require_recipient_approved=False, automatically_resolve_complete=False, ) if disbursement.transfer_type == 'BALANCE': transfer = make_target_balance_transfer( disbursement.disbursement_amount, db.session.query(User).filter(User.id == user.id).first(), automatically_resolve_complete=False, transfer_mode=TransferModeEnum.WEB, ) disbursement.credit_transfers.append(transfer) if auto_resolve and disbursement.state == 'APPROVED': transfer.approvers = disbursement.approvers transfer.add_approver_and_resolve_as_completed() except Exception as e: disbursement.errors = disbursement.errors + [ str(ta) + ': ' + str(e) ] db.session.commit() percent_complete = ( (idx + 1) / len(disbursement.transfer_accounts)) * 100 yield { 'message': 'success' if percent_complete == 100 else 'pending', 'percent_complete': math.floor(percent_complete), 'data': { 'credit_transfers': credit_transfers_schema.dump( disbursement.credit_transfers).data } } clear_metrics_cache() rebuild_metrics_cache()
def post(self, credit_transfer_id): post_data = request.get_json() uuid = post_data.get('uuid') transfer_type = post_data.get('transfer_type') transfer_amount = abs( round(float(post_data.get('transfer_amount') or 0), 6)) target_balance = post_data.get('target_balance') transfer_use = post_data.get('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') credit_transfers = [] response_list = [] is_bulk = False 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: response_object = { 'message': 'Transfer amount must be positive', } return make_response(jsonify(response_object)), 400 if recipient_transfer_accounts_ids: is_bulk = True 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 = [] for transfer_account_id in recipient_transfer_accounts_ids: try: individual_sender_user = None individual_recipient_user = find_user_with_transfer_account_from_identifiers( None, None, transfer_account_id) transfer_user_list.append( (individual_sender_user, individual_recipient_user)) except (NoTransferAccountError, UserNotFoundError) as e: response_list.append({'status': 400, 'message': str(e)}) else: try: individual_sender_user = 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)] except (NoTransferAccountError, UserNotFoundError) as e: response_object = { 'message': str(e), } return make_response(jsonify(response_object)), 400 if not CreditTransfer.check_has_correct_users_for_transfer_type( transfer_type, individual_sender_user, individual_recipient_user): response_object = { 'message': 'For transfer type {}, wrong users of {} and {}'.format( transfer_type, individual_sender_user, individual_recipient_user) } return make_response(jsonify(response_object)), 400 for sender_user, recipient_user in transfer_user_list: try: if transfer_type == 'PAYMENT': transfer = make_payment_transfer(transfer_amount, sender_user, recipient_user, transfer_use, uuid=uuid) elif transfer_type == 'WITHDRAWAL': transfer = make_withdrawal_transfer(transfer_amount, sender_user, uuid=uuid) elif transfer_type == 'DISBURSEMENT': transfer = make_disbursement_transfer(transfer_amount, recipient_user, uuid=uuid) elif transfer_type == 'BALANCE': transfer = make_target_balance_transfer(target_balance, recipient_user, uuid=uuid) 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: if is_bulk: credit_transfers.append(transfer) db.session.commit() response_list.append({ 'status': 200, 'message': 'Transfer Successful' }) else: db.session.commit() credit_transfer = credit_transfer_schema.dump( transfer).data response_object = { 'message': 'Transfer Successful', 'data': { 'credit_transfer': credit_transfer, } } return make_response(jsonify(response_object)), 201 db.session.commit() response_object = { 'message': 'Bulk Transfer Creation Successful', 'bulk_responses': response_list, 'data': { 'credit_transfers': credit_transfers_schema.dump(credit_transfers).data } } return make_response(jsonify(response_object)), 201
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 = 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 transfer_stats = { 'total_distributed': '15300.0000000000000000', 'total_spent': 0, 'total_exchanged': 0, 'has_transferred_count': 0, 'zero_balance_count': 0, 'total_beneficiaries': 3, 'total_users': 3, 'master_wallet_balance': 0, 'daily_transaction_volume': [{ 'date': '2020-04-29T07:05:01.771621', 'volume': 0 }], 'daily_disbursement_volume': [{ 'date': '2020-04-29T00:00:00', 'volume': ('300.0000000000000000') }, { 'date': '2020-04-28T00:00:00', 'volume': '15000.0000000000000000' }], 'transfer_use_breakdown': [], 'last_day_volume': { 'date': '2020-04-29T07:05:01.771617', 'volume': 0 }, 'filter_active': False } 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 make_transfers(disbursement_id, auto_resolve=False): send_transfer_account = g.user.default_organisation.queried_org_level_transfer_account from server.models.user import User from server.models.transfer_account import TransferAccount from server.models.disbursement import Disbursement disbursement = db.session.query(Disbursement).filter( Disbursement.id == disbursement_id).first() for idx, ta in enumerate(disbursement.transfer_accounts): user = ta.primary_user if disbursement.transfer_type == 'DISBURSEMENT': transfer = make_payment_transfer( disbursement.disbursement_amount, send_user=g.user, receive_user=db.session.query(User).filter( User.id == user.id).first(), send_transfer_account=send_transfer_account, receive_transfer_account=db.session.query(TransferAccount). filter(TransferAccount.id == ta.id).first(), transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, transfer_mode=TransferModeEnum.WEB, automatically_resolve_complete=False, ) if disbursement.transfer_type == 'RECLAMATION': transfer = make_payment_transfer( disbursement.disbursement_amount, send_user=db.session.query(User).filter( User.id == user.id).first(), send_transfer_account=db.session.query(TransferAccount).filter( TransferAccount.id == ta.id).first(), transfer_subtype=TransferSubTypeEnum.RECLAMATION, transfer_mode=TransferModeEnum.WEB, require_recipient_approved=False, automatically_resolve_complete=False, ) if disbursement.transfer_type == 'BALANCE': transfer = make_target_balance_transfer( disbursement.disbursement_amount, db.session.query(User).filter(User.id == user.id).first(), automatically_resolve_complete=False, transfer_mode=TransferModeEnum.WEB, ) disbursement.credit_transfers.append(transfer) if auto_resolve: # See below comment on batching issues transfer.resolve_as_complete_and_trigger_blockchain( batch_uuid=None) db.session.commit() percent_complete = ( (idx + 1) / len(disbursement.transfer_accounts)) * 100 yield { 'message': 'success' if percent_complete == 100 else 'pending', 'percent_complete': math.floor(percent_complete), 'data': { 'credit_transfers': credit_transfers_schema.dump( disbursement.credit_transfers).data } }