cic-stack/apps/cic-ussd/cic_ussd/processor/menu.py

327 lines
14 KiB
Python

# standard imports
import json
import logging
from datetime import datetime, timedelta
# external imports
import i18n.config
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.account.balance import (calculate_available_balance,
get_adjusted_balance,
get_balances,
get_cached_adjusted_balance,
get_cached_available_balance)
from cic_ussd.account.chain import Chain
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.statement import (
get_cached_statement,
parse_statement_transactions,
query_statement,
statement_transaction_set
)
from cic_ussd.account.tokens import get_default_token_symbol
from cic_ussd.account.transaction import from_wei, to_wei
from cic_ussd.cache import cache_data_key, cache_data
from cic_ussd.db.models.account import Account
from cic_ussd.metadata import PersonMetadata
from cic_ussd.phone_number import Support
from cic_ussd.processor.util import parse_person_metadata
from cic_ussd.translation import translation_for
from sqlalchemy.orm.session import Session
logg = logging.getLogger(__name__)
class MenuProcessor:
def __init__(self, account: Account, display_key: str, menu_name: str, session: Session, ussd_session: dict):
self.account = account
self.display_key = display_key
self.identifier = bytes.fromhex(self.account.blockchain_address)
self.menu_name = menu_name
self.session = session
self.ussd_session = ussd_session
def account_balances(self) -> str:
"""
:return:
:rtype:
"""
available_balance = get_cached_available_balance(self.account.blockchain_address)
adjusted_balance = get_cached_adjusted_balance(self.identifier)
token_symbol = get_default_token_symbol()
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
with_available_balance = f'{self.display_key}.available_balance'
with_fees = f'{self.display_key}.with_fees'
if not adjusted_balance:
return translation_for(key=with_available_balance,
preferred_language=preferred_language,
available_balance=available_balance,
token_symbol=token_symbol)
adjusted_balance = json.loads(adjusted_balance)
tax_wei = to_wei(int(available_balance)) - int(adjusted_balance)
tax = from_wei(int(tax_wei))
return translation_for(key=with_fees,
preferred_language=preferred_language,
available_balance=available_balance,
tax=tax,
token_symbol=token_symbol)
def account_statement(self) -> str:
"""
:return:
:rtype:
"""
cached_statement = get_cached_statement(self.account.blockchain_address)
statement = json.loads(cached_statement)
statement_transactions = parse_statement_transactions(statement)
transaction_sets = [statement_transactions[tx:tx + 3] for tx in range(0, len(statement_transactions), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
first_transaction_set = []
middle_transaction_set = []
last_transaction_set = []
if transaction_sets:
first_transaction_set = statement_transaction_set(preferred_language, transaction_sets[0])
if len(transaction_sets) >= 2:
middle_transaction_set = statement_transaction_set(preferred_language, transaction_sets[1])
if len(transaction_sets) >= 3:
last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2])
if self.display_key == 'ussd.kenya.first_transaction_set':
return translation_for(
self.display_key, preferred_language, first_transaction_set=first_transaction_set
)
if self.display_key == 'ussd.kenya.middle_transaction_set':
return translation_for(
self.display_key, preferred_language, middle_transaction_set=middle_transaction_set
)
if self.display_key == 'ussd.kenya.last_transaction_set':
return translation_for(
self.display_key, preferred_language, last_transaction_set=last_transaction_set
)
def help(self):
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
return translation_for(self.display_key, preferred_language, support_phone=Support.phone_number)
def person_metadata(self) -> str:
"""
:return:
:rtype:
"""
person_metadata = PersonMetadata(self.identifier)
cached_person_metadata = person_metadata.get_cached_metadata()
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
if cached_person_metadata:
return parse_person_metadata(cached_person_metadata, self.display_key, preferred_language)
absent = translation_for('helpers.not_provided', preferred_language)
return translation_for(
self.display_key,
preferred_language,
full_name=absent,
gender=absent,
age=absent,
location=absent,
products=absent
)
def pin_authorization(self, **kwargs) -> str:
"""
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
if self.account.failed_pin_attempts == 0:
return translation_for(f'{self.display_key}.first', preferred_language, **kwargs)
remaining_attempts = 3
remaining_attempts -= self.account.failed_pin_attempts
retry_pin_entry = translation_for(
'ussd.kenya.retry_pin_entry', preferred_language, remaining_attempts=remaining_attempts
)
return translation_for(
f'{self.display_key}.retry', preferred_language, retry_pin_entry=retry_pin_entry
)
def start_menu(self):
"""
:return:
:rtype:
"""
chain_str = Chain.spec.__str__()
token_symbol = get_default_token_symbol()
blockchain_address = self.account.blockchain_address
balances = get_balances(blockchain_address, chain_str, token_symbol, False)[0]
key = cache_data_key(self.identifier, MetadataPointer.BALANCES)
cache_data(key, json.dumps(balances))
available_balance = calculate_available_balance(balances)
now = datetime.now()
if (now - self.account.created).days >= 30:
if available_balance <= 0:
logg.info(f'Not retrieving adjusted balance, available balance: {available_balance} is insufficient.')
else:
timestamp = int((now - timedelta(30)).timestamp())
adjusted_balance = get_adjusted_balance(to_wei(int(available_balance)), chain_str, timestamp, token_symbol)
key = cache_data_key(self.identifier, MetadataPointer.BALANCES_ADJUSTED)
cache_data(key, json.dumps(adjusted_balance))
query_statement(blockchain_address)
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
return translation_for(
self.display_key, preferred_language, account_balance=available_balance, account_token_name=token_symbol
)
def transaction_pin_authorization(self) -> str:
"""
:return:
:rtype:
"""
recipient_phone_number = self.ussd_session.get('data').get('recipient_phone_number')
recipient = Account.get_by_phone_number(recipient_phone_number, self.session)
tx_recipient_information = recipient.standard_metadata_id()
tx_sender_information = self.account.standard_metadata_id()
token_symbol = get_default_token_symbol()
user_input = self.ussd_session.get('data').get('transaction_amount')
transaction_amount = to_wei(value=int(user_input))
return self.pin_authorization(
recipient_information=tx_recipient_information,
transaction_amount=from_wei(transaction_amount),
token_symbol=token_symbol,
sender_information=tx_sender_information
)
def exit_insufficient_balance(self):
"""
:return:
:rtype:
"""
available_balance = get_cached_available_balance(self.account.blockchain_address)
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
session_data = self.ussd_session.get('data')
transaction_amount = session_data.get('transaction_amount')
transaction_amount = to_wei(value=int(transaction_amount))
token_symbol = get_default_token_symbol()
recipient_phone_number = self.ussd_session.get('data').get('recipient_phone_number')
recipient = Account.get_by_phone_number(recipient_phone_number, self.session)
tx_recipient_information = recipient.standard_metadata_id()
return translation_for(
self.display_key,
preferred_language,
amount=from_wei(transaction_amount),
token_symbol=token_symbol,
recipient_information=tx_recipient_information,
token_balance=available_balance
)
def exit_invalid_menu_option(self):
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
return translation_for(self.display_key, preferred_language, support_phone=Support.phone_number)
def exit_pin_blocked(self):
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
return translation_for('ussd.kenya.exit_pin_blocked', preferred_language, support_phone=Support.phone_number)
def exit_successful_transaction(self):
"""
:return:
:rtype:
"""
amount = int(self.ussd_session.get('data').get('transaction_amount'))
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
transaction_amount = to_wei(amount)
token_symbol = get_default_token_symbol()
recipient_phone_number = self.ussd_session.get('data').get('recipient_phone_number')
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=self.session)
tx_recipient_information = recipient.standard_metadata_id()
tx_sender_information = self.account.standard_metadata_id()
return translation_for(
self.display_key,
preferred_language,
transaction_amount=from_wei(transaction_amount),
token_symbol=token_symbol,
recipient_information=tx_recipient_information,
sender_information=tx_sender_information
)
def response(account: Account, display_key: str, menu_name: str, session: Session, ussd_session: dict) -> str:
"""This function extracts the appropriate session data based on the current menu name. It then inserts them as
keywords in the i18n function.
:param account: The account in a running USSD session.
:type account: Account
: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
:param session:
:type session:
: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
"""
menu_processor = MenuProcessor(account, display_key, menu_name, session, ussd_session)
if menu_name == 'start':
return menu_processor.start_menu()
if menu_name == 'help':
return menu_processor.help()
if menu_name == 'transaction_pin_authorization':
return menu_processor.transaction_pin_authorization()
if menu_name == 'exit_insufficient_balance':
return menu_processor.exit_insufficient_balance()
if menu_name == 'exit_successful_transaction':
return menu_processor.exit_successful_transaction()
if menu_name == 'account_balances':
return menu_processor.account_balances()
if 'pin_authorization' in menu_name:
return menu_processor.pin_authorization()
if 'enter_current_pin' in menu_name:
return menu_processor.pin_authorization()
if 'transaction_set' in menu_name:
return menu_processor.account_statement()
if menu_name == 'display_user_metadata':
return menu_processor.person_metadata()
if menu_name == 'exit_invalid_menu_option':
return menu_processor.exit_invalid_menu_option()
if menu_name == 'exit_pin_blocked':
return menu_processor.exit_pin_blocked()
preferred_language = get_cached_preferred_language(account.blockchain_address)
return translation_for(display_key, preferred_language)