2021-02-06 16:13:47 +01:00
|
|
|
# standard imports
|
2021-06-23 15:25:09 +02:00
|
|
|
import datetime
|
2021-02-06 16:13:47 +01:00
|
|
|
import logging
|
2021-03-04 17:47:13 +01:00
|
|
|
import json
|
2021-02-06 16:13:47 +01:00
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
# third party imports
|
2021-03-09 17:05:01 +01:00
|
|
|
from sqlalchemy import desc
|
2021-05-01 16:14:20 +02:00
|
|
|
from cic_eth.api import Api
|
2021-06-29 12:49:25 +02:00
|
|
|
from sqlalchemy.orm.session import Session
|
2021-02-06 16:13:47 +01:00
|
|
|
from tinydb.table import Document
|
|
|
|
|
|
|
|
# local imports
|
2021-03-04 17:47:13 +01:00
|
|
|
from cic_ussd.account import define_account_tx_metadata, retrieve_account_statement
|
|
|
|
from cic_ussd.balance import BalanceManager, compute_operational_balance, get_cached_operational_balance
|
|
|
|
from cic_ussd.chain import Chain
|
2021-06-29 12:49:25 +02:00
|
|
|
from cic_ussd.db.models.account import Account
|
|
|
|
from cic_ussd.db.models.base import SessionBase
|
2021-02-06 16:13:47 +01:00
|
|
|
from cic_ussd.db.models.ussd_session import UssdSession
|
2021-06-29 12:49:25 +02:00
|
|
|
from cic_ussd.db.enum import AccountStatus
|
|
|
|
from cic_ussd.error import SeppukuError
|
2021-02-06 16:13:47 +01:00
|
|
|
from cic_ussd.menu.ussd_menu import UssdMenu
|
2021-03-04 17:47:13 +01:00
|
|
|
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
2021-06-29 12:49:25 +02:00
|
|
|
from cic_ussd.phone_number import Support
|
2021-03-04 17:47:13 +01:00
|
|
|
from cic_ussd.redis import cache_data, create_cached_data_key, get_cached_data
|
2021-02-06 16:13:47 +01:00
|
|
|
from cic_ussd.state_machine import UssdStateMachine
|
2021-03-04 17:47:13 +01:00
|
|
|
from cic_ussd.conversions import to_wei, from_wei
|
2021-02-06 16:13:47 +01:00
|
|
|
from cic_ussd.translation import translation_for
|
2021-04-06 19:53:38 +02:00
|
|
|
from cic_types.models.person import generate_metadata_pointer, get_contact_data_from_vcard
|
2021-02-06 16:13:47 +01:00
|
|
|
|
|
|
|
logg = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2021-05-01 16:14:20 +02:00
|
|
|
def get_default_token_data():
|
|
|
|
chain_str = Chain.spec.__str__()
|
|
|
|
cic_eth_api = Api(chain_str=chain_str)
|
|
|
|
default_token_request_task = cic_eth_api.default_token()
|
|
|
|
default_token_data = default_token_request_task.get()
|
|
|
|
return default_token_data
|
|
|
|
|
|
|
|
|
|
|
|
def retrieve_token_symbol(chain_str: str = Chain.spec.__str__()):
|
|
|
|
"""
|
|
|
|
:param chain_str:
|
|
|
|
:type chain_str:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
cache_key = create_cached_data_key(
|
|
|
|
identifier=chain_str.encode('utf-8'),
|
|
|
|
salt=':cic.default_token_data'
|
|
|
|
)
|
|
|
|
cached_data = get_cached_data(key=cache_key)
|
|
|
|
if cached_data:
|
|
|
|
default_token_data = json.loads(cached_data)
|
|
|
|
return default_token_data.get('symbol')
|
|
|
|
else:
|
|
|
|
logg.warning('Cached default token data not found. Attempting retrieval from default token API')
|
|
|
|
default_token_data = get_default_token_data()
|
|
|
|
if default_token_data:
|
|
|
|
return default_token_data.get('symbol')
|
|
|
|
else:
|
|
|
|
raise SeppukuError(f'Could not retrieve default token for: {chain_str}')
|
|
|
|
|
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def process_pin_authorization(account: Account, display_key: str, **kwargs) -> str:
|
|
|
|
"""This method provides translation for all ussd menu entries that follow the pin authorization pattern.
|
|
|
|
:param account: The account in a running USSD session.
|
|
|
|
:type account: Account
|
2021-02-06 16:13:47 +01:00
|
|
|
:param display_key: The path in the translation files defining an appropriate ussd response
|
|
|
|
:type display_key: str
|
|
|
|
:param kwargs: Any additional information required by the text values in the internationalization files.
|
|
|
|
:type kwargs
|
|
|
|
:return: A string value corresponding the ussd menu's text value.
|
|
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
remaining_attempts = 3
|
2021-06-29 12:49:25 +02:00
|
|
|
if account.failed_pin_attempts > 0:
|
2021-02-06 16:13:47 +01:00
|
|
|
return translation_for(
|
|
|
|
key=f'{display_key}.retry',
|
2021-06-29 12:49:25 +02:00
|
|
|
preferred_language=account.preferred_language,
|
|
|
|
remaining_attempts=(remaining_attempts - account.failed_pin_attempts)
|
2021-02-06 16:13:47 +01:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
return translation_for(
|
|
|
|
key=f'{display_key}.first',
|
2021-06-29 12:49:25 +02:00
|
|
|
preferred_language=account.preferred_language,
|
2021-02-06 16:13:47 +01:00
|
|
|
**kwargs
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def process_exit_insufficient_balance(account: Account, display_key: str, session: Session, ussd_session: dict):
|
2021-02-06 16:13:47 +01:00
|
|
|
"""This function processes the exit menu letting users their account balance is insufficient to perform a specific
|
|
|
|
transaction.
|
2021-06-29 12:49:25 +02:00
|
|
|
:param account: The account requesting access to the ussd menu.
|
|
|
|
:type account: Account
|
2021-02-06 16:13:47 +01:00
|
|
|
:param display_key: The path in the translation files defining an appropriate ussd response
|
|
|
|
:type display_key: str
|
2021-06-29 12:49:25 +02:00
|
|
|
:param session:
|
|
|
|
:type session:
|
2021-02-06 16:13:47 +01:00
|
|
|
:param ussd_session: A JSON serialized in-memory ussd session object
|
|
|
|
:type ussd_session: dict
|
|
|
|
:return: Corresponding translation text response
|
|
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
# get account balance
|
2021-06-29 12:49:25 +02:00
|
|
|
operational_balance = get_cached_operational_balance(blockchain_address=account.blockchain_address)
|
2021-02-06 16:13:47 +01:00
|
|
|
|
|
|
|
# compile response data
|
|
|
|
user_input = ussd_session.get('user_input').split('*')[-1]
|
|
|
|
transaction_amount = to_wei(value=int(user_input))
|
2021-05-01 16:14:20 +02:00
|
|
|
|
|
|
|
# get default data
|
|
|
|
token_symbol = retrieve_token_symbol()
|
2021-03-04 17:47:13 +01:00
|
|
|
|
2021-02-06 16:13:47 +01:00
|
|
|
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
2021-06-29 12:49:25 +02:00
|
|
|
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
|
2021-03-04 17:47:13 +01:00
|
|
|
|
|
|
|
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
2021-02-06 16:13:47 +01:00
|
|
|
|
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
2021-06-29 12:49:25 +02:00
|
|
|
preferred_language=account.preferred_language,
|
2021-02-06 16:13:47 +01:00
|
|
|
amount=from_wei(transaction_amount),
|
|
|
|
token_symbol=token_symbol,
|
|
|
|
recipient_information=tx_recipient_information,
|
2021-03-04 17:47:13 +01:00
|
|
|
token_balance=operational_balance
|
2021-02-06 16:13:47 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def process_exit_successful_transaction(account: Account, display_key: str, session: Session, ussd_session: dict):
|
2021-02-06 16:13:47 +01:00
|
|
|
"""This function processes the exit menu after a successful initiation for a transfer of tokens.
|
2021-06-29 12:49:25 +02:00
|
|
|
:param account: The account requesting access to the ussd menu.
|
|
|
|
:type account: Account
|
2021-02-06 16:13:47 +01:00
|
|
|
:param display_key: The path in the translation files defining an appropriate ussd response
|
|
|
|
:type display_key: str
|
2021-06-29 12:49:25 +02:00
|
|
|
:param session:
|
|
|
|
:type session:
|
2021-02-06 16:13:47 +01:00
|
|
|
:param ussd_session: A JSON serialized in-memory ussd session object
|
|
|
|
:type ussd_session: dict
|
|
|
|
:return: Corresponding translation text response
|
|
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
transaction_amount = to_wei(int(ussd_session.get('session_data').get('transaction_amount')))
|
2021-05-01 16:14:20 +02:00
|
|
|
token_symbol = retrieve_token_symbol()
|
2021-02-06 16:13:47 +01:00
|
|
|
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
2021-06-29 12:49:25 +02:00
|
|
|
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
|
2021-03-05 17:28:07 +01:00
|
|
|
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
2021-06-29 12:49:25 +02:00
|
|
|
tx_sender_information = define_account_tx_metadata(user=account)
|
2021-02-06 16:13:47 +01:00
|
|
|
|
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
2021-06-29 12:49:25 +02:00
|
|
|
preferred_language=account.preferred_language,
|
2021-02-06 16:13:47 +01:00
|
|
|
transaction_amount=from_wei(transaction_amount),
|
|
|
|
token_symbol=token_symbol,
|
|
|
|
recipient_information=tx_recipient_information,
|
|
|
|
sender_information=tx_sender_information
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def process_transaction_pin_authorization(account: Account, display_key: str, session: Session, ussd_session: dict):
|
2021-02-06 16:13:47 +01:00
|
|
|
"""This function processes pin authorization where making a transaction is concerned. It constructs a
|
|
|
|
pre-transaction response menu that shows the details of the transaction.
|
2021-06-29 12:49:25 +02:00
|
|
|
:param account: The account requesting access to the ussd menu.
|
|
|
|
:type account: Account
|
2021-02-06 16:13:47 +01:00
|
|
|
:param display_key: The path in the translation files defining an appropriate ussd response
|
|
|
|
:type display_key: str
|
2021-06-29 12:49:25 +02:00
|
|
|
:param session:
|
|
|
|
:type session:
|
2021-02-06 16:13:47 +01:00
|
|
|
:param ussd_session: The USSD session determining what user data needs to be extracted and added to the menu's
|
|
|
|
text values.
|
|
|
|
:type ussd_session: UssdSession
|
|
|
|
:return: Corresponding translation text response
|
|
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
# compile response data
|
|
|
|
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
2021-06-29 12:49:25 +02:00
|
|
|
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
|
2021-03-04 17:47:13 +01:00
|
|
|
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
2021-06-29 12:49:25 +02:00
|
|
|
tx_sender_information = define_account_tx_metadata(user=account)
|
2021-03-04 17:47:13 +01:00
|
|
|
|
2021-05-01 16:14:20 +02:00
|
|
|
token_symbol = retrieve_token_symbol()
|
2021-04-06 19:53:38 +02:00
|
|
|
user_input = ussd_session.get('session_data').get('transaction_amount')
|
2021-02-06 16:13:47 +01:00
|
|
|
transaction_amount = to_wei(value=int(user_input))
|
|
|
|
logg.debug('Requires integration to determine user tokens.')
|
|
|
|
return process_pin_authorization(
|
2021-06-29 12:49:25 +02:00
|
|
|
account=account,
|
2021-02-06 16:13:47 +01:00
|
|
|
display_key=display_key,
|
|
|
|
recipient_information=tx_recipient_information,
|
|
|
|
transaction_amount=from_wei(transaction_amount),
|
|
|
|
token_symbol=token_symbol,
|
|
|
|
sender_information=tx_sender_information
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def process_account_balances(user: Account, display_key: str):
|
2021-03-04 17:47:13 +01:00
|
|
|
"""
|
|
|
|
:param user:
|
|
|
|
:type user:
|
|
|
|
:param display_key:
|
|
|
|
:type display_key:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
# retrieve cached balance
|
|
|
|
operational_balance = get_cached_operational_balance(blockchain_address=user.blockchain_address)
|
|
|
|
|
|
|
|
logg.debug('Requires call to retrieve tax and bonus amounts')
|
|
|
|
tax = ''
|
|
|
|
bonus = ''
|
2021-05-01 16:14:20 +02:00
|
|
|
token_symbol = retrieve_token_symbol()
|
2021-03-04 17:47:13 +01:00
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
operational_balance=operational_balance,
|
|
|
|
tax=tax,
|
|
|
|
bonus=bonus,
|
2021-05-01 16:14:20 +02:00
|
|
|
token_symbol=token_symbol
|
2021-03-04 17:47:13 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-05-01 16:14:20 +02:00
|
|
|
def format_transactions(transactions: list, preferred_language: str, token_symbol: str):
|
2021-03-04 17:47:13 +01:00
|
|
|
|
|
|
|
formatted_transactions = ''
|
|
|
|
if len(transactions) > 0:
|
|
|
|
for transaction in transactions:
|
|
|
|
recipient_phone_number = transaction.get('recipient_phone_number')
|
|
|
|
sender_phone_number = transaction.get('sender_phone_number')
|
2021-03-05 17:28:07 +01:00
|
|
|
value = transaction.get('to_value')
|
2021-03-04 17:47:13 +01:00
|
|
|
timestamp = transaction.get('timestamp')
|
|
|
|
action_tag = transaction.get('action_tag')
|
2021-04-06 19:53:38 +02:00
|
|
|
direction = transaction.get('direction')
|
2021-05-01 16:14:20 +02:00
|
|
|
token_symbol = token_symbol
|
2021-03-04 17:47:13 +01:00
|
|
|
|
|
|
|
if action_tag == 'SENT' or action_tag == 'ULITUMA':
|
2021-04-06 19:53:38 +02:00
|
|
|
formatted_transactions += f'{action_tag} {value} {token_symbol} {direction} {recipient_phone_number} {timestamp}.\n'
|
2021-03-04 17:47:13 +01:00
|
|
|
else:
|
2021-04-06 19:53:38 +02:00
|
|
|
formatted_transactions += f'{action_tag} {value} {token_symbol} {direction} {sender_phone_number} {timestamp}. \n'
|
2021-03-04 17:47:13 +01:00
|
|
|
return formatted_transactions
|
|
|
|
else:
|
|
|
|
if preferred_language == 'en':
|
2021-04-06 19:53:38 +02:00
|
|
|
formatted_transactions = 'NO TRANSACTION HISTORY'
|
2021-03-04 17:47:13 +01:00
|
|
|
else:
|
2021-04-06 19:53:38 +02:00
|
|
|
formatted_transactions = 'HAMNA RIPOTI YA MATUMIZI'
|
2021-03-04 17:47:13 +01:00
|
|
|
return formatted_transactions
|
|
|
|
|
|
|
|
|
2021-04-19 10:44:40 +02:00
|
|
|
def process_display_user_metadata(user: Account, display_key: str):
|
2021-04-06 19:53:38 +02:00
|
|
|
"""
|
|
|
|
:param user:
|
|
|
|
:type user:
|
|
|
|
:param display_key:
|
|
|
|
:type display_key:
|
|
|
|
"""
|
|
|
|
key = generate_metadata_pointer(
|
|
|
|
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
|
2021-05-01 16:52:54 +02:00
|
|
|
cic_type=':cic.person'
|
2021-04-06 19:53:38 +02:00
|
|
|
)
|
2021-06-03 15:40:51 +02:00
|
|
|
cached_metadata = get_cached_data(key)
|
|
|
|
if cached_metadata:
|
|
|
|
user_metadata = json.loads(cached_metadata)
|
2021-04-06 19:53:38 +02:00
|
|
|
contact_data = get_contact_data_from_vcard(vcard=user_metadata.get('vcard'))
|
|
|
|
logg.debug(f'{contact_data}')
|
|
|
|
full_name = f'{contact_data.get("given")} {contact_data.get("family")}'
|
2021-06-23 15:25:09 +02:00
|
|
|
date_of_birth = user_metadata.get('date_of_birth')
|
|
|
|
year_of_birth = date_of_birth.get('year')
|
|
|
|
present_year = datetime.datetime.now().year
|
|
|
|
age = present_year - year_of_birth
|
2021-04-06 19:53:38 +02:00
|
|
|
gender = user_metadata.get('gender')
|
|
|
|
products = ', '.join(user_metadata.get('products'))
|
|
|
|
location = user_metadata.get('location').get('area_name')
|
|
|
|
|
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
full_name=full_name,
|
2021-06-23 15:25:09 +02:00
|
|
|
age=age,
|
2021-04-06 19:53:38 +02:00
|
|
|
gender=gender,
|
|
|
|
location=location,
|
|
|
|
products=products
|
|
|
|
)
|
|
|
|
else:
|
2021-06-07 09:47:02 +02:00
|
|
|
# TODO [Philip]: All these translations could be moved to translation files.
|
|
|
|
logg.warning(f'Expected person metadata but found none in cache for key: {key}')
|
|
|
|
|
|
|
|
absent = ''
|
|
|
|
if user.preferred_language == 'en':
|
|
|
|
absent = 'Not provided'
|
|
|
|
elif user.preferred_language == 'sw':
|
|
|
|
absent = 'Haijawekwa'
|
|
|
|
|
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
full_name=absent,
|
|
|
|
gender=absent,
|
2021-06-30 16:27:56 +02:00
|
|
|
age=absent,
|
2021-06-07 09:47:02 +02:00
|
|
|
location=absent,
|
|
|
|
products=absent
|
|
|
|
)
|
|
|
|
|
2021-04-06 19:53:38 +02:00
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def process_account_statement(user: Account, display_key: str):
|
2021-03-04 17:47:13 +01:00
|
|
|
"""
|
|
|
|
:param user:
|
|
|
|
:type user:
|
|
|
|
:param display_key:
|
|
|
|
:type display_key:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
# retrieve cached statement
|
|
|
|
identifier = blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address)
|
2021-05-01 16:52:54 +02:00
|
|
|
key = create_cached_data_key(identifier=identifier, salt=':cic.statement')
|
2021-03-04 17:47:13 +01:00
|
|
|
transactions = get_cached_data(key=key)
|
|
|
|
|
2021-05-01 16:14:20 +02:00
|
|
|
token_symbol = retrieve_token_symbol()
|
|
|
|
|
2021-03-04 17:47:13 +01:00
|
|
|
first_transaction_set = []
|
|
|
|
middle_transaction_set = []
|
|
|
|
last_transaction_set = []
|
|
|
|
|
2021-03-05 17:28:07 +01:00
|
|
|
transactions = json.loads(transactions)
|
|
|
|
|
2021-03-04 17:47:13 +01:00
|
|
|
if len(transactions) > 6:
|
|
|
|
last_transaction_set += transactions[6:]
|
|
|
|
middle_transaction_set += transactions[3:][:3]
|
|
|
|
first_transaction_set += transactions[:3]
|
|
|
|
# there are probably much cleaner and operational inexpensive ways to do this so find them
|
2021-04-06 19:53:38 +02:00
|
|
|
elif 3 < len(transactions) < 7:
|
2021-03-04 17:47:13 +01:00
|
|
|
middle_transaction_set += transactions[3:]
|
|
|
|
first_transaction_set += transactions[:3]
|
|
|
|
else:
|
|
|
|
first_transaction_set += transactions[:3]
|
|
|
|
|
|
|
|
if display_key == 'ussd.kenya.first_transaction_set':
|
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
first_transaction_set=format_transactions(
|
|
|
|
transactions=first_transaction_set,
|
2021-05-01 16:14:20 +02:00
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
token_symbol=token_symbol
|
2021-03-04 17:47:13 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
elif display_key == 'ussd.kenya.middle_transaction_set':
|
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
middle_transaction_set=format_transactions(
|
|
|
|
transactions=middle_transaction_set,
|
2021-05-01 16:14:20 +02:00
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
token_symbol=token_symbol
|
2021-03-04 17:47:13 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
elif display_key == 'ussd.kenya.last_transaction_set':
|
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
last_transaction_set=format_transactions(
|
|
|
|
transactions=last_transaction_set,
|
2021-05-01 16:14:20 +02:00
|
|
|
preferred_language=user.preferred_language,
|
|
|
|
token_symbol=token_symbol
|
2021-03-04 17:47:13 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-04-19 10:44:40 +02:00
|
|
|
def process_start_menu(display_key: str, user: Account):
|
2021-02-06 16:13:47 +01:00
|
|
|
"""This function gets data on an account's balance and token in order to append it to the start of the start menu's
|
|
|
|
title. It passes said arguments to the translation function and returns the appropriate corresponding text from the
|
|
|
|
translation files.
|
|
|
|
:param user: The user requesting access to the ussd menu.
|
2021-04-19 10:44:40 +02:00
|
|
|
:type user: Account
|
2021-02-06 16:13:47 +01:00
|
|
|
:param display_key: The path in the translation files defining an appropriate ussd response
|
|
|
|
:type display_key: str
|
|
|
|
:return: Corresponding translation text response
|
|
|
|
:rtype: str
|
|
|
|
"""
|
2021-05-01 16:14:20 +02:00
|
|
|
token_symbol = retrieve_token_symbol()
|
2021-03-04 17:47:13 +01:00
|
|
|
chain_str = Chain.spec.__str__()
|
|
|
|
blockchain_address = user.blockchain_address
|
|
|
|
balance_manager = BalanceManager(address=blockchain_address,
|
|
|
|
chain_str=chain_str,
|
2021-05-01 16:14:20 +02:00
|
|
|
token_symbol=token_symbol)
|
2021-03-04 17:47:13 +01:00
|
|
|
|
|
|
|
# get balances synchronously for display on start menu
|
|
|
|
balances_data = balance_manager.get_balances()
|
|
|
|
|
|
|
|
key = create_cached_data_key(
|
|
|
|
identifier=bytes.fromhex(blockchain_address[2:]),
|
2021-05-01 16:52:54 +02:00
|
|
|
salt=':cic.balances_data'
|
2021-03-04 17:47:13 +01:00
|
|
|
)
|
|
|
|
cache_data(key=key, data=json.dumps(balances_data))
|
|
|
|
|
|
|
|
# get operational balance
|
|
|
|
operational_balance = compute_operational_balance(balances=balances_data)
|
|
|
|
|
|
|
|
# retrieve and cache account's statement
|
|
|
|
retrieve_account_statement(blockchain_address=blockchain_address)
|
|
|
|
|
2021-02-06 16:13:47 +01:00
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
|
|
|
preferred_language=user.preferred_language,
|
2021-03-04 17:47:13 +01:00
|
|
|
account_balance=operational_balance,
|
2021-02-06 16:13:47 +01:00
|
|
|
account_token_name=token_symbol
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def retrieve_most_recent_ussd_session(phone_number: str, session: Session) -> UssdSession:
|
2021-03-09 17:05:01 +01:00
|
|
|
# get last ussd session based on user phone number
|
2021-06-29 12:49:25 +02:00
|
|
|
session = SessionBase.bind_session(session=session)
|
|
|
|
last_ussd_session = session.query(UssdSession)\
|
2021-03-09 17:05:01 +01:00
|
|
|
.filter_by(msisdn=phone_number)\
|
|
|
|
.order_by(desc(UssdSession.created))\
|
|
|
|
.first()
|
2021-06-29 12:49:25 +02:00
|
|
|
SessionBase.release_session(session=session)
|
2021-03-09 17:05:01 +01:00
|
|
|
return last_ussd_session
|
|
|
|
|
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def process_request(account: Account, session, user_input: str, ussd_session: Optional[dict] = None) -> Document:
|
2021-02-06 16:13:47 +01:00
|
|
|
"""This function assesses a request based on the user from the request comes, the session_id and the user's
|
|
|
|
input. It determines whether the request translates to a return to an existing session by checking whether the
|
|
|
|
provided session id exists in the database or whether the creation of a new ussd session object is warranted.
|
|
|
|
It then returns the appropriate ussd menu text values.
|
2021-06-29 12:49:25 +02:00
|
|
|
:param account: The account requesting access to the ussd menu.
|
|
|
|
:type account: Account
|
|
|
|
:param session:
|
|
|
|
:type session:
|
2021-02-06 16:13:47 +01:00
|
|
|
:param user_input: The value a user enters in the ussd menu.
|
|
|
|
:type user_input: str
|
|
|
|
:param ussd_session: A JSON serialized in-memory ussd session object
|
|
|
|
:type ussd_session: dict
|
|
|
|
:return: A ussd menu's corresponding text value.
|
|
|
|
:rtype: Document
|
|
|
|
"""
|
2021-05-01 16:14:20 +02:00
|
|
|
|
2021-02-06 16:13:47 +01:00
|
|
|
if ussd_session:
|
|
|
|
if user_input == "0":
|
|
|
|
return UssdMenu.parent_menu(menu_name=ussd_session.get('state'))
|
|
|
|
else:
|
2021-06-29 12:49:25 +02:00
|
|
|
successive_state = next_state(
|
|
|
|
account=account,
|
|
|
|
session=session,
|
|
|
|
ussd_session=ussd_session,
|
|
|
|
user_input=user_input)
|
2021-02-06 16:13:47 +01:00
|
|
|
return UssdMenu.find_by_name(name=successive_state)
|
|
|
|
else:
|
2021-06-29 12:49:25 +02:00
|
|
|
if account.has_valid_pin():
|
|
|
|
last_ussd_session = retrieve_most_recent_ussd_session(phone_number=account.phone_number, session=session)
|
2021-03-09 17:05:01 +01:00
|
|
|
|
|
|
|
if last_ussd_session:
|
|
|
|
# get last state
|
|
|
|
last_state = last_ussd_session.state
|
|
|
|
# if last state is account_creation_prompt and metadata exists, show start menu
|
2021-04-06 19:53:38 +02:00
|
|
|
if last_state in [
|
|
|
|
'account_creation_prompt',
|
|
|
|
'exit',
|
|
|
|
'exit_invalid_pin',
|
|
|
|
'exit_invalid_new_pin',
|
|
|
|
'exit_pin_mismatch',
|
2021-06-03 15:40:51 +02:00
|
|
|
'exit_invalid_request',
|
|
|
|
'exit_successful_transaction'
|
2021-06-23 06:29:38 +02:00
|
|
|
]:
|
2021-03-09 17:05:01 +01:00
|
|
|
return UssdMenu.find_by_name(name='start')
|
|
|
|
else:
|
|
|
|
return UssdMenu.find_by_name(name=last_state)
|
2021-02-06 16:13:47 +01:00
|
|
|
else:
|
2021-06-29 12:49:25 +02:00
|
|
|
if account.failed_pin_attempts >= 3 and account.get_account_status() == AccountStatus.LOCKED.name:
|
2021-02-06 16:13:47 +01:00
|
|
|
return UssdMenu.find_by_name(name='exit_pin_blocked')
|
2021-06-29 12:49:25 +02:00
|
|
|
elif account.preferred_language is None:
|
2021-02-06 16:13:47 +01:00
|
|
|
return UssdMenu.find_by_name(name='initial_language_selection')
|
|
|
|
else:
|
|
|
|
return UssdMenu.find_by_name(name='initial_pin_entry')
|
|
|
|
|
|
|
|
|
2021-06-29 12:49:25 +02:00
|
|
|
def next_state(account: Account, session, ussd_session: dict, user_input: str) -> str:
|
2021-02-06 16:13:47 +01:00
|
|
|
"""This function navigates the state machine based on the ussd session object and user inputs it receives.
|
|
|
|
It checks the user input and provides the successive state in the state machine. It then updates the session's
|
|
|
|
state attribute with the new state.
|
2021-06-29 12:49:25 +02:00
|
|
|
:param account: The account requesting access to the ussd menu.
|
|
|
|
:type account: Account
|
|
|
|
:param session:
|
|
|
|
:type session:
|
2021-02-06 16:13:47 +01:00
|
|
|
:param ussd_session: A JSON serialized in-memory ussd session object
|
|
|
|
:type ussd_session: dict
|
|
|
|
:param user_input: The value a user enters in the ussd menu.
|
|
|
|
:type user_input: str
|
|
|
|
:return: A string value corresponding the successive give a specific state in the state machine.
|
|
|
|
"""
|
|
|
|
state_machine = UssdStateMachine(ussd_session=ussd_session)
|
2021-06-29 12:49:25 +02:00
|
|
|
state_machine.scan_data((user_input, ussd_session, account, session))
|
2021-02-06 16:13:47 +01:00
|
|
|
new_state = state_machine.state
|
|
|
|
|
|
|
|
return new_state
|
|
|
|
|
|
|
|
|
2021-06-07 10:02:03 +02:00
|
|
|
def process_exit_invalid_menu_option(display_key: str, preferred_language: str):
|
|
|
|
return translation_for(
|
|
|
|
key=display_key,
|
|
|
|
preferred_language=preferred_language,
|
|
|
|
support_phone=Support.phone_number
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-02-06 16:13:47 +01:00
|
|
|
def custom_display_text(
|
2021-06-29 12:49:25 +02:00
|
|
|
account: Account,
|
2021-02-06 16:13:47 +01:00
|
|
|
display_key: str,
|
|
|
|
menu_name: str,
|
2021-06-29 12:49:25 +02:00
|
|
|
session: Session,
|
|
|
|
ussd_session: dict) -> str:
|
2021-02-06 16:13:47 +01:00
|
|
|
"""This function extracts the appropriate session data based on the current menu name. It then inserts them as
|
|
|
|
keywords in the i18n function.
|
2021-06-29 12:49:25 +02:00
|
|
|
:param account: The account in a running USSD session.
|
|
|
|
:type account: Account
|
2021-02-06 16:13:47 +01:00
|
|
|
:param display_key: The path in the translation files defining an appropriate ussd response
|
|
|
|
:type display_key: str
|
|
|
|
:param menu_name: The name by which a specific menu can be identified.
|
|
|
|
:type menu_name: str
|
2021-06-29 12:49:25 +02:00
|
|
|
:param session:
|
|
|
|
:type session:
|
2021-02-06 16:13:47 +01:00
|
|
|
:param ussd_session: A JSON serialized in-memory ussd session object
|
|
|
|
:type ussd_session: dict
|
|
|
|
:return: A string value corresponding the ussd menu's text value.
|
|
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
if menu_name == 'transaction_pin_authorization':
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_transaction_pin_authorization(
|
|
|
|
account=account,
|
|
|
|
display_key=display_key,
|
|
|
|
session=session,
|
|
|
|
ussd_session=ussd_session)
|
2021-02-06 16:13:47 +01:00
|
|
|
elif menu_name == 'exit_insufficient_balance':
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_exit_insufficient_balance(
|
|
|
|
account=account,
|
|
|
|
display_key=display_key,
|
|
|
|
session=session,
|
|
|
|
ussd_session=ussd_session)
|
2021-02-06 16:13:47 +01:00
|
|
|
elif menu_name == 'exit_successful_transaction':
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_exit_successful_transaction(
|
|
|
|
account=account,
|
|
|
|
display_key=display_key,
|
|
|
|
session=session,
|
|
|
|
ussd_session=ussd_session)
|
2021-02-06 16:13:47 +01:00
|
|
|
elif menu_name == 'start':
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_start_menu(display_key=display_key, user=account)
|
2021-03-04 17:47:13 +01:00
|
|
|
elif 'pin_authorization' in menu_name:
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_pin_authorization(
|
|
|
|
account=account,
|
|
|
|
display_key=display_key,
|
|
|
|
session=session)
|
2021-04-06 19:53:38 +02:00
|
|
|
elif 'enter_current_pin' in menu_name:
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_pin_authorization(
|
|
|
|
account=account,
|
|
|
|
display_key=display_key,
|
|
|
|
session=session)
|
2021-03-04 17:47:13 +01:00
|
|
|
elif menu_name == 'account_balances':
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_account_balances(display_key=display_key, user=account)
|
2021-03-04 17:47:13 +01:00
|
|
|
elif 'transaction_set' in menu_name:
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_account_statement(display_key=display_key, user=account)
|
2021-04-06 19:53:38 +02:00
|
|
|
elif menu_name == 'display_user_metadata':
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_display_user_metadata(display_key=display_key, user=account)
|
2021-06-07 10:02:03 +02:00
|
|
|
elif menu_name == 'exit_invalid_menu_option':
|
2021-06-29 12:49:25 +02:00
|
|
|
return process_exit_invalid_menu_option(display_key=display_key, preferred_language=account.preferred_language)
|
2021-02-06 16:13:47 +01:00
|
|
|
else:
|
2021-06-29 12:49:25 +02:00
|
|
|
return translation_for(key=display_key, preferred_language=account.preferred_language)
|