The great bump

This commit is contained in:
2021-08-06 16:29:01 +00:00
parent f764b73f66
commit 0672a17d2e
195 changed files with 5791 additions and 4983 deletions

View File

@@ -0,0 +1,248 @@
# standard imports
import json
import logging
from typing import Tuple
# third-party imports
import celery
import i18n
from chainlib.hash import strip_0x
from cic_types.models.person import get_contact_data_from_vcard, generate_vcard_from_contact_data, manage_identity_data
# local imports
from cic_ussd.account.chain import Chain
from cic_ussd.account.maps import gender, language
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.error import MetadataNotFoundError
from cic_ussd.metadata import PersonMetadata
from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.translation import translation_for
from sqlalchemy.orm.session import Session
logg = logging.getLogger(__file__)
def change_preferred_language(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
r_user_input = language().get(user_input)
session = SessionBase.bind_session(session)
account.preferred_language = r_user_input
session.add(account)
session.flush()
SessionBase.release_session(session)
preferences_data = {
'preferred_language': r_user_input
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[account.blockchain_address, preferences_data],
queue='cic-ussd'
)
return s.apply_async()
def update_account_status_to_active(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function sets user's account to active.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, account, session = state_machine_data
session = SessionBase.bind_session(session=session)
account.activate_account()
session.add(account)
session.flush()
SessionBase.release_session(session=session)
def parse_gender(account: Account, user_input: str):
"""
:param account:
:type account:
:param user_input:
:type user_input:
:return:
:rtype:
"""
preferred_language = get_cached_preferred_language(account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
r_user_input = gender().get(user_input)
return translation_for(f'helpers.{r_user_input}', preferred_language)
def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function saves first name data to the ussd session in the redis cache.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, account, session = state_machine_data
session = SessionBase.bind_session(session=session)
current_state = ussd_session.get('state')
key = ''
if 'given_name' in current_state:
key = 'given_name'
if 'date_of_birth' in current_state:
key = 'date_of_birth'
if 'family_name' in current_state:
key = 'family_name'
if 'gender' in current_state:
key = 'gender'
user_input = parse_gender(account, user_input)
if 'location' in current_state:
key = 'location'
if 'products' in current_state:
key = 'products'
if ussd_session.get('data'):
data = ussd_session.get('data')
data[key] = user_input
else:
data = {
key: user_input
}
save_session_data('cic-ussd', session, data, ussd_session)
SessionBase.release_session(session)
def parse_person_metadata(account: Account, metadata: dict):
"""
:param account:
:type account:
:param metadata:
:type metadata:
:return:
:rtype:
"""
set_gender = metadata.get('gender')
given_name = metadata.get('given_name')
family_name = metadata.get('family_name')
email = metadata.get('email')
if isinstance(metadata.get('date_of_birth'), dict):
date_of_birth = metadata.get('date_of_birth')
else:
date_of_birth = {
"year": int(metadata.get('date_of_birth')[:4])
}
if isinstance(metadata.get('location'), dict):
location = metadata.get('location')
else:
location = {
"area_name": metadata.get('location')
}
if isinstance(metadata.get('products'), list):
products = metadata.get('products')
else:
products = metadata.get('products').split(',')
phone_number = account.phone_number
date_registered = int(account.created.replace().timestamp())
blockchain_address = account.blockchain_address
chain_spec = f'{Chain.spec.common_name()}:{Chain.spec.engine()}: {Chain.spec.chain_id()}'
if isinstance(metadata.get('identities'), dict):
identities = metadata.get('identities')
else:
identities = manage_identity_data(
blockchain_address=blockchain_address,
blockchain_type=Chain.spec.engine(),
chain_spec=chain_spec
)
return {
"date_registered": date_registered,
"date_of_birth": date_of_birth,
"gender": set_gender,
"identities": identities,
"location": location,
"products": products,
"vcard": generate_vcard_from_contact_data(
email=email,
family_name=family_name,
given_name=given_name,
tel=phone_number
)
}
def save_complete_person_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function persists elements of the user metadata stored in session data
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, account, session = state_machine_data
metadata = ussd_session.get('data')
person_metadata = parse_person_metadata(account, metadata)
blockchain_address = account.blockchain_address
s_create_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.create_person_metadata', [blockchain_address, person_metadata], queue='cic-ussd')
s_create_person_metadata.apply_async()
def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, account, session = state_machine_data
blockchain_address = account.blockchain_address
identifier = bytes.fromhex(strip_0x(blockchain_address))
person_metadata = PersonMetadata(identifier)
cached_person_metadata = person_metadata.get_cached_metadata()
if not cached_person_metadata:
raise MetadataNotFoundError(f'Expected user metadata but found none in cache for key: {blockchain_address}')
person_metadata = json.loads(cached_person_metadata)
data = ussd_session.get('data')
contact_data = {}
vcard = person_metadata.get('vcard')
if vcard:
contact_data = get_contact_data_from_vcard(vcard)
person_metadata.pop('vcard')
given_name = data.get('given_name') or contact_data.get('given')
family_name = data.get('family_name') or contact_data.get('family')
date_of_birth = data.get('date_of_birth') or person_metadata.get('date_of_birth')
set_gender = data.get('gender') or person_metadata.get('gender')
location = data.get('location') or person_metadata.get('location')
products = data.get('products') or person_metadata.get('products')
if isinstance(date_of_birth, str):
year = int(date_of_birth)
person_metadata['date_of_birth'] = {'year': year}
person_metadata['gender'] = set_gender
person_metadata['given_name'] = given_name
person_metadata['family_name'] = family_name
if isinstance(location, str):
location_data = person_metadata.get('location')
location_data['area_name'] = location
person_metadata['location'] = location_data
person_metadata['products'] = products
if contact_data:
contact_data.pop('given')
contact_data.pop('family')
contact_data.pop('tel')
person_metadata = {**person_metadata, **contact_data}
parsed_person_metadata = parse_person_metadata(account, person_metadata)
s_edit_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.create_person_metadata',
[blockchain_address, parsed_person_metadata]
)
s_edit_person_metadata.apply_async(queue='cic-ussd')

View File

@@ -1,21 +0,0 @@
# standard imports
import logging
from typing import Tuple
# third-party imports
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.db.models.account import Account
logg = logging.getLogger(__file__)
def process_mini_statement_request(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function compiles a brief statement of a user's last three inbound and outbound transactions and send the
same as a message on their selected avenue for notification.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str
"""
user_input, ussd_session, user, session = state_machine_data
logg.debug('This section requires integration with cic-eth. (The last 6 transactions would be sent as an sms.)')

View File

@@ -5,44 +5,47 @@ ussd menu facilitating the return of appropriate menu responses based on said us
# standard imports
from typing import Tuple
# external imports
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.db.models.account import Account
def menu_one_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
def menu_one_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks that user input matches a string with value '1'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str
:return: A user input's match with '1'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
return user_input == '1'
def menu_two_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
def menu_two_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks that user input matches a string with value '2'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A user input's match with '2'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
return user_input == '2'
def menu_three_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
def menu_three_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""This function checks that user input matches a string with value '3'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
:return: A user input's match with '3'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
return user_input == '3'
def menu_four_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
def menu_four_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""
This function checks that user input matches a string with value '4'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -50,11 +53,11 @@ def menu_four_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '4'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
return user_input == '4'
def menu_five_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
def menu_five_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""
This function checks that user input matches a string with value '5'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -62,11 +65,11 @@ def menu_five_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '5'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
return user_input == '5'
def menu_six_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
def menu_six_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""
This function checks that user input matches a string with value '6'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -74,11 +77,11 @@ def menu_six_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
:return: A user input's match with '6'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
return user_input == '6'
def menu_zero_zero_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
def menu_zero_zero_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""
This function checks that user input matches a string with value '00'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -86,11 +89,11 @@ def menu_zero_zero_selected(state_machine_data: Tuple[str, dict, Account]) -> bo
:return: A user input's match with '00'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
return user_input == '00'
def menu_ninety_nine_selected(state_machine_data: Tuple[str, dict, Account]) -> bool:
def menu_ninety_nine_selected(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
"""
This function checks that user input matches a string with value '99'
:param state_machine_data: A tuple containing user input, a ussd session and user object.
@@ -98,5 +101,5 @@ def menu_ninety_nine_selected(state_machine_data: Tuple[str, dict, Account]) ->
:return: A user input's match with '99'
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
return user_input == '99'

View File

@@ -16,8 +16,7 @@ from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.enum import AccountStatus
from cic_ussd.encoder import create_password_hash, check_password_hash
from cic_ussd.operations import persist_session_to_db_task, create_or_update_session
from cic_ussd.redis import InMemoryStore
from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session
logg = logging.getLogger(__file__)
@@ -31,7 +30,7 @@ def is_valid_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool
:return: A pin's validity
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
pin_is_valid = False
matcher = r'^\d{4}$'
if re.match(matcher, user_input):
@@ -46,8 +45,11 @@ def is_authorized_pin(state_machine_data: Tuple[str, dict, Account, Session]) ->
:return: A match between two pin values.
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
return user.verify_password(password=user_input)
user_input, ussd_session, account, session = state_machine_data
is_verified_password = account.verify_password(password=user_input)
if not is_verified_password:
account.failed_pin_attempts += 1
return is_verified_password
def is_locked_account(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
@@ -57,8 +59,8 @@ def is_locked_account(state_machine_data: Tuple[str, dict, Account, Session]) ->
:return: A match between two pin values.
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
return user.get_account_status() == AccountStatus.LOCKED.name
user_input, ussd_session, account, session = state_machine_data
return account.get_status(session) == AccountStatus.LOCKED.name
def save_initial_pin_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -67,38 +69,25 @@ def save_initial_pin_to_session_data(state_machine_data: Tuple[str, dict, Accoun
:type state_machine_data: tuple
"""
user_input, ussd_session, user, session = state_machine_data
# define redis cache entry point
cache = InMemoryStore.cache
# get external session id
external_session_id = ussd_session.get('external_session_id')
# get corresponding session record
in_redis_ussd_session = cache.get(external_session_id)
in_redis_ussd_session = json.loads(in_redis_ussd_session)
# set initial pin data
initial_pin = create_password_hash(user_input)
if ussd_session.get('session_data'):
session_data = ussd_session.get('session_data')
session_data['initial_pin'] = initial_pin
if ussd_session.get('data'):
data = ussd_session.get('data')
data['initial_pin'] = initial_pin
else:
session_data = {
data = {
'initial_pin': initial_pin
}
# create new in memory ussd session with current ussd session data
external_session_id = ussd_session.get('external_session_id')
create_or_update_session(
external_session_id=external_session_id,
phone=in_redis_ussd_session.get('msisdn'),
service_code=in_redis_ussd_session.get('service_code'),
msisdn=ussd_session.get('msisdn'),
service_code=ussd_session.get('service_code'),
user_input=user_input,
current_menu=in_redis_ussd_session.get('state'),
state=ussd_session.get('state'),
session=session,
session_data=session_data
data=data
)
persist_session_to_db_task(external_session_id=external_session_id, queue='cic-ussd')
persist_ussd_session(external_session_id, 'cic-ussd')
def pins_match(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
@@ -109,7 +98,7 @@ def pins_match(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
initial_pin = ussd_session.get('session_data').get('initial_pin')
initial_pin = ussd_session.get('data').get('initial_pin')
return check_password_hash(user_input, initial_pin)
@@ -120,7 +109,7 @@ def complete_pin_change(state_machine_data: Tuple[str, dict, Account, Session]):
"""
user_input, ussd_session, user, session = state_machine_data
session = SessionBase.bind_session(session=session)
password_hash = ussd_session.get('session_data').get('initial_pin')
password_hash = ussd_session.get('data').get('initial_pin')
user.password_hash = password_hash
session.add(user)
session.flush()
@@ -134,8 +123,8 @@ def is_blocked_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bo
:return: A match between two pin values.
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
return user.get_account_status() == AccountStatus.LOCKED.name
user_input, ussd_session, account, session = state_machine_data
return account.get_status(session) == AccountStatus.LOCKED.name
def is_valid_new_pin(state_machine_data: Tuple[str, dict, Account, Session]) -> bool:

View File

@@ -1,23 +1,28 @@
# standard imports
import logging
from typing import Tuple
# external imports
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.tokens import get_default_token_symbol
from cic_ussd.db.models.account import Account
logg = logging.getLogger()
from cic_ussd.notifications import Notifier
from cic_ussd.phone_number import Support
def send_terms_to_user_if_required(state_machine_data: Tuple[str, dict, Account]):
user_input, ussd_session, user, session = state_machine_data
logg.debug('Requires integration to cic-notify.')
def process_mini_statement_request(state_machine_data: Tuple[str, dict, Account]):
user_input, ussd_session, user, session = state_machine_data
logg.debug('Requires integration to cic-notify.')
def upsell_unregistered_recipient(state_machine_data: Tuple[str, dict, Account]):
user_input, ussd_session, user, session = state_machine_data
logg.debug('Requires integration to cic-notify.')
def upsell_unregistered_recipient(state_machine_data: Tuple[str, dict, Account, Session]):
""""""
user_input, ussd_session, account, session = state_machine_data
notifier = Notifier()
phone_number = ussd_session.get('data')['recipient_phone_number']
preferred_language = get_cached_preferred_language(account.blockchain_address)
token_symbol = get_default_token_symbol()
tx_sender_information = account.standard_metadata_id()
notifier.send_sms_notification('sms.upsell_unregistered_recipient',
phone_number,
preferred_language,
tx_sender_information=tx_sender_information,
token_symbol=token_symbol,
support_phone=Support.phone_number)

View File

@@ -1,24 +1,21 @@
# standard imports
import json
import logging
from typing import Tuple
# third party imports
import celery
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.balance import compute_operational_balance
from cic_ussd.chain import Chain
from cic_ussd.account.balance import get_cached_available_balance
from cic_ussd.account.chain import Chain
from cic_ussd.account.tokens import get_default_token_symbol
from cic_ussd.account.transaction import OutgoingTransaction
from cic_ussd.db.enum import AccountStatus
from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.enum import AccountStatus
from cic_ussd.operations import save_to_in_memory_ussd_session_data
from cic_ussd.phone_number import process_phone_number, E164Format
from cic_ussd.processor import retrieve_token_symbol
from cic_ussd.redis import create_cached_data_key, get_cached_data
from cic_ussd.transactions import OutgoingTransactionProcessor
from cic_ussd.session.ussd_session import save_session_data
from sqlalchemy.orm.session import Session
logg = logging.getLogger(__file__)
@@ -31,13 +28,15 @@ def is_valid_recipient(state_machine_data: Tuple[str, dict, Account, Session]) -
:return: A user's validity
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
phone_number = process_phone_number(user_input, E164Format.region)
session = SessionBase.bind_session(session=session)
recipient = Account.get_by_phone_number(phone_number=phone_number, session=session)
SessionBase.release_session(session=session)
is_not_initiator = phone_number != user.phone_number
has_active_account_status = user.get_account_status() == AccountStatus.ACTIVE.name
is_not_initiator = phone_number != account.phone_number
has_active_account_status = False
if recipient:
has_active_account_status = recipient.get_status(session) == AccountStatus.ACTIVE.name
return is_not_initiator and has_active_account_status and recipient is not None
@@ -49,7 +48,7 @@ def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, Account, Se
:return: A transaction amount's validity
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
try:
return int(user_input) > 0
except ValueError:
@@ -64,16 +63,8 @@ def has_sufficient_balance(state_machine_data: Tuple[str, dict, Account, Session
:return: An account balance's validity
:rtype: bool
"""
user_input, ussd_session, user, session = state_machine_data
# get cached balance
key = create_cached_data_key(
identifier=bytes.fromhex(user.blockchain_address[2:]),
salt=':cic.balances_data'
)
cached_balance = get_cached_data(key=key)
operational_balance = compute_operational_balance(balances=json.loads(cached_balance))
return int(user_input) <= operational_balance
user_input, ussd_session, account, session = state_machine_data
return int(user_input) <= get_cached_available_balance(account.blockchain_address)
def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -81,17 +72,13 @@ def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, Ac
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
session_data = ussd_session.get('session_data') or {}
session_data = ussd_session.get('data') or {}
recipient_phone_number = process_phone_number(phone_number=user_input, region=E164Format.region)
session_data['recipient_phone_number'] = recipient_phone_number
save_to_in_memory_ussd_session_data(
queue='cic-ussd',
session=session,
session_data=session_data,
ussd_session=ussd_session)
save_session_data('cic-ussd', session, session_data, ussd_session)
def retrieve_recipient_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -101,18 +88,13 @@ def retrieve_recipient_metadata(state_machine_data: Tuple[str, dict, Account, Se
:return:
:rtype:
"""
user_input, ussd_session, user, session = state_machine_data
recipient_phone_number = process_phone_number(phone_number=user_input, region=E164Format.region)
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
user_input, ussd_session, account, session = state_machine_data
recipient_phone_number = process_phone_number(user_input, E164Format.region)
recipient = Account.get_by_phone_number(recipient_phone_number, session)
blockchain_address = recipient.blockchain_address
# retrieve and cache account's metadata
s_query_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata',
[blockchain_address]
)
s_query_person_metadata.apply_async(queue='cic-ussd')
'cic_ussd.tasks.metadata.query_person_metadata', [blockchain_address], queue='cic-ussd')
s_query_person_metadata.apply_async()
def save_transaction_amount_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -120,16 +102,11 @@ def save_transaction_amount_to_session_data(state_machine_data: Tuple[str, dict,
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
session_data = ussd_session.get('session_data') or {}
session_data = ussd_session.get('data') or {}
session_data['transaction_amount'] = user_input
save_to_in_memory_ussd_session_data(
queue='cic-ussd',
session=session,
session_data=session_data,
ussd_session=ussd_session)
save_session_data('cic-ussd', session, session_data, ussd_session)
def process_transaction_request(state_machine_data: Tuple[str, dict, Account, Session]):
@@ -137,20 +114,18 @@ def process_transaction_request(state_machine_data: Tuple[str, dict, Account, Se
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
# retrieve token symbol
chain_str = Chain.spec.__str__()
# get user from phone number
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
recipient_phone_number = ussd_session.get('data').get('recipient_phone_number')
recipient = Account.get_by_phone_number(phone_number=recipient_phone_number, session=session)
to_address = recipient.blockchain_address
from_address = user.blockchain_address
amount = int(ussd_session.get('session_data').get('transaction_amount'))
token_symbol = retrieve_token_symbol(chain_str=chain_str)
from_address = account.blockchain_address
amount = int(ussd_session.get('data').get('transaction_amount'))
token_symbol = get_default_token_symbol()
outgoing_tx_processor = OutgoingTransactionProcessor(chain_str=chain_str,
from_address=from_address,
to_address=to_address)
outgoing_tx_processor.process_outgoing_transfer_transaction(amount=amount, token_symbol=token_symbol)
outgoing_tx_processor = OutgoingTransaction(chain_str=chain_str,
from_address=from_address,
to_address=to_address)
outgoing_tx_processor.transfer(amount=amount, token_symbol=token_symbol)

View File

@@ -1,292 +0,0 @@
# standard imports
import json
import logging
from typing import Tuple
# third-party imports
import celery
from cic_types.models.person import generate_metadata_pointer
from cic_types.models.person import generate_vcard_from_contact_data, manage_identity_data
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.chain import Chain
from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.error import MetadataNotFoundError
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
from cic_ussd.operations import save_to_in_memory_ussd_session_data
from cic_ussd.redis import get_cached_data
logg = logging.getLogger(__file__)
def change_preferred_language_to_en(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function changes the user's preferred language to english.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, user, session = state_machine_data
session = SessionBase.bind_session(session=session)
user.preferred_language = 'en'
session.add(user)
session.flush()
SessionBase.release_session(session=session)
preferences_data = {
'preferred_language': 'en'
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[user.blockchain_address, preferences_data]
)
s.apply_async(queue='cic-ussd')
def change_preferred_language_to_sw(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function changes the user's preferred language to swahili.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, account, session = state_machine_data
session = SessionBase.bind_session(session=session)
account.preferred_language = 'sw'
session.add(account)
session.flush()
SessionBase.release_session(session=session)
preferences_data = {
'preferred_language': 'sw'
}
s = celery.signature(
'cic_ussd.tasks.metadata.add_preferences_metadata',
[account.blockchain_address, preferences_data]
)
s.apply_async(queue='cic-ussd')
def update_account_status_to_active(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function sets user's account to active.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, account, session = state_machine_data
session = SessionBase.bind_session(session=session)
account.activate_account()
session.add(account)
session.flush()
SessionBase.release_session(session=session)
def process_gender_user_input(user: Account, user_input: str):
"""
:param user:
:type user:
:param user_input:
:type user_input:
:return:
:rtype:
"""
gender = ""
if user.preferred_language == 'en':
if user_input == '1':
gender = 'Male'
elif user_input == '2':
gender = 'Female'
elif user_input == '3':
gender = 'Other'
else:
if user_input == '1':
gender = 'Mwanaume'
elif user_input == '2':
gender = 'Mwanamke'
elif user_input == '3':
gender = 'Nyingine'
return gender
def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function saves first name data to the ussd session in the redis cache.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, user, session = state_machine_data
session = SessionBase.bind_session(session=session)
# get current menu
current_state = ussd_session.get('state')
# define session data key from current state
key = ''
if 'given_name' in current_state:
key = 'given_name'
if 'date_of_birth' in current_state:
key = 'date_of_birth'
if 'family_name' in current_state:
key = 'family_name'
if 'gender' in current_state:
key = 'gender'
user_input = process_gender_user_input(user=user, user_input=user_input)
if 'location' in current_state:
key = 'location'
if 'products' in current_state:
key = 'products'
# check if there is existing session data
if ussd_session.get('session_data'):
session_data = ussd_session.get('session_data')
session_data[key] = user_input
else:
session_data = {
key: user_input
}
save_to_in_memory_ussd_session_data(
queue='cic-ussd',
session=session,
session_data=session_data,
ussd_session=ussd_session)
def format_user_metadata(metadata: dict, user: Account):
"""
:param metadata:
:type metadata:
:param user:
:type user:
:return:
:rtype:
"""
gender = metadata.get('gender')
given_name = metadata.get('given_name')
family_name = metadata.get('family_name')
if isinstance(metadata.get('date_of_birth'), dict):
date_of_birth = metadata.get('date_of_birth')
else:
date_of_birth = {
"year": int(metadata.get('date_of_birth')[:4])
}
# check whether there's existing location data
if isinstance(metadata.get('location'), dict):
location = metadata.get('location')
else:
location = {
"area_name": metadata.get('location')
}
# check whether it is a list
if isinstance(metadata.get('products'), list):
products = metadata.get('products')
else:
products = metadata.get('products').split(',')
phone_number = user.phone_number
date_registered = int(user.created.replace().timestamp())
blockchain_address = user.blockchain_address
chain_spec = f'{Chain.spec.common_name()}:{Chain.spec.network_id()}'
identities = manage_identity_data(
blockchain_address=blockchain_address,
blockchain_type=Chain.spec.engine(),
chain_spec=chain_spec
)
return {
"date_registered": date_registered,
"date_of_birth": date_of_birth,
"gender": gender,
"identities": identities,
"location": location,
"products": products,
"vcard": generate_vcard_from_contact_data(
family_name=family_name,
given_name=given_name,
tel=phone_number
)
}
def save_complete_user_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function persists elements of the user metadata stored in session data
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: tuple
"""
user_input, ussd_session, user, session = state_machine_data
# get session data
metadata = ussd_session.get('session_data')
# format metadata appropriately
user_metadata = format_user_metadata(metadata=metadata, user=user)
blockchain_address = user.blockchain_address
s_create_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.create_person_metadata',
[blockchain_address, user_metadata]
)
s_create_person_metadata.apply_async(queue='cic-ussd')
def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, Account, Session]):
user_input, ussd_session, user, session = state_machine_data
blockchain_address = user.blockchain_address
key = generate_metadata_pointer(
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
cic_type=':cic.person'
)
user_metadata = get_cached_data(key=key)
if not user_metadata:
raise MetadataNotFoundError(f'Expected user metadata but found none in cache for key: {blockchain_address}')
given_name = ussd_session.get('session_data').get('given_name')
family_name = ussd_session.get('session_data').get('family_name')
date_of_birth = ussd_session.get('session_data').get('date_of_birth')
gender = ussd_session.get('session_data').get('gender')
location = ussd_session.get('session_data').get('location')
products = ussd_session.get('session_data').get('products')
# validate user metadata
user_metadata = json.loads(user_metadata)
# edit specific metadata attribute
if given_name:
user_metadata['given_name'] = given_name
if family_name:
user_metadata['family_name'] = family_name
if date_of_birth and len(date_of_birth) == 4:
year = int(date_of_birth[:4])
user_metadata['date_of_birth'] = {
'year': year
}
if gender:
user_metadata['gender'] = gender
if location:
# get existing location metadata:
location_data = user_metadata.get('location')
location_data['area_name'] = location
user_metadata['location'] = location_data
if products:
user_metadata['products'] = products
user_metadata = format_user_metadata(metadata=user_metadata, user=user)
s_edit_person_metadata = celery.signature(
'cic_ussd.tasks.metadata.create_person_metadata',
[blockchain_address, user_metadata]
)
s_edit_person_metadata.apply_async(queue='cic-ussd')
def get_user_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
user_input, ussd_session, user, session = state_machine_data
blockchain_address = user.blockchain_address
s_get_user_metadata = celery.signature(
'cic_ussd.tasks.metadata.query_person_metadata',
[blockchain_address]
)
s_get_user_metadata.apply_async(queue='cic-ussd')

View File

@@ -4,67 +4,58 @@ import re
from typing import Tuple
# third-party imports
from cic_types.models.person import generate_metadata_pointer
from chainlib.hash import strip_0x
from sqlalchemy.orm.session import Session
# local imports
from cic_ussd.db.models.account import Account
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
from cic_ussd.redis import get_cached_data
from cic_ussd.metadata import PersonMetadata
logg = logging.getLogger()
def has_cached_user_metadata(state_machine_data: Tuple[str, dict, Account]):
def has_cached_person_metadata(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function checks whether the attributes of the user's metadata constituting a profile are filled out.
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str
"""
user_input, ussd_session, user, session = state_machine_data
# check for user metadata in cache
key = generate_metadata_pointer(
identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
cic_type=':cic.person'
)
user_metadata = get_cached_data(key=key)
return user_metadata is not None
user_input, ussd_session, account, session = state_machine_data
identifier = bytes.fromhex(strip_0x(account.blockchain_address))
metadata_client = PersonMetadata(identifier)
return metadata_client.get_cached_metadata() is not None
def is_valid_name(state_machine_data: Tuple[str, dict, Account]):
def is_valid_name(state_machine_data: Tuple[str, dict, Account, Session]):
"""This function checks that a user provided name is valid
:param state_machine_data: A tuple containing user input, a ussd session and user object.
:type state_machine_data: str
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
name_matcher = "^[a-zA-Z]+$"
valid_name = re.match(name_matcher, user_input)
if valid_name:
return True
else:
return False
return bool(valid_name)
def is_valid_gender_selection(state_machine_data: Tuple[str, dict, Account]):
def is_valid_gender_selection(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, user, session = state_machine_data
selection_matcher = "^[1-2]$"
if re.match(selection_matcher, user_input):
return True
else:
return False
user_input, ussd_session, account, session = state_machine_data
selection_matcher = "^[1-3]$"
return bool(re.match(selection_matcher, user_input))
def is_valid_date(state_machine_data: Tuple[str, dict, Account]):
def is_valid_date(state_machine_data: Tuple[str, dict, Account, Session]):
"""
:param state_machine_data:
:type state_machine_data:
:return:
:rtype:
"""
user_input, ussd_session, user, session = state_machine_data
user_input, ussd_session, account, session = state_machine_data
# For MVP this value is defaulting to year
return len(user_input) == 4 and int(user_input) >= 1900