Merge branch 'lash/fix-tx-list' into 'master'
Make tx listing tasks work properly See merge request grassrootseconomics/cic-internal-integration!45
This commit is contained in:
		
						commit
						aa99b16ad2
					
				@ -149,6 +149,9 @@ def tx_collate(tx_batches, chain_str, offset, limit, newest_first=True):
 | 
			
		||||
    txs_by_block = {}
 | 
			
		||||
    chain_spec = ChainSpec.from_chain_str(chain_str)
 | 
			
		||||
 | 
			
		||||
    if isinstance(tx_batches, dict):
 | 
			
		||||
        tx_batches = [tx_batches]
 | 
			
		||||
 | 
			
		||||
    for b in tx_batches:
 | 
			
		||||
        for v in b.values():
 | 
			
		||||
            tx = None
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ version = (
 | 
			
		||||
        0,
 | 
			
		||||
        10,
 | 
			
		||||
        0,
 | 
			
		||||
        'alpha.37',
 | 
			
		||||
        'alpha.38',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
version_object = semver.VersionInfo(
 | 
			
		||||
 | 
			
		||||
@ -12,5 +12,3 @@ MENU_FILE=/usr/local/lib/python3.8/site-packages/cic_ussd/db/ussd_menu.json
 | 
			
		||||
[statemachine]
 | 
			
		||||
STATES=/usr/src/cic-ussd/states/
 | 
			
		||||
TRANSITIONS=/usr/src/cic-ussd/transitions/
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,5 @@
 | 
			
		||||
[cic]
 | 
			
		||||
chain_spec = Bloxberg:8995
 | 
			
		||||
engine = evm
 | 
			
		||||
common_name = bloxberg
 | 
			
		||||
network_id = 8996
 | 
			
		||||
meta_url = http://localhost:63380
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								apps/cic-ussd/.config/pgp.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/cic-ussd/.config/pgp.ini
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
[keys]
 | 
			
		||||
public = ../cic-internal-integration/apps/contract-migration/testdata/pgp/publickeys_meta.asc
 | 
			
		||||
private = ../cic-internal-integration/apps/contract-migration/testdata/pgp/privatekeys_meta.asc
 | 
			
		||||
passphrase = merman
 | 
			
		||||
@ -7,8 +7,8 @@ PASSWORD_PEPPER=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=
 | 
			
		||||
SERVICE_CODE=*483*46#
 | 
			
		||||
 | 
			
		||||
[ussd]
 | 
			
		||||
MENU_FILE=cic_ussd/db/ussd_menu.json
 | 
			
		||||
MENU_FILE=/usr/local/lib/python3.8/site-packages/cic_ussd/db/ussd_menu.json
 | 
			
		||||
 | 
			
		||||
[statemachine]
 | 
			
		||||
STATES=states/
 | 
			
		||||
TRANSITIONS=transitions/
 | 
			
		||||
STATES=/usr/src/cic-ussd/states/
 | 
			
		||||
TRANSITIONS=/usr/src/cic-ussd/transitions/
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,5 @@
 | 
			
		||||
[cic]
 | 
			
		||||
chain_spec = Bloxberg:8995
 | 
			
		||||
engine = evm
 | 
			
		||||
common_name = bloxberg
 | 
			
		||||
network_id = 8996
 | 
			
		||||
meta_url = http://localhost:63380
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								apps/cic-ussd/.config/test/pgp.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/cic-ussd/.config/test/pgp.ini
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
[keys]
 | 
			
		||||
public = ../cic-internal-integration/apps/contract-migration/testdata/pgp/publickeys_meta.asc
 | 
			
		||||
private = ../cic-internal-integration/apps/contract-migration/testdata/pgp/privatekeys_meta.asc
 | 
			
		||||
passphrase = merman
 | 
			
		||||
							
								
								
									
										49
									
								
								apps/cic-ussd/cic_ussd/account.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								apps/cic-ussd/cic_ussd/account.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
from cic_eth.api import Api
 | 
			
		||||
from cic_types.models.person import Person
 | 
			
		||||
from cic_types.processor import generate_metadata_pointer
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.db.models.user import User
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
 | 
			
		||||
from cic_ussd.redis import get_cached_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def define_account_tx_metadata(user: User):
 | 
			
		||||
    # get sender metadata
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(
 | 
			
		||||
        blockchain_address=user.blockchain_address
 | 
			
		||||
    )
 | 
			
		||||
    key = generate_metadata_pointer(
 | 
			
		||||
        identifier=identifier,
 | 
			
		||||
        cic_type='cic.person'
 | 
			
		||||
    )
 | 
			
		||||
    account_metadata = get_cached_data(key=key)
 | 
			
		||||
 | 
			
		||||
    if account_metadata:
 | 
			
		||||
        account_metadata = json.loads(account_metadata)
 | 
			
		||||
        person = Person()
 | 
			
		||||
        deserialized_person = person.deserialize(metadata=account_metadata)
 | 
			
		||||
        given_name = deserialized_person.given_name
 | 
			
		||||
        family_name = deserialized_person.family_name
 | 
			
		||||
        phone_number = deserialized_person.tel
 | 
			
		||||
 | 
			
		||||
        return f'{given_name} {family_name} {phone_number}'
 | 
			
		||||
    else:
 | 
			
		||||
        phone_number = user.phone_number
 | 
			
		||||
        return phone_number
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def retrieve_account_statement(blockchain_address: str):
 | 
			
		||||
    chain_str = Chain.spec.__str__()
 | 
			
		||||
    cic_eth_api = Api(
 | 
			
		||||
        chain_str=chain_str,
 | 
			
		||||
        callback_queue='cic-ussd',
 | 
			
		||||
        callback_task='cic_ussd.tasks.callback_handler.process_statement_callback',
 | 
			
		||||
        callback_param=blockchain_address
 | 
			
		||||
    )
 | 
			
		||||
    result = cic_eth_api.list(address=blockchain_address, limit=9)
 | 
			
		||||
@ -1,39 +0,0 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import logging
 | 
			
		||||
from collections import deque
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
from cic_eth.api import Api
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.transactions import from_wei
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BalanceManager:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, address: str, chain_str: str, token_symbol: str):
 | 
			
		||||
        """
 | 
			
		||||
        :param address: Ethereum address of account whose balance is being queried
 | 
			
		||||
        :type address: str, 0x-hex
 | 
			
		||||
        :param chain_str: The chain name and network id.
 | 
			
		||||
        :type chain_str: str
 | 
			
		||||
        :param token_symbol: ERC20 token symbol of whose balance is being queried
 | 
			
		||||
        :type token_symbol: str
 | 
			
		||||
        """
 | 
			
		||||
        self.address = address
 | 
			
		||||
        self.chain_str = chain_str
 | 
			
		||||
        self.token_symbol = token_symbol
 | 
			
		||||
 | 
			
		||||
    def get_operational_balance(self) -> float:
 | 
			
		||||
        """This question queries cic-eth for an account's balance
 | 
			
		||||
        :return: The current balance of the account as reflected on the blockchain.
 | 
			
		||||
        :rtype: int
 | 
			
		||||
        """
 | 
			
		||||
        cic_eth_api = Api(chain_str=self.chain_str, callback_task=None)
 | 
			
		||||
        balance_request_task = cic_eth_api.balance(address=self.address, token_symbol=self.token_symbol)
 | 
			
		||||
        balance_request_task_results = balance_request_task.collect()
 | 
			
		||||
        balance_result = deque(balance_request_task_results, maxlen=1).pop()
 | 
			
		||||
        balance = from_wei(value=balance_result[-1])
 | 
			
		||||
        return balance
 | 
			
		||||
							
								
								
									
										90
									
								
								apps/cic-ussd/cic_ussd/balance.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								apps/cic-ussd/cic_ussd/balance.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import celery
 | 
			
		||||
from cic_eth.api import Api
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.error import CachedDataNotFoundError
 | 
			
		||||
from cic_ussd.redis import create_cached_data_key, get_cached_data
 | 
			
		||||
from cic_ussd.conversions import from_wei
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BalanceManager:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, address: str, chain_str: str, token_symbol: str):
 | 
			
		||||
        """
 | 
			
		||||
        :param address: Ethereum address of account whose balance is being queried
 | 
			
		||||
        :type address: str, 0x-hex
 | 
			
		||||
        :param chain_str: The chain name and network id.
 | 
			
		||||
        :type chain_str: str
 | 
			
		||||
        :param token_symbol: ERC20 token symbol of whose balance is being queried
 | 
			
		||||
        :type token_symbol: str
 | 
			
		||||
        """
 | 
			
		||||
        self.address = address
 | 
			
		||||
        self.chain_str = chain_str
 | 
			
		||||
        self.token_symbol = token_symbol
 | 
			
		||||
 | 
			
		||||
    def get_balances(self, asynchronous: bool = False) -> Union[celery.Task, dict]:
 | 
			
		||||
        """
 | 
			
		||||
        This function queries cic-eth for an account's balances, It provides a means to receive the balance either
 | 
			
		||||
        asynchronously or synchronously depending on the provided value for teh asynchronous parameter. It returns a
 | 
			
		||||
        dictionary containing network, outgoing and incoming balances.
 | 
			
		||||
        :param asynchronous: Boolean value checking whether to return balances asynchronously
 | 
			
		||||
        :type asynchronous: bool
 | 
			
		||||
        :return:
 | 
			
		||||
        :rtype:
 | 
			
		||||
        """
 | 
			
		||||
        if asynchronous:
 | 
			
		||||
            cic_eth_api = Api(
 | 
			
		||||
                chain_str=self.chain_str,
 | 
			
		||||
                callback_queue='cic-ussd',
 | 
			
		||||
                callback_task='cic_ussd.tasks.callback_handler.process_balances_callback',
 | 
			
		||||
                callback_param=''
 | 
			
		||||
            )
 | 
			
		||||
            cic_eth_api.balance(address=self.address, token_symbol=self.token_symbol)
 | 
			
		||||
        else:
 | 
			
		||||
            cic_eth_api = Api(chain_str=self.chain_str)
 | 
			
		||||
            balance_request_task = cic_eth_api.balance(
 | 
			
		||||
                address=self.address,
 | 
			
		||||
                token_symbol=self.token_symbol)
 | 
			
		||||
            return balance_request_task.get()[0]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def compute_operational_balance(balances: dict) -> float:
 | 
			
		||||
    """This function calculates the right balance given incoming and outgoing
 | 
			
		||||
    :param balances:
 | 
			
		||||
    :type balances:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    incoming_balance = balances.get('balance_incoming')
 | 
			
		||||
    outgoing_balance = balances.get('balance_outgoing')
 | 
			
		||||
    network_balance = balances.get('balance_network')
 | 
			
		||||
 | 
			
		||||
    operational_balance = (network_balance + incoming_balance) - outgoing_balance
 | 
			
		||||
    return from_wei(value=operational_balance)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_cached_operational_balance(blockchain_address: str):
 | 
			
		||||
    """
 | 
			
		||||
    :param blockchain_address:
 | 
			
		||||
    :type blockchain_address:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    key = create_cached_data_key(
 | 
			
		||||
        identifier=bytes.fromhex(blockchain_address[2:]),
 | 
			
		||||
        salt='cic.balances_data'
 | 
			
		||||
    )
 | 
			
		||||
    cached_balance = get_cached_data(key=key)
 | 
			
		||||
    if cached_balance:
 | 
			
		||||
        operational_balance = compute_operational_balance(balances=json.loads(cached_balance))
 | 
			
		||||
        return operational_balance
 | 
			
		||||
    else:
 | 
			
		||||
        raise CachedDataNotFoundError('Cached operational balance not found.')
 | 
			
		||||
							
								
								
									
										10
									
								
								apps/cic-ussd/cic_ussd/chain.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								apps/cic-ussd/cic_ussd/chain.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
# local imports
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
from chainlib.chain import ChainSpec
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Chain:
 | 
			
		||||
    spec: ChainSpec = None
 | 
			
		||||
							
								
								
									
										41
									
								
								apps/cic-ussd/cic_ussd/conversions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								apps/cic-ussd/cic_ussd/conversions.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import decimal
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def truncate(value: float, decimals: int):
 | 
			
		||||
    """This function truncates a value to a specified number of decimals places.
 | 
			
		||||
    :param value: The value to be truncated.
 | 
			
		||||
    :type value: float
 | 
			
		||||
    :param decimals: The number of decimals for the value to be truncated to
 | 
			
		||||
    :type decimals: int
 | 
			
		||||
    :return: The truncated value.
 | 
			
		||||
    :rtype: int
 | 
			
		||||
    """
 | 
			
		||||
    decimal.getcontext().rounding = decimal.ROUND_DOWN
 | 
			
		||||
    contextualized_value = decimal.Decimal(value)
 | 
			
		||||
    return round(contextualized_value, decimals)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def from_wei(value: int) -> float:
 | 
			
		||||
    """This function converts values in Wei to a token in the cic network.
 | 
			
		||||
    :param value: Value in Wei
 | 
			
		||||
    :type value: int
 | 
			
		||||
    :return: SRF equivalent of value in Wei
 | 
			
		||||
    :rtype: float
 | 
			
		||||
    """
 | 
			
		||||
    value = float(value) / 1e+6
 | 
			
		||||
    return truncate(value=value, decimals=2)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_wei(value: int) -> int:
 | 
			
		||||
    """This functions converts values from a token in the cic network to Wei.
 | 
			
		||||
    :param value: Value in SRF
 | 
			
		||||
    :type value: int
 | 
			
		||||
    :return: Wei equivalent of value in SRF
 | 
			
		||||
    :rtype: int
 | 
			
		||||
    """
 | 
			
		||||
    return int(value * 1e+6)
 | 
			
		||||
@ -1,213 +1,237 @@
 | 
			
		||||
{
 | 
			
		||||
    "ussd_menu": {
 | 
			
		||||
        "1": {
 | 
			
		||||
            "description": "The self signup process has been initiated and the account is being created",
 | 
			
		||||
            "display_key": "ussd.kenya.account_creation_prompt",
 | 
			
		||||
            "name": "account_creation_prompt",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "2": {
 | 
			
		||||
            "description": "Start menu. This is the entry point for users to select their preferred language",
 | 
			
		||||
            "description": "Entry point for users to select their preferred language.",
 | 
			
		||||
            "display_key": "ussd.kenya.initial_language_selection",
 | 
			
		||||
            "name": "initial_language_selection",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "3": {
 | 
			
		||||
            "description": "PIN setup entry menu",
 | 
			
		||||
        "2": {
 | 
			
		||||
            "description": "Entry point for users to enter a pin to secure their account.",
 | 
			
		||||
            "display_key": "ussd.kenya.initial_pin_entry",
 | 
			
		||||
            "name": "initial_pin_entry",
 | 
			
		||||
            "parent": "initial_language_selection"
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "4": {
 | 
			
		||||
            "description": "Confirm new PIN menu",
 | 
			
		||||
        "3": {
 | 
			
		||||
            "description": "Pin confirmation entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.initial_pin_confirmation",
 | 
			
		||||
            "name": "initial_pin_confirmation",
 | 
			
		||||
            "parent": "initial_pin_entry"
 | 
			
		||||
        },
 | 
			
		||||
        "4": {
 | 
			
		||||
            "description": "The signup process has been initiated and the account is being created.",
 | 
			
		||||
            "display_key": "ussd.kenya.account_creation_prompt",
 | 
			
		||||
            "name": "account_creation_prompt",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "5": {
 | 
			
		||||
            "description": "Start menu. This is the entry point for activated users",
 | 
			
		||||
            "description": "Entry point for activated users.",
 | 
			
		||||
            "display_key": "ussd.kenya.start",
 | 
			
		||||
            "name": "start",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "6": {
 | 
			
		||||
            "description": "Send Token recipient entry",
 | 
			
		||||
            "description": "Given name entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_given_name",
 | 
			
		||||
            "name": "enter_given_name",
 | 
			
		||||
            "parent": "metadata_management"
 | 
			
		||||
        },
 | 
			
		||||
        "7": {
 | 
			
		||||
            "description": "Family name entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_family_name",
 | 
			
		||||
            "name": "enter_family_name",
 | 
			
		||||
            "parent": "metadata_management"
 | 
			
		||||
        },
 | 
			
		||||
        "8": {
 | 
			
		||||
            "description": "Gender entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_gender",
 | 
			
		||||
            "name": "enter_gender",
 | 
			
		||||
            "parent": "metadata_management"
 | 
			
		||||
        },
 | 
			
		||||
        "9": {
 | 
			
		||||
            "description": "Age entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_gender",
 | 
			
		||||
            "name": "enter_gender",
 | 
			
		||||
            "parent": "metadata_management"
 | 
			
		||||
        },
 | 
			
		||||
        "10": {
 | 
			
		||||
            "description": "Location entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_location",
 | 
			
		||||
            "name": "enter_location",
 | 
			
		||||
            "parent": "metadata_management"
 | 
			
		||||
        },
 | 
			
		||||
        "11": {
 | 
			
		||||
            "description": "Products entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_products",
 | 
			
		||||
            "name": "enter_products",
 | 
			
		||||
            "parent": "metadata_management"
 | 
			
		||||
        },
 | 
			
		||||
        "12": {
 | 
			
		||||
            "description": "Entry point for activated users.",
 | 
			
		||||
            "display_key": "ussd.kenya.start",
 | 
			
		||||
            "name": "start",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "13": {
 | 
			
		||||
            "description": "Send Token recipient entry.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_transaction_recipient",
 | 
			
		||||
            "name": "enter_transaction_recipient",
 | 
			
		||||
            "parent": "start"
 | 
			
		||||
        },
 | 
			
		||||
        "7": {
 | 
			
		||||
            "description": "Send Token amount prompt menu",
 | 
			
		||||
        "14": {
 | 
			
		||||
            "description": "Send Token amount prompt menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_transaction_amount",
 | 
			
		||||
            "name": "enter_transaction_amount",
 | 
			
		||||
            "parent": "start"
 | 
			
		||||
        },
 | 
			
		||||
        "8": {
 | 
			
		||||
            "description": "PIN entry for authorization to send token",
 | 
			
		||||
        "15": {
 | 
			
		||||
            "description": "Pin entry for authorization to send token.",
 | 
			
		||||
            "display_key": "ussd.kenya.transaction_pin_authorization",
 | 
			
		||||
            "name": "transaction_pin_authorization",
 | 
			
		||||
            "parent": "start"
 | 
			
		||||
        },
 | 
			
		||||
        "9": {
 | 
			
		||||
            "description": "Terminal of a menu flow where an SMS is expected after.",
 | 
			
		||||
            "display_key": "ussd.kenya.complete",
 | 
			
		||||
            "name": "complete",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "10": {
 | 
			
		||||
            "description": "Help menu",
 | 
			
		||||
            "display_key": "ussd.kenya.help",
 | 
			
		||||
            "name": "help",
 | 
			
		||||
        "16": {
 | 
			
		||||
            "description": "Manage account menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.account_management",
 | 
			
		||||
            "name": "account_management",
 | 
			
		||||
            "parent": "start"
 | 
			
		||||
        },
 | 
			
		||||
        "11": {
 | 
			
		||||
            "description": "Manage account menu",
 | 
			
		||||
            "display_key": "ussd.kenya.profile_management",
 | 
			
		||||
            "name": "profile_management",
 | 
			
		||||
        "17": {
 | 
			
		||||
            "description": "Manage metadata menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.metadata_management",
 | 
			
		||||
            "name": "metadata_management",
 | 
			
		||||
            "parent": "start"
 | 
			
		||||
        },
 | 
			
		||||
        "12": {
 | 
			
		||||
            "description": "Manage business directory info",
 | 
			
		||||
        "18": {
 | 
			
		||||
            "description": "Manage user's preferred language menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.select_preferred_language",
 | 
			
		||||
            "name": "select_preferred_language",
 | 
			
		||||
            "parent": "account_management"
 | 
			
		||||
        },
 | 
			
		||||
        "13": {
 | 
			
		||||
            "description": "About business directory info",
 | 
			
		||||
        "19": {
 | 
			
		||||
            "description": "Retrieve mini-statement menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.mini_statement_pin_authorization",
 | 
			
		||||
            "name": "mini_statement_pin_authorization",
 | 
			
		||||
            "parent": "account_management"
 | 
			
		||||
        },
 | 
			
		||||
        "14": {
 | 
			
		||||
            "description": "Change business directory info",
 | 
			
		||||
        "20": {
 | 
			
		||||
            "description": "Manage user's pin menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_current_pin",
 | 
			
		||||
            "name": "enter_current_pin",
 | 
			
		||||
            "parent": "account_management"
 | 
			
		||||
        },
 | 
			
		||||
        "15": {
 | 
			
		||||
            "description": "New PIN entry menu",
 | 
			
		||||
        "21": {
 | 
			
		||||
            "description": "New pin entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_new_pin",
 | 
			
		||||
            "name": "enter_new_pin",
 | 
			
		||||
            "parent": "account_management"
 | 
			
		||||
        },
 | 
			
		||||
        "16": {
 | 
			
		||||
            "description": "First name entry menu",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_first_name",
 | 
			
		||||
            "name": "enter_first_name",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "17": {
 | 
			
		||||
            "description": "Last name entry menu",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_last_name",
 | 
			
		||||
            "name": "enter_last_name",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "18": {
 | 
			
		||||
            "description": "Gender entry menu",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_gender",
 | 
			
		||||
            "name": "enter_gender",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "19": {
 | 
			
		||||
            "description": "Location entry menu",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_location",
 | 
			
		||||
            "name": "enter_location",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "20": {
 | 
			
		||||
            "description": "Business profile entry menu",
 | 
			
		||||
            "display_key": "ussd.kenya.enter_business_profile",
 | 
			
		||||
            "name": "enter_business_profile",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "21": {
 | 
			
		||||
            "description": "Menu to display a user's entire profile",
 | 
			
		||||
            "display_key": "ussd.kenya.display_user_profile_data",
 | 
			
		||||
            "name": "display_user_profile_data",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "22": {
 | 
			
		||||
            "description": "Pin authorization to change name",
 | 
			
		||||
            "display_key": "ussd.kenya.name_management_pin_authorization",
 | 
			
		||||
            "name": "name_management_pin_authorization",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
            "description": "Pin entry menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.standard_pin_authorization",
 | 
			
		||||
            "name": "standard_pin_authorization",
 | 
			
		||||
            "parent": "start"
 | 
			
		||||
        },
 | 
			
		||||
        "23": {
 | 
			
		||||
            "description": "Pin authorization to change gender",
 | 
			
		||||
            "display_key": "ussd.kenya.gender_management_pin_authorization",
 | 
			
		||||
            "name": "gender_management_pin_authorization",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "24": {
 | 
			
		||||
            "description": "Pin authorization to change location",
 | 
			
		||||
            "display_key": "ussd.kenya.location_management_pin_authorization",
 | 
			
		||||
            "name": "location_management_pin_authorization",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "26": {
 | 
			
		||||
            "description": "Pin authorization to display user's profile",
 | 
			
		||||
            "display_key": "ussd.kenya.view_profile_pin_authorization",
 | 
			
		||||
            "name": "view_profile_pin_authorization",
 | 
			
		||||
            "parent": "profile_management"
 | 
			
		||||
        },
 | 
			
		||||
        "27": {
 | 
			
		||||
            "description": "Exit menu",
 | 
			
		||||
            "description": "Exit menu.",
 | 
			
		||||
            "display_key": "ussd.kenya.exit",
 | 
			
		||||
            "name": "exit",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "28": {
 | 
			
		||||
            "description": "Invalid menu option",
 | 
			
		||||
        "24": {
 | 
			
		||||
            "description": "Invalid menu option.",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_invalid_menu_option",
 | 
			
		||||
            "name": "exit_invalid_menu_option",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "29": {
 | 
			
		||||
            "description": "PIN policy violation",
 | 
			
		||||
        "25": {
 | 
			
		||||
            "description": "Pin policy violation.",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_invalid_pin",
 | 
			
		||||
            "name": "exit_invalid_pin",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "30": {
 | 
			
		||||
            "description": "PIN mismatch. New PIN and the new PIN confirmation do not match",
 | 
			
		||||
        "26": {
 | 
			
		||||
            "description": "Pin mismatch. New pin and the new pin confirmation do not match",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_pin_mismatch",
 | 
			
		||||
            "name": "exit_pin_mismatch",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "31": {
 | 
			
		||||
            "description": "Ussd PIN Blocked Menu",
 | 
			
		||||
        "27": {
 | 
			
		||||
            "description": "Ussd pin blocked Menu",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_pin_blocked",
 | 
			
		||||
            "name": "exit_pin_blocked",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "32": {
 | 
			
		||||
            "description": "Key params missing in request",
 | 
			
		||||
        "28": {
 | 
			
		||||
            "description": "Key params missing in request.",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_invalid_request",
 | 
			
		||||
            "name": "exit_invalid_request",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "33": {
 | 
			
		||||
            "description": "The user did not select a choice",
 | 
			
		||||
        "29": {
 | 
			
		||||
            "description": "The user did not select a choice.",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_invalid_input",
 | 
			
		||||
            "name": "exit_invalid_input",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "34": {
 | 
			
		||||
        "30": {
 | 
			
		||||
            "description": "Exit following unsuccessful transaction due to insufficient account balance.",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_insufficient_balance",
 | 
			
		||||
            "name": "exit_insufficient_balance",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "31": {
 | 
			
		||||
            "description": "Exit following a successful transaction.",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_successful_transaction",
 | 
			
		||||
            "name": "exit_successful_transaction",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "32": {
 | 
			
		||||
            "description": "End of a menu flow.",
 | 
			
		||||
            "display_key": "ussd.kenya.complete",
 | 
			
		||||
            "name": "complete",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "33": {
 | 
			
		||||
            "description": "Pin entry menu to view account balances.",
 | 
			
		||||
            "display_key": "ussd.kenya.account_balances_pin_authorization",
 | 
			
		||||
            "name": "account_balances_pin_authorization",
 | 
			
		||||
            "parent": "account_management"
 | 
			
		||||
        },
 | 
			
		||||
        "34": {
 | 
			
		||||
            "description": "Pin entry menu to view account statement.",
 | 
			
		||||
            "display_key": "ussd.kenya.account_statement_pin_authorization",
 | 
			
		||||
            "name": "account_statement_pin_authorization",
 | 
			
		||||
            "parent": "account_management"
 | 
			
		||||
        },
 | 
			
		||||
        "35": {
 | 
			
		||||
            "description": "Manage account menu",
 | 
			
		||||
            "display_key": "ussd.kenya.account_management",
 | 
			
		||||
            "name": "account_management",
 | 
			
		||||
            "parent": "start"
 | 
			
		||||
            "description": "Menu to display account balances.",
 | 
			
		||||
            "display_key": "ussd.kenya.account_balances",
 | 
			
		||||
            "name": "account_balances",
 | 
			
		||||
            "parent": "account_management"
 | 
			
		||||
        },
 | 
			
		||||
        "36": {
 | 
			
		||||
            "description": "Exit following insufficient balance to perform a transaction.",
 | 
			
		||||
            "display_key": "ussd.kenya.exit_insufficient_balance",
 | 
			
		||||
            "name": "exit_insufficient_balance",
 | 
			
		||||
            "description": "Menu to display first set of transactions in statement.",
 | 
			
		||||
            "display_key": "ussd.kenya.first_transaction_set",
 | 
			
		||||
            "name": "first_transaction_set",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "37": {
 | 
			
		||||
            "description": "Menu to display middle set of transactions in statement.",
 | 
			
		||||
            "display_key": "ussd.kenya.middle_transaction_set",
 | 
			
		||||
            "name": "middle_transaction_set",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "38": {
 | 
			
		||||
            "description": "Menu to display last set of transactions in statement.",
 | 
			
		||||
            "display_key": "ussd.kenya.last_transaction_set",
 | 
			
		||||
            "name": "last_transaction_set",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        },
 | 
			
		||||
        "39": {
 | 
			
		||||
            "description": "Menu to instruct users to call the office.",
 | 
			
		||||
            "display_key": "ussd.key.help",
 | 
			
		||||
            "name": "help",
 | 
			
		||||
            "parent": null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -17,3 +17,17 @@ class ActionDataNotFoundError(OSError):
 | 
			
		||||
    """Raised when action data matching a specific task uuid is not found in the redis cache"""
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserMetadataNotFoundError(OSError):
 | 
			
		||||
    """Raised when metadata is expected but not available in cache."""
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UnsupportedMethodError(OSError):
 | 
			
		||||
    """Raised when the method passed to the make request function is unsupported."""
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CachedDataNotFoundError(OSError):
 | 
			
		||||
    """Raised when the method passed to the make request function is unsupported."""
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										43
									
								
								apps/cic-ussd/cic_ussd/metadata/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								apps/cic-ussd/cic_ussd/metadata/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import requests
 | 
			
		||||
from chainlib.eth.address import to_checksum
 | 
			
		||||
from hexathon import add_0x
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.error import UnsupportedMethodError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def make_request(method: str, url: str, data: any = None, headers: dict = None):
 | 
			
		||||
    """
 | 
			
		||||
    :param method:
 | 
			
		||||
    :type method:
 | 
			
		||||
    :param url:
 | 
			
		||||
    :type url:
 | 
			
		||||
    :param data:
 | 
			
		||||
    :type data:
 | 
			
		||||
    :param headers:
 | 
			
		||||
    :type headers:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    if method == 'GET':
 | 
			
		||||
        result = requests.get(url=url)
 | 
			
		||||
    elif method == 'POST':
 | 
			
		||||
        result = requests.post(url=url, data=data, headers=headers)
 | 
			
		||||
    elif method == 'PUT':
 | 
			
		||||
        result = requests.put(url=url, data=data, headers=headers)
 | 
			
		||||
    else:
 | 
			
		||||
        raise UnsupportedMethodError(f'Unsupported method: {method}')
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def blockchain_address_to_metadata_pointer(blockchain_address: str):
 | 
			
		||||
    """
 | 
			
		||||
    :param blockchain_address:
 | 
			
		||||
    :type blockchain_address:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    return bytes.fromhex(blockchain_address[2:])
 | 
			
		||||
							
								
								
									
										63
									
								
								apps/cic-ussd/cic_ussd/metadata/signer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								apps/cic-ussd/cic_ussd/metadata/signer.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Optional
 | 
			
		||||
from urllib.request import Request, urlopen
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import gnupg
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Signer:
 | 
			
		||||
    """
 | 
			
		||||
    :cvar gpg_path:
 | 
			
		||||
    :type gpg_path:
 | 
			
		||||
    :cvar gpg_passphrase:
 | 
			
		||||
    :type gpg_passphrase:
 | 
			
		||||
    :cvar key_file_path:
 | 
			
		||||
    :type key_file_path:
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    gpg_path: str = None
 | 
			
		||||
    gpg_passphrase: str = None
 | 
			
		||||
    key_file_path: str = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.gpg = gnupg.GPG(gnupghome=self.gpg_path)
 | 
			
		||||
 | 
			
		||||
        # parse key file data
 | 
			
		||||
        key_file = open(self.key_file_path, 'r')
 | 
			
		||||
        self.key_data = key_file.read()
 | 
			
		||||
        key_file.close()
 | 
			
		||||
 | 
			
		||||
    def get_operational_key(self):
 | 
			
		||||
        """
 | 
			
		||||
        :return:
 | 
			
		||||
        :rtype:
 | 
			
		||||
        """
 | 
			
		||||
        # import key data into keyring
 | 
			
		||||
        self.gpg.import_keys(key_data=self.key_data)
 | 
			
		||||
        gpg_keys = self.gpg.list_keys()
 | 
			
		||||
        key_algorithm = gpg_keys[0].get('algo')
 | 
			
		||||
        key_id = gpg_keys[0].get("keyid")
 | 
			
		||||
        logg.info(f'using signing key: {key_id}, algorithm: {key_algorithm}')
 | 
			
		||||
        return gpg_keys[0]
 | 
			
		||||
 | 
			
		||||
    def sign_digest(self, data: bytes):
 | 
			
		||||
        """
 | 
			
		||||
        :param data:
 | 
			
		||||
        :type data:
 | 
			
		||||
        :return:
 | 
			
		||||
        :rtype:
 | 
			
		||||
        """
 | 
			
		||||
        data = json.loads(data)
 | 
			
		||||
        digest = data['digest']
 | 
			
		||||
        key_id = self.get_operational_key().get('keyid')
 | 
			
		||||
        signature = self.gpg.sign(digest, passphrase=self.gpg_passphrase, keyid=key_id)
 | 
			
		||||
        return str(signature)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										102
									
								
								apps/cic-ussd/cic_ussd/metadata/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								apps/cic-ussd/cic_ussd/metadata/user.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import requests
 | 
			
		||||
from cic_types.models.person import generate_metadata_pointer, Person
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.metadata import make_request
 | 
			
		||||
from cic_ussd.metadata.signer import Signer
 | 
			
		||||
from cic_ussd.redis import cache_data
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserMetadata:
 | 
			
		||||
    """
 | 
			
		||||
    :cvar base_url:
 | 
			
		||||
    :type base_url:
 | 
			
		||||
    """
 | 
			
		||||
    base_url = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, identifier: bytes):
 | 
			
		||||
        """
 | 
			
		||||
        :param identifier:
 | 
			
		||||
        :type identifier:
 | 
			
		||||
        """
 | 
			
		||||
        self. headers = {
 | 
			
		||||
            'X-CIC-AUTOMERGE': 'server',
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
        self.identifier = identifier
 | 
			
		||||
        self.metadata_pointer = generate_metadata_pointer(
 | 
			
		||||
                identifier=self.identifier,
 | 
			
		||||
                cic_type='cic.person'
 | 
			
		||||
        )
 | 
			
		||||
        if self.base_url:
 | 
			
		||||
            self.url = os.path.join(self.base_url, self.metadata_pointer)
 | 
			
		||||
 | 
			
		||||
    def create(self, data: dict):
 | 
			
		||||
        try:
 | 
			
		||||
            data = json.dumps(data).encode('utf-8')
 | 
			
		||||
            result = make_request(method='POST', url=self.url, data=data, headers=self.headers)
 | 
			
		||||
            metadata = result.content
 | 
			
		||||
            self.edit(data=metadata, engine='pgp')
 | 
			
		||||
            logg.info(f'Get sign material response status: {result.status_code}')
 | 
			
		||||
            result.raise_for_status()
 | 
			
		||||
        except requests.exceptions.HTTPError as error:
 | 
			
		||||
            raise RuntimeError(error)
 | 
			
		||||
 | 
			
		||||
    def edit(self, data: bytes, engine: str):
 | 
			
		||||
        """
 | 
			
		||||
        :param data:
 | 
			
		||||
        :type data:
 | 
			
		||||
        :param engine:
 | 
			
		||||
        :type engine:
 | 
			
		||||
        :return:
 | 
			
		||||
        :rtype:
 | 
			
		||||
        """
 | 
			
		||||
        cic_meta_signer = Signer()
 | 
			
		||||
        signature = cic_meta_signer.sign_digest(data=data)
 | 
			
		||||
        algorithm = cic_meta_signer.get_operational_key().get('algo')
 | 
			
		||||
        formatted_data = {
 | 
			
		||||
            'm': data.decode('utf-8'),
 | 
			
		||||
            's': {
 | 
			
		||||
                'engine': engine,
 | 
			
		||||
                'algo': algorithm,
 | 
			
		||||
                'data': signature,
 | 
			
		||||
                'digest': json.loads(data).get('digest'),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        formatted_data = json.dumps(formatted_data).encode('utf-8')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            result = make_request(method='PUT', url=self.url, data=formatted_data, headers=self.headers)
 | 
			
		||||
            logg.info(f'Signed content submission status: {result.status_code}.')
 | 
			
		||||
            result.raise_for_status()
 | 
			
		||||
        except requests.exceptions.HTTPError as error:
 | 
			
		||||
            raise RuntimeError(error)
 | 
			
		||||
 | 
			
		||||
    def query(self):
 | 
			
		||||
        result = make_request(method='GET', url=self.url)
 | 
			
		||||
        status = result.status_code
 | 
			
		||||
        logg.info(f'Get latest data status: {status}')
 | 
			
		||||
        try:
 | 
			
		||||
            if status == 200:
 | 
			
		||||
                response_data = result.content
 | 
			
		||||
                data = json.loads(response_data.decode())
 | 
			
		||||
 | 
			
		||||
                # validate data
 | 
			
		||||
                person = Person()
 | 
			
		||||
                deserialized_person = person.deserialize(metadata=json.loads(data))
 | 
			
		||||
 | 
			
		||||
                cache_data(key=self.metadata_pointer, data=json.dumps(deserialized_person.serialize()))
 | 
			
		||||
            elif status == 404:
 | 
			
		||||
                logg.info('The data is not available and might need to be added.')
 | 
			
		||||
            result.raise_for_status()
 | 
			
		||||
        except requests.exceptions.HTTPError as error:
 | 
			
		||||
            raise RuntimeError(error)
 | 
			
		||||
@ -5,7 +5,6 @@ import logging
 | 
			
		||||
# third party imports
 | 
			
		||||
import celery
 | 
			
		||||
import i18n
 | 
			
		||||
import phonenumbers
 | 
			
		||||
from cic_eth.api.api_task import Api
 | 
			
		||||
from tinydb.table import Document
 | 
			
		||||
from typing import Optional
 | 
			
		||||
@ -239,7 +238,7 @@ def persist_session_to_db_task(external_session_id: str, queue: str):
 | 
			
		||||
    :type queue: str
 | 
			
		||||
    """
 | 
			
		||||
    s_persist_session_to_db = celery.signature(
 | 
			
		||||
        'cic_ussd.tasks.ussd.persist_session_to_db',
 | 
			
		||||
        'cic_ussd.tasks.ussd_session.persist_session_to_db',
 | 
			
		||||
        [external_session_id]
 | 
			
		||||
    )
 | 
			
		||||
    s_persist_session_to_db.apply_async(queue=queue)
 | 
			
		||||
@ -453,37 +452,3 @@ def save_to_in_memory_ussd_session_data(queue: str, session_data: dict, ussd_ses
 | 
			
		||||
    )
 | 
			
		||||
    persist_session_to_db_task(external_session_id=external_session_id, queue=queue)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_phone_number(phone_number: str, region: str):
 | 
			
		||||
    """This function parses any phone number for the provided region
 | 
			
		||||
    :param phone_number: A string with a phone number.
 | 
			
		||||
    :type phone_number: str
 | 
			
		||||
    :param region: Caller defined region
 | 
			
		||||
    :type region: str
 | 
			
		||||
    :return: The parsed phone number value based on the defined region
 | 
			
		||||
    :rtype: str
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(phone_number, str):
 | 
			
		||||
        try:
 | 
			
		||||
            phone_number = str(int(phone_number))
 | 
			
		||||
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    phone_number_object = phonenumbers.parse(phone_number, region)
 | 
			
		||||
    parsed_phone_number = phonenumbers.format_number(phone_number_object, phonenumbers.PhoneNumberFormat.E164)
 | 
			
		||||
 | 
			
		||||
    return parsed_phone_number
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_user_by_phone_number(phone_number: str) -> Optional[User]:
 | 
			
		||||
    """This function queries the database for a user based on the provided phone number.
 | 
			
		||||
    :param phone_number: A valid phone number.
 | 
			
		||||
    :type phone_number: str
 | 
			
		||||
    :return: A user object matching a given phone number
 | 
			
		||||
    :rtype: User|None
 | 
			
		||||
    """
 | 
			
		||||
    # consider adding region to user's metadata
 | 
			
		||||
    phone_number = process_phone_number(phone_number=phone_number, region='KE')
 | 
			
		||||
    user = User.session.query(User).filter_by(phone_number=phone_number).first()
 | 
			
		||||
    return user
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										43
									
								
								apps/cic-ussd/cic_ussd/phone_number.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								apps/cic-ussd/cic_ussd/phone_number.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import phonenumbers
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.db.models.user import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_phone_number(phone_number: str, region: str):
 | 
			
		||||
    """This function parses any phone number for the provided region
 | 
			
		||||
    :param phone_number: A string with a phone number.
 | 
			
		||||
    :type phone_number: str
 | 
			
		||||
    :param region: Caller defined region
 | 
			
		||||
    :type region: str
 | 
			
		||||
    :return: The parsed phone number value based on the defined region
 | 
			
		||||
    :rtype: str
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(phone_number, str):
 | 
			
		||||
        try:
 | 
			
		||||
            phone_number = str(int(phone_number))
 | 
			
		||||
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    phone_number_object = phonenumbers.parse(phone_number, region)
 | 
			
		||||
    parsed_phone_number = phonenumbers.format_number(phone_number_object, phonenumbers.PhoneNumberFormat.E164)
 | 
			
		||||
 | 
			
		||||
    return parsed_phone_number
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_user_by_phone_number(phone_number: str) -> Optional[User]:
 | 
			
		||||
    """This function queries the database for a user based on the provided phone number.
 | 
			
		||||
    :param phone_number: A valid phone number.
 | 
			
		||||
    :type phone_number: str
 | 
			
		||||
    :return: A user object matching a given phone number
 | 
			
		||||
    :rtype: User|None
 | 
			
		||||
    """
 | 
			
		||||
    # consider adding region to user's metadata
 | 
			
		||||
    phone_number = process_phone_number(phone_number=phone_number, region='KE')
 | 
			
		||||
    user = User.session.query(User).filter_by(phone_number=phone_number).first()
 | 
			
		||||
    return user
 | 
			
		||||
@ -1,17 +1,26 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import logging
 | 
			
		||||
import json
 | 
			
		||||
import re
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
# third party imports
 | 
			
		||||
import celery
 | 
			
		||||
from cic_types.models.person import Person
 | 
			
		||||
from tinydb.table import Document
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.accounts import BalanceManager
 | 
			
		||||
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
 | 
			
		||||
from cic_ussd.db.models.user import AccountStatus, User
 | 
			
		||||
from cic_ussd.db.models.ussd_session import UssdSession
 | 
			
		||||
from cic_ussd.menu.ussd_menu import UssdMenu
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
 | 
			
		||||
from cic_ussd.phone_number import get_user_by_phone_number
 | 
			
		||||
from cic_ussd.redis import cache_data, create_cached_data_key, get_cached_data
 | 
			
		||||
from cic_ussd.state_machine import UssdStateMachine
 | 
			
		||||
from cic_ussd.transactions import to_wei, from_wei
 | 
			
		||||
from cic_ussd.conversions import to_wei, from_wei
 | 
			
		||||
from cic_ussd.translation import translation_for
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger(__name__)
 | 
			
		||||
@ -57,17 +66,17 @@ def process_exit_insufficient_balance(display_key: str, user: User, ussd_session
 | 
			
		||||
    :rtype: str
 | 
			
		||||
    """
 | 
			
		||||
    # get account balance
 | 
			
		||||
    balance_manager = BalanceManager(address=user.blockchain_address,
 | 
			
		||||
                                     chain_str=UssdStateMachine.chain_str,
 | 
			
		||||
                                     token_symbol='SRF')
 | 
			
		||||
    balance = balance_manager.get_operational_balance()
 | 
			
		||||
    operational_balance = get_cached_operational_balance(blockchain_address=user.blockchain_address)
 | 
			
		||||
 | 
			
		||||
    # compile response data
 | 
			
		||||
    user_input = ussd_session.get('user_input').split('*')[-1]
 | 
			
		||||
    transaction_amount = to_wei(value=int(user_input))
 | 
			
		||||
    token_symbol = 'SRF'
 | 
			
		||||
 | 
			
		||||
    recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
 | 
			
		||||
    tx_recipient_information = recipient_phone_number
 | 
			
		||||
    recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
 | 
			
		||||
 | 
			
		||||
    tx_recipient_information = define_account_tx_metadata(user=recipient)
 | 
			
		||||
 | 
			
		||||
    return translation_for(
 | 
			
		||||
        key=display_key,
 | 
			
		||||
@ -75,7 +84,7 @@ def process_exit_insufficient_balance(display_key: str, user: User, ussd_session
 | 
			
		||||
        amount=from_wei(transaction_amount),
 | 
			
		||||
        token_symbol=token_symbol,
 | 
			
		||||
        recipient_information=tx_recipient_information,
 | 
			
		||||
        token_balance=balance
 | 
			
		||||
        token_balance=operational_balance
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -122,9 +131,10 @@ def process_transaction_pin_authorization(user: User, display_key: str, ussd_ses
 | 
			
		||||
    """
 | 
			
		||||
    # compile response data
 | 
			
		||||
    recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
 | 
			
		||||
    tx_recipient_information = recipient_phone_number
 | 
			
		||||
    tx_sender_information = user.phone_number
 | 
			
		||||
    logg.debug('Requires integration with cic-meta to get user name.')
 | 
			
		||||
    recipient = get_user_by_phone_number(phone_number=recipient_phone_number)
 | 
			
		||||
    tx_recipient_information = define_account_tx_metadata(user=recipient)
 | 
			
		||||
    tx_sender_information = define_account_tx_metadata(user=user)
 | 
			
		||||
 | 
			
		||||
    token_symbol = 'SRF'
 | 
			
		||||
    user_input = ussd_session.get('user_input').split('*')[-1]
 | 
			
		||||
    transaction_amount = to_wei(value=int(user_input))
 | 
			
		||||
@ -139,6 +149,123 @@ def process_transaction_pin_authorization(user: User, display_key: str, ussd_ses
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_account_balances(user: User, display_key: str, ussd_session: dict):
 | 
			
		||||
    """
 | 
			
		||||
    :param user:
 | 
			
		||||
    :type user:
 | 
			
		||||
    :param display_key:
 | 
			
		||||
    :type display_key:
 | 
			
		||||
    :param ussd_session:
 | 
			
		||||
    :type ussd_session:
 | 
			
		||||
    :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 = ''
 | 
			
		||||
 | 
			
		||||
    return translation_for(
 | 
			
		||||
        key=display_key,
 | 
			
		||||
        preferred_language=user.preferred_language,
 | 
			
		||||
        operational_balance=operational_balance,
 | 
			
		||||
        tax=tax,
 | 
			
		||||
        bonus=bonus,
 | 
			
		||||
        token_symbol='SRF'
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_transactions(transactions: list, preferred_language: str):
 | 
			
		||||
    
 | 
			
		||||
    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')
 | 
			
		||||
            value = transaction.get('destination_value')
 | 
			
		||||
            timestamp = transaction.get('timestamp')
 | 
			
		||||
            action_tag = transaction.get('action_tag')
 | 
			
		||||
            token_symbol = transaction.get('destination_token_symbol')
 | 
			
		||||
 | 
			
		||||
            if action_tag == 'SENT' or action_tag == 'ULITUMA':
 | 
			
		||||
                formatted_transactions += f'{action_tag} {value} {token_symbol} {recipient_phone_number} {timestamp}.\n'
 | 
			
		||||
            else:
 | 
			
		||||
                formatted_transactions += f'{action_tag} {value} {token_symbol} {sender_phone_number} {timestamp}. \n'
 | 
			
		||||
        return formatted_transactions
 | 
			
		||||
    else:
 | 
			
		||||
        if preferred_language == 'en':
 | 
			
		||||
            formatted_transactions = 'Empty'
 | 
			
		||||
        else:
 | 
			
		||||
            formatted_transactions = 'Hamna historia'
 | 
			
		||||
        return formatted_transactions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_account_statement(user: User, display_key: str, ussd_session: dict):
 | 
			
		||||
    """
 | 
			
		||||
    :param user:
 | 
			
		||||
    :type user:
 | 
			
		||||
    :param display_key:
 | 
			
		||||
    :type display_key:
 | 
			
		||||
    :param ussd_session:
 | 
			
		||||
    :type ussd_session:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    # retrieve cached statement
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address)
 | 
			
		||||
    key = create_cached_data_key(identifier=identifier, salt='cic.statement')
 | 
			
		||||
    transactions = get_cached_data(key=key)
 | 
			
		||||
 | 
			
		||||
    first_transaction_set = []
 | 
			
		||||
    middle_transaction_set = []
 | 
			
		||||
    last_transaction_set = []
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    elif 4 < len(transactions) < 7:
 | 
			
		||||
        middle_transaction_set += transactions[3:]
 | 
			
		||||
        first_transaction_set += transactions[:3]
 | 
			
		||||
    else:
 | 
			
		||||
        first_transaction_set += transactions[:3]
 | 
			
		||||
 | 
			
		||||
    logg.debug(f'TRANSACTIONS: {transactions}')
 | 
			
		||||
 | 
			
		||||
    if display_key == 'ussd.kenya.first_transaction_set':
 | 
			
		||||
        logg.debug(f'FIRST TRANSACTION SET: {first_transaction_set}')
 | 
			
		||||
        return translation_for(
 | 
			
		||||
            key=display_key,
 | 
			
		||||
            preferred_language=user.preferred_language,
 | 
			
		||||
            first_transaction_set=format_transactions(
 | 
			
		||||
                transactions=first_transaction_set,
 | 
			
		||||
                preferred_language=user.preferred_language
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    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,
 | 
			
		||||
                preferred_language=user.preferred_language
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
                preferred_language=user.preferred_language
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_start_menu(display_key: str, user: User):
 | 
			
		||||
    """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
 | 
			
		||||
@ -150,16 +277,41 @@ def process_start_menu(display_key: str, user: User):
 | 
			
		||||
    :return: Corresponding translation text response
 | 
			
		||||
    :rtype: str
 | 
			
		||||
    """
 | 
			
		||||
    balance_manager = BalanceManager(address=user.blockchain_address,
 | 
			
		||||
                                     chain_str=UssdStateMachine.chain_str,
 | 
			
		||||
    chain_str = Chain.spec.__str__()
 | 
			
		||||
    blockchain_address = user.blockchain_address
 | 
			
		||||
    balance_manager = BalanceManager(address=blockchain_address,
 | 
			
		||||
                                     chain_str=chain_str,
 | 
			
		||||
                                     token_symbol='SRF')
 | 
			
		||||
    balance = balance_manager.get_operational_balance()
 | 
			
		||||
 | 
			
		||||
    # 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:]),
 | 
			
		||||
        salt='cic.balances_data'
 | 
			
		||||
    )
 | 
			
		||||
    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 metadata
 | 
			
		||||
    s_query_user_metadata = celery.signature(
 | 
			
		||||
        'cic_ussd.tasks.metadata.query_user_metadata',
 | 
			
		||||
        [blockchain_address]
 | 
			
		||||
    )
 | 
			
		||||
    s_query_user_metadata.apply_async(queue='cic-ussd')
 | 
			
		||||
 | 
			
		||||
    # retrieve and cache account's statement
 | 
			
		||||
    retrieve_account_statement(blockchain_address=blockchain_address)
 | 
			
		||||
 | 
			
		||||
    # TODO [Philip]: figure out how to get token symbol from a metadata layer of sorts.
 | 
			
		||||
    token_symbol = 'SRF'
 | 
			
		||||
    logg.debug("Requires integration to determine user's balance and token.")
 | 
			
		||||
 | 
			
		||||
    return translation_for(
 | 
			
		||||
        key=display_key,
 | 
			
		||||
        preferred_language=user.preferred_language,
 | 
			
		||||
        account_balance=balance,
 | 
			
		||||
        account_balance=operational_balance,
 | 
			
		||||
        account_token_name=token_symbol
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -241,5 +393,11 @@ def custom_display_text(
 | 
			
		||||
        return process_exit_successful_transaction(display_key=display_key, user=user, ussd_session=ussd_session)
 | 
			
		||||
    elif menu_name == 'start':
 | 
			
		||||
        return process_start_menu(display_key=display_key, user=user)
 | 
			
		||||
    elif 'pin_authorization' in menu_name:
 | 
			
		||||
        return process_pin_authorization(display_key=display_key, user=user)
 | 
			
		||||
    elif menu_name == 'account_balances':
 | 
			
		||||
        return process_account_balances(display_key=display_key, user=user, ussd_session=ussd_session)
 | 
			
		||||
    elif 'transaction_set' in menu_name:
 | 
			
		||||
        return process_account_statement(display_key=display_key, user=user, ussd_session=ussd_session)
 | 
			
		||||
    else:
 | 
			
		||||
        return translation_for(key=display_key, preferred_language=user.preferred_language)
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,52 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import hashlib
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
from redis import Redis
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InMemoryStore:
 | 
			
		||||
    cache: Redis = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cache_data(key: str, data: str):
 | 
			
		||||
    """
 | 
			
		||||
    :param key:
 | 
			
		||||
    :type key:
 | 
			
		||||
    :param data:
 | 
			
		||||
    :type data:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    cache = InMemoryStore.cache
 | 
			
		||||
    cache.set(name=key, value=data)
 | 
			
		||||
    cache.persist(name=key)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_cached_data(key: str):
 | 
			
		||||
    """
 | 
			
		||||
    :param key:
 | 
			
		||||
    :type key:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    cache = InMemoryStore.cache
 | 
			
		||||
    return cache.get(name=key)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_cached_data_key(identifier: bytes, salt: str):
 | 
			
		||||
    """
 | 
			
		||||
    :param identifier:
 | 
			
		||||
    :type identifier:
 | 
			
		||||
    :param salt:
 | 
			
		||||
    :type salt:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    hash_object = hashlib.new("sha256")
 | 
			
		||||
    hash_object.update(identifier)
 | 
			
		||||
    hash_object.update(salt.encode(encoding="utf-8"))
 | 
			
		||||
    return hash_object.digest().hex()
 | 
			
		||||
 | 
			
		||||
@ -12,14 +12,18 @@ import redis
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
from confini import Config
 | 
			
		||||
from chainlib.chain import ChainSpec
 | 
			
		||||
from urllib.parse import quote_plus
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.db import dsn_from_config
 | 
			
		||||
from cic_ussd.db.models.base import SessionBase
 | 
			
		||||
from cic_ussd.encoder import PasswordEncoder
 | 
			
		||||
from cic_ussd.files.local_files import create_local_file_data_stores, json_file_parser
 | 
			
		||||
from cic_ussd.menu.ussd_menu import UssdMenu
 | 
			
		||||
from cic_ussd.metadata.signer import Signer
 | 
			
		||||
from cic_ussd.metadata.user import UserMetadata
 | 
			
		||||
from cic_ussd.operations import (define_response_with_content,
 | 
			
		||||
                                 process_menu_interaction_requests,
 | 
			
		||||
                                 define_multilingual_responses)
 | 
			
		||||
@ -59,6 +63,7 @@ config.censor('PASSWORD', 'DATABASE')
 | 
			
		||||
# define log levels
 | 
			
		||||
if args.vv:
 | 
			
		||||
    logging.getLogger().setLevel(logging.DEBUG)
 | 
			
		||||
    logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
 | 
			
		||||
elif args.v:
 | 
			
		||||
    logging.getLogger().setLevel(logging.INFO)
 | 
			
		||||
 | 
			
		||||
@ -92,6 +97,14 @@ InMemoryStore.cache = redis.StrictRedis(host=config.get('REDIS_HOSTNAME'),
 | 
			
		||||
                                        decode_responses=True)
 | 
			
		||||
InMemoryUssdSession.redis_cache = InMemoryStore.cache
 | 
			
		||||
 | 
			
		||||
# define metadata URL
 | 
			
		||||
UserMetadata.base_url = config.get('CIC_META_URL')
 | 
			
		||||
 | 
			
		||||
# define signer values
 | 
			
		||||
Signer.gpg_path = '/tmp/.gpg'
 | 
			
		||||
Signer.gpg_passphrase = config.get('KEYS_PASSPHRASE')
 | 
			
		||||
Signer.key_file_path = config.get('KEYS_PRIVATE')
 | 
			
		||||
 | 
			
		||||
# initialize celery app
 | 
			
		||||
celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
 | 
			
		||||
 | 
			
		||||
@ -99,7 +112,13 @@ celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY
 | 
			
		||||
states = json_file_parser(filepath=config.get('STATEMACHINE_STATES'))
 | 
			
		||||
transitions = json_file_parser(filepath=config.get('STATEMACHINE_TRANSITIONS'))
 | 
			
		||||
 | 
			
		||||
UssdStateMachine.chain_str = config.get('CIC_CHAIN_SPEC')
 | 
			
		||||
chain_spec = ChainSpec(
 | 
			
		||||
    common_name=config.get('CIC_COMMON_NAME'),
 | 
			
		||||
    engine=config.get('CIC_ENGINE'),
 | 
			
		||||
    network_id=config.get('CIC_NETWORK_ID')
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
Chain.spec = chain_spec
 | 
			
		||||
UssdStateMachine.states = states
 | 
			
		||||
UssdStateMachine.transitions = transitions
 | 
			
		||||
 | 
			
		||||
@ -152,7 +171,8 @@ def application(env, start_response):
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        # handle menu interaction requests
 | 
			
		||||
        response = process_menu_interaction_requests(chain_str=config.get('CIC_CHAIN_SPEC'),
 | 
			
		||||
        chain_str = chain_spec.__str__()
 | 
			
		||||
        response = process_menu_interaction_requests(chain_str=chain_str,
 | 
			
		||||
                                                     external_session_id=external_session_id,
 | 
			
		||||
                                                     phone_number=phone_number,
 | 
			
		||||
                                                     queue=args.q,
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,8 @@ from confini import Config
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.db import dsn_from_config
 | 
			
		||||
from cic_ussd.db.models.base import SessionBase
 | 
			
		||||
from cic_ussd.metadata.signer import Signer
 | 
			
		||||
from cic_ussd.metadata.user import UserMetadata
 | 
			
		||||
from cic_ussd.redis import InMemoryStore
 | 
			
		||||
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
 | 
			
		||||
 | 
			
		||||
@ -37,6 +39,7 @@ config.censor('PASSWORD', 'DATABASE')
 | 
			
		||||
# define log levels
 | 
			
		||||
if args.vv:
 | 
			
		||||
    logging.getLogger().setLevel(logging.DEBUG)
 | 
			
		||||
    logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
 | 
			
		||||
elif args.v:
 | 
			
		||||
    logging.getLogger().setLevel(logging.INFO)
 | 
			
		||||
 | 
			
		||||
@ -59,6 +62,14 @@ InMemoryStore.cache = redis.StrictRedis(host=config.get('REDIS_HOSTNAME'),
 | 
			
		||||
                                        decode_responses=True)
 | 
			
		||||
InMemoryUssdSession.redis_cache = InMemoryStore.cache
 | 
			
		||||
 | 
			
		||||
# define metadata URL
 | 
			
		||||
UserMetadata.base_url = config.get('CIC_META_URL')
 | 
			
		||||
 | 
			
		||||
# define signer values
 | 
			
		||||
Signer.gpg_path = '/tmp/.gpg'
 | 
			
		||||
Signer.gpg_passphrase = config.get('KEYS_PASSPHRASE')
 | 
			
		||||
Signer.key_file_path = config.get('KEYS_PRIVATE')
 | 
			
		||||
 | 
			
		||||
# set up celery
 | 
			
		||||
current_app = celery.Celery(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,18 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
 | 
			
		||||
# third party imports
 | 
			
		||||
import celery
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.accounts import BalanceManager
 | 
			
		||||
from cic_ussd.balance import BalanceManager, compute_operational_balance
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.db.models.user import AccountStatus, User
 | 
			
		||||
from cic_ussd.operations import get_user_by_phone_number, save_to_in_memory_ussd_session_data
 | 
			
		||||
from cic_ussd.state_machine.state_machine import UssdStateMachine
 | 
			
		||||
from cic_ussd.operations import save_to_in_memory_ussd_session_data
 | 
			
		||||
from cic_ussd.phone_number import get_user_by_phone_number
 | 
			
		||||
from cic_ussd.redis import create_cached_data_key, get_cached_data
 | 
			
		||||
from cic_ussd.transactions import OutgoingTransactionProcessor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,22 +31,7 @@ def is_valid_recipient(state_machine_data: Tuple[str, dict, User]) -> bool:
 | 
			
		||||
    recipient = get_user_by_phone_number(phone_number=user_input)
 | 
			
		||||
    is_not_initiator = user_input != user.phone_number
 | 
			
		||||
    has_active_account_status = user.get_account_status() == AccountStatus.ACTIVE.name
 | 
			
		||||
    logg.debug('This section requires implementation of checks for user roles and authorization status of an account.')
 | 
			
		||||
    return is_not_initiator and has_active_account_status
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_token_agent(state_machine_data: Tuple[str, dict, User]) -> bool:
 | 
			
		||||
    """This function checks that a user exists, is not the initiator of the transaction, has an active account status
 | 
			
		||||
    and is authorized to perform exchange transactions.
 | 
			
		||||
    :param state_machine_data: A tuple containing user input, a ussd session and user object.
 | 
			
		||||
    :type state_machine_data: tuple
 | 
			
		||||
    :return: A user's validity
 | 
			
		||||
    :rtype: bool
 | 
			
		||||
    """
 | 
			
		||||
    user_input, ussd_session, user = state_machine_data
 | 
			
		||||
    # is_token_agent = AccountRole.TOKEN_AGENT.value in user.get_user_roles()
 | 
			
		||||
    logg.debug('This section requires implementation of user roles and authorization to facilitate exchanges.')
 | 
			
		||||
    return is_valid_recipient(state_machine_data=state_machine_data)
 | 
			
		||||
    return is_not_initiator and has_active_account_status and recipient is not None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, User]) -> bool:
 | 
			
		||||
@ -70,10 +59,17 @@ def has_sufficient_balance(state_machine_data: Tuple[str, dict, User]) -> bool:
 | 
			
		||||
    """
 | 
			
		||||
    user_input, ussd_session, user = state_machine_data
 | 
			
		||||
    balance_manager = BalanceManager(address=user.blockchain_address,
 | 
			
		||||
                                     chain_str=UssdStateMachine.chain_str,
 | 
			
		||||
                                     chain_str=Chain.spec.__str__(),
 | 
			
		||||
                                     token_symbol='SRF')
 | 
			
		||||
    balance = balance_manager.get_operational_balance()
 | 
			
		||||
    return int(user_input) <= balance
 | 
			
		||||
    # 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
@ -88,6 +84,25 @@ def save_recipient_phone_to_session_data(state_machine_data: Tuple[str, dict, Us
 | 
			
		||||
    save_to_in_memory_ussd_session_data(queue='cic-ussd', session_data=session_data, ussd_session=ussd_session)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def retrieve_recipient_metadata(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """
 | 
			
		||||
    :param state_machine_data:
 | 
			
		||||
    :type state_machine_data:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    user_input, ussd_session, user = state_machine_data
 | 
			
		||||
 | 
			
		||||
    recipient = get_user_by_phone_number(phone_number=user_input)
 | 
			
		||||
    blockchain_address = recipient.blockchain_address
 | 
			
		||||
    # retrieve and cache account's metadata
 | 
			
		||||
    s_query_user_metadata = celery.signature(
 | 
			
		||||
        'cic_ussd.tasks.metadata.query_user_metadata',
 | 
			
		||||
        [blockchain_address]
 | 
			
		||||
    )
 | 
			
		||||
    s_query_user_metadata.apply_async(queue='cic-ussd')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def save_transaction_amount_to_session_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """This function saves the phone number corresponding the intended recipients blockchain account.
 | 
			
		||||
    :param state_machine_data: A tuple containing user input, a ussd session and user object.
 | 
			
		||||
@ -113,7 +128,8 @@ def process_transaction_request(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    to_address = recipient.blockchain_address
 | 
			
		||||
    from_address = user.blockchain_address
 | 
			
		||||
    amount = int(ussd_session.get('session_data').get('transaction_amount'))
 | 
			
		||||
    outgoing_tx_processor = OutgoingTransactionProcessor(chain_str=UssdStateMachine.chain_str,
 | 
			
		||||
    chain_str = Chain.spec.__str__()
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,20 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import celery
 | 
			
		||||
from cic_types.models.person import Person, generate_metadata_pointer
 | 
			
		||||
from cic_types.models.person import generate_vcard_from_contact_data, manage_identity_data
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.db.models.user import User
 | 
			
		||||
from cic_ussd.error import UserMetadataNotFoundError
 | 
			
		||||
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__)
 | 
			
		||||
 | 
			
		||||
@ -42,7 +52,29 @@ def update_account_status_to_active(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    User.session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def save_profile_attribute_to_session_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
def process_gender_user_input(user: User, user_input: str):
 | 
			
		||||
    """
 | 
			
		||||
    :param user:
 | 
			
		||||
    :type user:
 | 
			
		||||
    :param user_input:
 | 
			
		||||
    :type user_input:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    if user.preferred_language == 'en':
 | 
			
		||||
        if user_input == '1':
 | 
			
		||||
            gender = 'Male'
 | 
			
		||||
        else:
 | 
			
		||||
            gender = 'Female'
 | 
			
		||||
    else:
 | 
			
		||||
        if user_input == '1':
 | 
			
		||||
            gender = 'Mwanaume'
 | 
			
		||||
        else:
 | 
			
		||||
            gender = 'Mwanamke'
 | 
			
		||||
    return gender
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def save_metadata_attribute_to_session_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """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
 | 
			
		||||
@ -54,16 +86,17 @@ def save_profile_attribute_to_session_data(state_machine_data: Tuple[str, dict,
 | 
			
		||||
 | 
			
		||||
    # define session data key from current state
 | 
			
		||||
    key = ''
 | 
			
		||||
    if 'first_name' in current_state:
 | 
			
		||||
        key = 'first_name'
 | 
			
		||||
    elif 'last_name' in current_state:
 | 
			
		||||
        key = 'last_name'
 | 
			
		||||
    if 'given_name' in current_state:
 | 
			
		||||
        key = 'given_name'
 | 
			
		||||
    elif 'family_name' in current_state:
 | 
			
		||||
        key = 'family_name'
 | 
			
		||||
    elif 'gender' in current_state:
 | 
			
		||||
        key = 'gender'
 | 
			
		||||
        user_input = process_gender_user_input(user=user, user_input=user_input)
 | 
			
		||||
    elif 'location' in current_state:
 | 
			
		||||
        key = 'location'
 | 
			
		||||
    elif 'business_profile' in current_state:
 | 
			
		||||
        key = 'business_profile'
 | 
			
		||||
    elif 'products' in current_state:
 | 
			
		||||
        key = 'products'
 | 
			
		||||
 | 
			
		||||
    # check if there is existing session data
 | 
			
		||||
    if ussd_session.get('session_data'):
 | 
			
		||||
@ -76,14 +109,120 @@ def save_profile_attribute_to_session_data(state_machine_data: Tuple[str, dict,
 | 
			
		||||
    save_to_in_memory_ussd_session_data(queue='cic-ussd', session_data=session_data, ussd_session=ussd_session)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def persist_profile_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """This function persists elements of the user profile stored in session data
 | 
			
		||||
def format_user_metadata(metadata: dict, user: User):
 | 
			
		||||
    """
 | 
			
		||||
    :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')
 | 
			
		||||
    location = {
 | 
			
		||||
        "area_name": metadata.get('location')
 | 
			
		||||
    }
 | 
			
		||||
    products = []
 | 
			
		||||
    if metadata.get('products'):
 | 
			
		||||
        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,
 | 
			
		||||
        "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, User]):
 | 
			
		||||
    """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 = state_machine_data
 | 
			
		||||
 | 
			
		||||
    # get session data
 | 
			
		||||
    profile_data = ussd_session.get('session_data')
 | 
			
		||||
    logg.debug('This section requires implementation of user metadata.')
 | 
			
		||||
    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_user_metadata = celery.signature(
 | 
			
		||||
        'cic_ussd.tasks.metadata.create_user_metadata',
 | 
			
		||||
        [blockchain_address, user_metadata]
 | 
			
		||||
    )
 | 
			
		||||
    s_create_user_metadata.apply_async(queue='cic-ussd')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    user_input, ussd_session, user = 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 UserMetadataNotFoundError(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')
 | 
			
		||||
    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
 | 
			
		||||
    person = Person()
 | 
			
		||||
    user_metadata = json.loads(user_metadata)
 | 
			
		||||
    deserialized_person = person.deserialize(metadata=user_metadata)
 | 
			
		||||
 | 
			
		||||
    # edit specific metadata attribute
 | 
			
		||||
    if given_name:
 | 
			
		||||
        deserialized_person.given_name = given_name
 | 
			
		||||
    elif family_name:
 | 
			
		||||
        deserialized_person.family_name = family_name
 | 
			
		||||
    elif gender:
 | 
			
		||||
        deserialized_person.gender = gender
 | 
			
		||||
    elif location:
 | 
			
		||||
        # get existing location metadata:
 | 
			
		||||
        location_data = user_metadata.get('location')
 | 
			
		||||
        location_data['area_name'] = location
 | 
			
		||||
        deserialized_person.location = location_data
 | 
			
		||||
    elif products:
 | 
			
		||||
        deserialized_person.products = products
 | 
			
		||||
 | 
			
		||||
    edited_metadata = deserialized_person.serialize()
 | 
			
		||||
 | 
			
		||||
    s_edit_user_metadata = celery.signature(
 | 
			
		||||
        'cic_ussd.tasks.metadata.edit_user_metadata',
 | 
			
		||||
        [blockchain_address, edited_metadata, 'pgp']
 | 
			
		||||
    )
 | 
			
		||||
    s_edit_user_metadata.apply_async(queue='cic-ussd')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_user_metadata(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    user_input, ussd_session, user = state_machine_data
 | 
			
		||||
    blockchain_address = user.blockchain_address
 | 
			
		||||
    s_get_user_metadata = celery.signature(
 | 
			
		||||
        'cic_ussd.tasks.metadata.query_user_metadata',
 | 
			
		||||
        [blockchain_address]
 | 
			
		||||
    )
 | 
			
		||||
    s_get_user_metadata.apply_async(queue='cic-ussd')
 | 
			
		||||
 | 
			
		||||
@ -3,55 +3,30 @@ import logging
 | 
			
		||||
import re
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
from cic_types.models.person import generate_metadata_pointer
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.db.models.user import User
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
 | 
			
		||||
from cic_ussd.redis import get_cached_data
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_complete_profile_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
def has_cached_user_metadata(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """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 = state_machine_data
 | 
			
		||||
    logg.debug('This section requires implementation of user metadata.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_empty_username_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """This function checks whether the aspects of the user's name metadata is 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 = state_machine_data
 | 
			
		||||
    logg.debug('This section requires implementation of user metadata.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_empty_gender_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """This function checks whether the aspects of the user's gender metadata is 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 = state_machine_data
 | 
			
		||||
    logg.debug('This section requires implementation of user metadata.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_empty_location_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """This function checks whether the aspects of the user's location metadata is 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 = state_machine_data
 | 
			
		||||
    logg.debug('This section requires implementation of user metadata.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_empty_business_profile_data(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """This function checks whether the aspects of the user's business profile metadata is 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 = state_machine_data
 | 
			
		||||
    logg.debug('This section requires implementation of user metadata.')
 | 
			
		||||
    # 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_name(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
@ -66,3 +41,18 @@ def is_valid_name(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
        return True
 | 
			
		||||
    else:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_gender_selection(state_machine_data: Tuple[str, dict, User]):
 | 
			
		||||
    """
 | 
			
		||||
    :param state_machine_data:
 | 
			
		||||
    :type state_machine_data:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    user_input, ussd_session, user = state_machine_data
 | 
			
		||||
    selection_matcher = "^[1-2]$"
 | 
			
		||||
    if re.match(selection_matcher, user_input):
 | 
			
		||||
        return True
 | 
			
		||||
    else:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
@ -14,15 +14,11 @@ class UssdStateMachine(Machine):
 | 
			
		||||
    menu  as well as providing a means for navigating through these states based on different user inputs.
 | 
			
		||||
    It defines different helper functions that co-ordinate with the stakeholder components of the ussd menu: i.e  the
 | 
			
		||||
    User, UssdSession, UssdMenu to facilitate user interaction with ussd menu.
 | 
			
		||||
 | 
			
		||||
    :cvar chain_str: The chain name and network id.
 | 
			
		||||
    :type chain_str: str
 | 
			
		||||
    :cvar states: A list of pre-defined states.
 | 
			
		||||
    :type states: list
 | 
			
		||||
    :cvar transitions: A list of pre-defined transitions.
 | 
			
		||||
    :type transitions: list
 | 
			
		||||
    """
 | 
			
		||||
    chain_str = None
 | 
			
		||||
    states = []
 | 
			
		||||
    transitions = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import celery
 | 
			
		||||
 | 
			
		||||
celery_app = celery.current_app
 | 
			
		||||
# export external celery task modules
 | 
			
		||||
from .foo import log_it_plz
 | 
			
		||||
from .ussd import persist_session_to_db
 | 
			
		||||
from .callback_handler import process_account_creation_callback
 | 
			
		||||
from .logger import *
 | 
			
		||||
from .ussd_session import *
 | 
			
		||||
from .callback_handler import *
 | 
			
		||||
from .metadata import *
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								apps/cic-ussd/cic_ussd/tasks/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/cic-ussd/cic_ussd/tasks/base.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import celery
 | 
			
		||||
import sqlalchemy
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CriticalTask(celery.Task):
 | 
			
		||||
    retry_jitter = True
 | 
			
		||||
    retry_backoff = True
 | 
			
		||||
    retry_backoff_max = 8
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CriticalSQLAlchemyTask(CriticalTask):
 | 
			
		||||
    autoretry_for = (
 | 
			
		||||
        sqlalchemy.exc.DatabaseError,
 | 
			
		||||
        sqlalchemy.exc.TimeoutError,
 | 
			
		||||
    )
 | 
			
		||||
@ -1,23 +1,26 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import celery
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.conversions import from_wei
 | 
			
		||||
from cic_ussd.db.models.base import SessionBase
 | 
			
		||||
from cic_ussd.db.models.user import User
 | 
			
		||||
from cic_ussd.account import define_account_tx_metadata
 | 
			
		||||
from cic_ussd.error import ActionDataNotFoundError
 | 
			
		||||
from cic_ussd.redis import InMemoryStore
 | 
			
		||||
from cic_ussd.redis import InMemoryStore, cache_data, create_cached_data_key
 | 
			
		||||
from cic_ussd.tasks.base import CriticalSQLAlchemyTask
 | 
			
		||||
from cic_ussd.transactions import IncomingTransactionProcessor
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger(__file__)
 | 
			
		||||
celery_app = celery.current_app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@celery_app.task(bind=True)
 | 
			
		||||
@celery_app.task(bind=True, base=CriticalSQLAlchemyTask)
 | 
			
		||||
def process_account_creation_callback(self, result: str, url: str, status_code: int):
 | 
			
		||||
    """This function defines a task that creates a user and
 | 
			
		||||
    :param result: The blockchain address for the created account
 | 
			
		||||
@ -49,14 +52,14 @@ def process_account_creation_callback(self, result: str, url: str, status_code:
 | 
			
		||||
            user = User(blockchain_address=result, phone_number=phone_number)
 | 
			
		||||
            session.add(user)
 | 
			
		||||
            session.commit()
 | 
			
		||||
            session.close()
 | 
			
		||||
 | 
			
		||||
            # expire cache
 | 
			
		||||
            cache.expire(task_id, timedelta(seconds=30))
 | 
			
		||||
            session.close()
 | 
			
		||||
            cache.expire(task_id, timedelta(seconds=180))
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            cache.expire(task_id, timedelta(seconds=30))
 | 
			
		||||
            session.close()
 | 
			
		||||
            cache.expire(task_id, timedelta(seconds=180))
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        session.close()
 | 
			
		||||
@ -65,9 +68,8 @@ def process_account_creation_callback(self, result: str, url: str, status_code:
 | 
			
		||||
 | 
			
		||||
@celery_app.task
 | 
			
		||||
def process_incoming_transfer_callback(result: dict, param: str, status_code: int):
 | 
			
		||||
    logg.debug(f'PARAM: {param}, RESULT: {result}, STATUS_CODE: {status_code}')
 | 
			
		||||
    session = SessionBase.create_session()
 | 
			
		||||
    if result and status_code == 0:
 | 
			
		||||
    if status_code == 0:
 | 
			
		||||
 | 
			
		||||
        # collect result data
 | 
			
		||||
        recipient_blockchain_address = result.get('recipient')
 | 
			
		||||
@ -93,22 +95,125 @@ def process_incoming_transfer_callback(result: dict, param: str, status_code: in
 | 
			
		||||
                                                             value=value)
 | 
			
		||||
 | 
			
		||||
        if param == 'tokengift':
 | 
			
		||||
            logg.debug('Name information would require integration with cic meta.')
 | 
			
		||||
            incoming_tx_processor.process_token_gift_incoming_transactions(first_name="")
 | 
			
		||||
            incoming_tx_processor.process_token_gift_incoming_transactions()
 | 
			
		||||
        elif param == 'transfer':
 | 
			
		||||
            logg.debug('Name information would require integration with cic meta.')
 | 
			
		||||
            if sender_user:
 | 
			
		||||
                sender_information = f'{sender_user.phone_number}, {""}, {""}'
 | 
			
		||||
                incoming_tx_processor.process_transfer_incoming_transaction(sender_information=sender_information)
 | 
			
		||||
                sender_information = define_account_tx_metadata(user=sender_user)
 | 
			
		||||
                incoming_tx_processor.process_transfer_incoming_transaction(
 | 
			
		||||
                    sender_information=sender_information,
 | 
			
		||||
                    recipient_blockchain_address=recipient_blockchain_address
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                logg.warning(
 | 
			
		||||
                    f'Tx with sender: {sender_blockchain_address} was received but has no matching user in the system.'
 | 
			
		||||
                )
 | 
			
		||||
                incoming_tx_processor.process_transfer_incoming_transaction(
 | 
			
		||||
                    sender_information=sender_blockchain_address)
 | 
			
		||||
                    sender_information='GRASSROOTS ECONOMICS',
 | 
			
		||||
                    recipient_blockchain_address=recipient_blockchain_address
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            session.close()
 | 
			
		||||
            raise ValueError(f'Unexpected transaction param: {param}.')
 | 
			
		||||
    else:
 | 
			
		||||
        session.close()
 | 
			
		||||
        raise ValueError(f'Unexpected status code: {status_code}.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@celery_app.task
 | 
			
		||||
def process_balances_callback(result: list, param: str, status_code: int):
 | 
			
		||||
    if status_code == 0:
 | 
			
		||||
        balances_data = result[0]
 | 
			
		||||
        blockchain_address = balances_data.get('address')
 | 
			
		||||
        key = create_cached_data_key(
 | 
			
		||||
            identifier=bytes.fromhex(blockchain_address[2:]),
 | 
			
		||||
            salt='cic.balances_data'
 | 
			
		||||
        )
 | 
			
		||||
        cache_data(key=key, data=json.dumps(balances_data))
 | 
			
		||||
    else:
 | 
			
		||||
        raise ValueError(f'Unexpected status code: {status_code}.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO: clean up this handler
 | 
			
		||||
def define_transaction_action_tag(
 | 
			
		||||
        preferred_language: str,
 | 
			
		||||
        sender_blockchain_address: str,
 | 
			
		||||
        param: str):
 | 
			
		||||
    # check if out going ot incoming transaction
 | 
			
		||||
    if sender_blockchain_address == param:
 | 
			
		||||
        # check preferred language
 | 
			
		||||
        if preferred_language == 'en':
 | 
			
		||||
            action_tag = 'SENT'
 | 
			
		||||
        else:
 | 
			
		||||
            action_tag = 'ULITUMA'
 | 
			
		||||
    else:
 | 
			
		||||
        if preferred_language == 'en':
 | 
			
		||||
            action_tag = 'RECEIVED'
 | 
			
		||||
        else:
 | 
			
		||||
            action_tag = 'ULIPOKEA'
 | 
			
		||||
    return action_tag
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@celery_app.task
 | 
			
		||||
def process_statement_callback(result, param: str, status_code: int):
 | 
			
		||||
    if status_code == 0:
 | 
			
		||||
        # create session
 | 
			
		||||
        session = SessionBase.create_session()
 | 
			
		||||
        processed_transactions = []
 | 
			
		||||
 | 
			
		||||
        # process transaction data to cache
 | 
			
		||||
        for transaction in result:
 | 
			
		||||
            sender_blockchain_address = transaction.get('sender')
 | 
			
		||||
            recipient_address = transaction.get('recipient')
 | 
			
		||||
            source_token = transaction.get('source_token')
 | 
			
		||||
            destination_token_symbol = transaction.get('destination_token_symbol')
 | 
			
		||||
 | 
			
		||||
            # filter out any transactions that are "gassy"
 | 
			
		||||
            if '0x0000000000000000000000000000000000000000' in source_token:
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                # describe a processed transaction
 | 
			
		||||
                processed_transaction = {}
 | 
			
		||||
 | 
			
		||||
                # check if sender is in the system
 | 
			
		||||
                sender: User = session.query(User).filter_by(blockchain_address=sender_blockchain_address).first()
 | 
			
		||||
                if sender:
 | 
			
		||||
                    processed_transaction['sender_phone_number'] = sender.phone_number
 | 
			
		||||
 | 
			
		||||
                    action_tag = define_transaction_action_tag(
 | 
			
		||||
                        preferred_language=sender.preferred_language,
 | 
			
		||||
                        sender_blockchain_address=sender_blockchain_address,
 | 
			
		||||
                        param=param
 | 
			
		||||
                    )
 | 
			
		||||
                    processed_transaction['action_tag'] = action_tag
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    processed_transaction['sender_phone_number'] = 'GRASSROOTS ECONOMICS'
 | 
			
		||||
 | 
			
		||||
                # check if recipient is in the system
 | 
			
		||||
                recipient: User = session.query(User).filter_by(blockchain_address=recipient_address).first()
 | 
			
		||||
                if recipient:
 | 
			
		||||
                    processed_transaction['recipient_phone_number'] = recipient.phone_number
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    logg.warning(f'Tx with recipient not found in cic-ussd')
 | 
			
		||||
 | 
			
		||||
                # add transaction values
 | 
			
		||||
                processed_transaction['destination_value'] = from_wei(value=transaction.get('destination_value'))
 | 
			
		||||
                processed_transaction['destination_token_symbol'] = destination_token_symbol
 | 
			
		||||
                processed_transaction['source_value'] = from_wei(value=transaction.get('source_value'))
 | 
			
		||||
 | 
			
		||||
                raw_timestamp = transaction.get('timestamp')
 | 
			
		||||
                timestamp = datetime.utcfromtimestamp(raw_timestamp).strftime('%d/%m/%y, %H:%M')
 | 
			
		||||
                processed_transaction['timestamp'] = timestamp
 | 
			
		||||
 | 
			
		||||
                processed_transactions.append(processed_transaction)
 | 
			
		||||
 | 
			
		||||
        # cache account statement
 | 
			
		||||
        identifier = bytes.fromhex(param[2:])
 | 
			
		||||
        key = create_cached_data_key(identifier=identifier, salt='cic.statement')
 | 
			
		||||
        data = json.dumps(processed_transactions)
 | 
			
		||||
 | 
			
		||||
        # cache statement data
 | 
			
		||||
        cache_data(key=key, data=data)
 | 
			
		||||
    else:
 | 
			
		||||
        raise ValueError(f'Unexpected status code: {status_code}.')
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								apps/cic-ussd/cic_ussd/tasks/metadata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								apps/cic-ussd/cic_ussd/tasks/metadata.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import celery
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
 | 
			
		||||
from cic_ussd.metadata.user import UserMetadata
 | 
			
		||||
 | 
			
		||||
celery_app = celery.current_app
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@celery_app.task
 | 
			
		||||
def query_user_metadata(blockchain_address: str):
 | 
			
		||||
    """
 | 
			
		||||
    :param blockchain_address:
 | 
			
		||||
    :type blockchain_address:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
 | 
			
		||||
    user_metadata_client = UserMetadata(identifier=identifier)
 | 
			
		||||
    user_metadata_client.query()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@celery_app.task
 | 
			
		||||
def create_user_metadata(blockchain_address: str, data: dict):
 | 
			
		||||
    """
 | 
			
		||||
    :param blockchain_address:
 | 
			
		||||
    :type blockchain_address:
 | 
			
		||||
    :param data:
 | 
			
		||||
    :type data:
 | 
			
		||||
    :return:
 | 
			
		||||
    :rtype:
 | 
			
		||||
    """
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
 | 
			
		||||
    user_metadata_client = UserMetadata(identifier=identifier)
 | 
			
		||||
    user_metadata_client.create(data=data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@celery_app.task
 | 
			
		||||
def edit_user_metadata(blockchain_address: str, data: bytes, engine: str):
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address)
 | 
			
		||||
    user_metadata_client = UserMetadata(identifier=identifier)
 | 
			
		||||
    user_metadata_client.edit(data=data, engine=engine)
 | 
			
		||||
@ -11,12 +11,13 @@ from cic_ussd.db.models.base import SessionBase
 | 
			
		||||
from cic_ussd.db.models.ussd_session import UssdSession
 | 
			
		||||
from cic_ussd.error import SessionNotFoundError
 | 
			
		||||
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
 | 
			
		||||
from cic_ussd.tasks.base import CriticalSQLAlchemyTask
 | 
			
		||||
 | 
			
		||||
celery_app = celery.current_app
 | 
			
		||||
logg = get_logger(__file__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@celery_app.task
 | 
			
		||||
@celery_app.task(base=CriticalSQLAlchemyTask)
 | 
			
		||||
def persist_session_to_db(external_session_id: str):
 | 
			
		||||
    """
 | 
			
		||||
    This task initiates the saving of the session object to the database and it's removal from the in-memory storage.
 | 
			
		||||
@ -62,11 +63,10 @@ def persist_session_to_db(external_session_id: str):
 | 
			
		||||
                in_db_ussd_session.set_data(key=key, value=value, session=session)
 | 
			
		||||
 | 
			
		||||
        session.add(in_db_ussd_session)
 | 
			
		||||
        session.commit()
 | 
			
		||||
        session.close()
 | 
			
		||||
        InMemoryUssdSession.redis_cache.expire(external_session_id, timedelta(minutes=1))
 | 
			
		||||
    else:
 | 
			
		||||
        session.close()
 | 
			
		||||
        raise SessionNotFoundError('Session does not exist!')
 | 
			
		||||
 | 
			
		||||
    session.commit()
 | 
			
		||||
    session.close()
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ from datetime import datetime
 | 
			
		||||
from cic_eth.api import Api
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.balance import get_cached_operational_balance
 | 
			
		||||
from cic_ussd.notifications import Notifier
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -35,7 +36,7 @@ def from_wei(value: int) -> float:
 | 
			
		||||
    :return: SRF equivalent of value in Wei
 | 
			
		||||
    :rtype: float
 | 
			
		||||
    """
 | 
			
		||||
    value = float(value) / 1e+18
 | 
			
		||||
    value = float(value) / 1e+6
 | 
			
		||||
    return truncate(value=value, decimals=2)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -67,11 +68,9 @@ class IncomingTransactionProcessor:
 | 
			
		||||
        self.token_symbol = token_symbol
 | 
			
		||||
        self.value = value
 | 
			
		||||
 | 
			
		||||
    def process_token_gift_incoming_transactions(self, first_name: str):
 | 
			
		||||
    def process_token_gift_incoming_transactions(self):
 | 
			
		||||
        """This function processes incoming transactions with a "tokengift" param, it collects all appropriate data to
 | 
			
		||||
        send out notifications to users when their accounts are successfully created.
 | 
			
		||||
        :param first_name: The first name of the recipient of the token gift transaction.
 | 
			
		||||
        :type first_name: str
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        balance = from_wei(value=self.value)
 | 
			
		||||
@ -80,20 +79,22 @@ class IncomingTransactionProcessor:
 | 
			
		||||
                                       phone_number=self.phone_number,
 | 
			
		||||
                                       preferred_language=self.preferred_language,
 | 
			
		||||
                                       balance=balance,
 | 
			
		||||
                                       first_name=first_name,
 | 
			
		||||
                                       token_symbol=self.token_symbol)
 | 
			
		||||
 | 
			
		||||
    def process_transfer_incoming_transaction(self, sender_information: str):
 | 
			
		||||
    def process_transfer_incoming_transaction(self, sender_information: str, recipient_blockchain_address: str):
 | 
			
		||||
        """This function processes incoming transactions with the "transfer" param and issues notifications to users
 | 
			
		||||
        about reception of funds into their accounts.
 | 
			
		||||
        :param sender_information: A string with a user's full name and phone number.
 | 
			
		||||
        :type sender_information: str
 | 
			
		||||
        :param recipient_blockchain_address:
 | 
			
		||||
        type recipient_blockchain_address: str
 | 
			
		||||
        """
 | 
			
		||||
        key = 'sms.received_tokens'
 | 
			
		||||
        amount = from_wei(value=self.value)
 | 
			
		||||
        timestamp = datetime.now().strftime('%d-%m-%y, %H:%M %p')
 | 
			
		||||
 | 
			
		||||
        logg.debug('Balance requires implementation of cic-eth integration with balance.')
 | 
			
		||||
        operational_balance = get_cached_operational_balance(blockchain_address=recipient_blockchain_address)
 | 
			
		||||
 | 
			
		||||
        notifier.send_sms_notification(key=key,
 | 
			
		||||
                                       phone_number=self.phone_number,
 | 
			
		||||
                                       preferred_language=self.preferred_language,
 | 
			
		||||
@ -101,7 +102,7 @@ class IncomingTransactionProcessor:
 | 
			
		||||
                                       token_symbol=self.token_symbol,
 | 
			
		||||
                                       tx_sender_information=sender_information,
 | 
			
		||||
                                       timestamp=timestamp,
 | 
			
		||||
                                       balance='')
 | 
			
		||||
                                       balance=operational_balance)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutgoingTransactionProcessor:
 | 
			
		||||
 | 
			
		||||
@ -110,7 +110,7 @@ def validate_phone_number(phone: str):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_response_type(processor_response: str) -> bool:
 | 
			
		||||
    """
 | 
			
		||||
    """1*3443*3443*Philip*Wanga*1*Juja*Software Developer*2*3
 | 
			
		||||
    This function checks the prefix for a corresponding menu's text from the response offered by the Ussd Processor and
 | 
			
		||||
    determines whether the response should prompt the end of a ussd session or the
 | 
			
		||||
    :param processor_response: A ussd menu's text value.
 | 
			
		||||
 | 
			
		||||
@ -6,10 +6,12 @@ betterpath==0.2.2
 | 
			
		||||
billiard==3.6.3.0
 | 
			
		||||
celery==4.4.7
 | 
			
		||||
cffi==1.14.3
 | 
			
		||||
cic-eth~=0.10.0a22
 | 
			
		||||
chainlib~=0.0.1a15
 | 
			
		||||
cic-eth==0.10.0a38
 | 
			
		||||
cic-notify==0.3.1
 | 
			
		||||
cic-types==0.1.0a8
 | 
			
		||||
click==7.1.2
 | 
			
		||||
confini~=0.3.6a1
 | 
			
		||||
confini==0.3.5
 | 
			
		||||
cryptography==3.2.1
 | 
			
		||||
faker==4.17.1
 | 
			
		||||
iniconfig==1.1.1
 | 
			
		||||
@ -34,6 +36,7 @@ python-i18n==0.3.9
 | 
			
		||||
pytz==2020.1
 | 
			
		||||
PyYAML==5.3.1
 | 
			
		||||
redis==3.5.3
 | 
			
		||||
requests==2.24.0
 | 
			
		||||
semver==2.13.0
 | 
			
		||||
six==1.15.0
 | 
			
		||||
SQLAlchemy==1.3.20
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,13 @@
 | 
			
		||||
[
 | 
			
		||||
  "account_management",
 | 
			
		||||
  "profile_management",
 | 
			
		||||
  "metadata_management",
 | 
			
		||||
  "select_preferred_language",
 | 
			
		||||
  "enter_current_pin",
 | 
			
		||||
  "mini_statement_inquiry_pin_authorization",
 | 
			
		||||
  "enter_new_pin",
 | 
			
		||||
  "new_pin_confirmation",
 | 
			
		||||
  "display_user_profile_data"
 | 
			
		||||
  "display_user_metadata",
 | 
			
		||||
  "standard_pin_authorization",
 | 
			
		||||
  "account_balances_pin_authorization",
 | 
			
		||||
  "account_statement_pin_authorization",
 | 
			
		||||
  "account_balances"
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										5
									
								
								apps/cic-ussd/states/account_statement_states.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/cic-ussd/states/account_statement_states.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
[
 | 
			
		||||
  "first_transaction_set",
 | 
			
		||||
  "middle_transaction_set",
 | 
			
		||||
  "last_transaction_set"
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										8
									
								
								apps/cic-ussd/states/user_metadata_states.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								apps/cic-ussd/states/user_metadata_states.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
[
 | 
			
		||||
  "enter_given_name",
 | 
			
		||||
  "enter_family_name",
 | 
			
		||||
  "enter_gender",
 | 
			
		||||
  "enter_age",
 | 
			
		||||
  "enter_location",
 | 
			
		||||
  "enter_products"
 | 
			
		||||
]
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
[
 | 
			
		||||
  "enter_first_name",
 | 
			
		||||
  "enter_last_name",
 | 
			
		||||
  "enter_gender",
 | 
			
		||||
  "enter_location",
 | 
			
		||||
  "enter_business_profile",
 | 
			
		||||
  "view_profile_pin_authorization"
 | 
			
		||||
]
 | 
			
		||||
@ -3,4 +3,5 @@ pytest-alembic==0.2.5
 | 
			
		||||
pytest-celery==0.0.0a1
 | 
			
		||||
pytest-cov==2.10.1
 | 
			
		||||
pytest-mock==3.3.1
 | 
			
		||||
pytest-redis==2.0.0
 | 
			
		||||
pytest-redis==2.0.0
 | 
			
		||||
requests-mock==1.8.0
 | 
			
		||||
							
								
								
									
										30
									
								
								apps/cic-ussd/tests/cic_ussd/db/test_db.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/cic-ussd/tests/cic_ussd/db/test_db.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.db import dsn_from_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_dsn_from_config(load_config):
 | 
			
		||||
    """
 | 
			
		||||
    """
 | 
			
		||||
    # test dsn for sqlite engine
 | 
			
		||||
    dsn = dsn_from_config(load_config)
 | 
			
		||||
    scheme = f'{load_config.get("DATABASE_ENGINE")}+{load_config.get("DATABASE_DRIVER")}'
 | 
			
		||||
    assert dsn == f'{scheme}:///{load_config.get("DATABASE_NAME")}'
 | 
			
		||||
 | 
			
		||||
    # test dsn for other db formats
 | 
			
		||||
    overrides = {
 | 
			
		||||
        'DATABASE_PASSWORD': 'password',
 | 
			
		||||
        'DATABASE_DRIVER': 'psycopg2',
 | 
			
		||||
        'DATABASE_ENGINE': 'postgresql'
 | 
			
		||||
    }
 | 
			
		||||
    load_config.dict_override(dct=overrides, dct_description='Override values to test different db formats.')
 | 
			
		||||
 | 
			
		||||
    scheme = f'{load_config.get("DATABASE_ENGINE")}+{load_config.get("DATABASE_DRIVER")}'
 | 
			
		||||
 | 
			
		||||
    dsn = dsn_from_config(load_config)
 | 
			
		||||
    assert dsn == f"{scheme}://{load_config.get('DATABASE_USER')}:{load_config.get('DATABASE_PASSWORD')}@{load_config.get('DATABASE_HOST')}:{load_config.get('DATABASE_PORT')}/{load_config.get('DATABASE_NAME')}"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										80
									
								
								apps/cic-ussd/tests/cic_ussd/metadata/test_metadata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								apps/cic-ussd/tests/cic_ussd/metadata/test_metadata.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import pytest
 | 
			
		||||
import requests
 | 
			
		||||
import requests_mock
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.error import UnsupportedMethodError
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer, make_request
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_make_request(define_metadata_pointer_url, mock_meta_get_response, mock_meta_post_response, person_metadata):
 | 
			
		||||
    with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
        request_mocker.register_uri(
 | 
			
		||||
            'GET',
 | 
			
		||||
            define_metadata_pointer_url,
 | 
			
		||||
            status_code=200,
 | 
			
		||||
            reason='OK',
 | 
			
		||||
            content=json.dumps(mock_meta_get_response).encode('utf-8')
 | 
			
		||||
        )
 | 
			
		||||
        response = make_request(method='GET', url=define_metadata_pointer_url)
 | 
			
		||||
        assert response.content == requests.get(define_metadata_pointer_url).content
 | 
			
		||||
 | 
			
		||||
    with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
        request_mocker.register_uri(
 | 
			
		||||
            'POST',
 | 
			
		||||
            define_metadata_pointer_url,
 | 
			
		||||
            status_code=201,
 | 
			
		||||
            reason='CREATED',
 | 
			
		||||
            content=json.dumps(mock_meta_post_response).encode('utf-8')
 | 
			
		||||
        )
 | 
			
		||||
        response = make_request(
 | 
			
		||||
            method='POST',
 | 
			
		||||
            url=define_metadata_pointer_url,
 | 
			
		||||
            data=json.dumps(person_metadata).encode('utf-8'),
 | 
			
		||||
            headers={
 | 
			
		||||
                'X-CIC-AUTOMERGE': 'server',
 | 
			
		||||
                'Content-Type': 'application/json'
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        assert response.content == requests.post(define_metadata_pointer_url).content
 | 
			
		||||
 | 
			
		||||
    with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
        request_mocker.register_uri(
 | 
			
		||||
            'PUT',
 | 
			
		||||
            define_metadata_pointer_url,
 | 
			
		||||
            status_code=200,
 | 
			
		||||
            reason='OK'
 | 
			
		||||
        )
 | 
			
		||||
        response = make_request(
 | 
			
		||||
            method='PUT',
 | 
			
		||||
            url=define_metadata_pointer_url,
 | 
			
		||||
            data=json.dumps(person_metadata).encode('utf-8'),
 | 
			
		||||
            headers={
 | 
			
		||||
                'X-CIC-AUTOMERGE': 'server',
 | 
			
		||||
                'Content-Type': 'application/json'
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        assert response.content == requests.put(define_metadata_pointer_url).content
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(UnsupportedMethodError) as error:
 | 
			
		||||
        with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
            request_mocker.register_uri(
 | 
			
		||||
                'DELETE',
 | 
			
		||||
                define_metadata_pointer_url,
 | 
			
		||||
                status_code=200,
 | 
			
		||||
                reason='OK'
 | 
			
		||||
            )
 | 
			
		||||
            make_request(
 | 
			
		||||
                method='DELETE',
 | 
			
		||||
                url=define_metadata_pointer_url
 | 
			
		||||
            )
 | 
			
		||||
        assert str(error.value) == 'Unsupported method: DELETE'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_blockchain_address_to_metadata_pointer(create_activated_user):
 | 
			
		||||
    blockchain_address = create_activated_user.blockchain_address
 | 
			
		||||
    assert type(blockchain_address_to_metadata_pointer(blockchain_address)) == bytes
 | 
			
		||||
							
								
								
									
										34
									
								
								apps/cic-ussd/tests/cic_ussd/metadata/test_signer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								apps/cic-ussd/tests/cic_ussd/metadata/test_signer.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import shutil
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.metadata.signer import Signer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_client(load_config, setup_metadata_signer, person_metadata):
 | 
			
		||||
    signer = Signer()
 | 
			
		||||
    # get gpg used
 | 
			
		||||
    digest = 'a4337bc45a8fc544c03f52dc550cd6e1e87021bc896588bd79e901e2'
 | 
			
		||||
    person_metadata['digest'] = digest
 | 
			
		||||
    gpg = signer.gpg
 | 
			
		||||
 | 
			
		||||
    # check that key data was loaded
 | 
			
		||||
    assert signer.key_data is not None
 | 
			
		||||
 | 
			
		||||
    # check that correct operational key is returned
 | 
			
		||||
    gpg.import_keys(key_data=signer.key_data)
 | 
			
		||||
    gpg_keys = gpg.list_keys()
 | 
			
		||||
    assert signer.get_operational_key() == gpg_keys[0]
 | 
			
		||||
 | 
			
		||||
    # check that correct signature is returned
 | 
			
		||||
    key_id = signer.get_operational_key().get('keyid')
 | 
			
		||||
    signature = gpg.sign(message=digest, passphrase=load_config.get('KEYS_PASSPHRASE'), keyid=key_id)
 | 
			
		||||
    assert str(signature) == signer.sign_digest(data=person_metadata)
 | 
			
		||||
 | 
			
		||||
    # remove tmp gpg file
 | 
			
		||||
    shutil.rmtree(Signer.gpg_path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										123
									
								
								apps/cic-ussd/tests/cic_ussd/metadata/test_user_metadata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								apps/cic-ussd/tests/cic_ussd/metadata/test_user_metadata.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import pytest
 | 
			
		||||
import requests_mock
 | 
			
		||||
from cic_types.models.person import generate_metadata_pointer
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
 | 
			
		||||
from cic_ussd.metadata.signer import Signer
 | 
			
		||||
from cic_ussd.metadata.user import UserMetadata
 | 
			
		||||
from cic_ussd.redis import get_cached_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_user_metadata(create_activated_user, define_metadata_pointer_url, load_config):
 | 
			
		||||
    UserMetadata.base_url = load_config.get('CIC_META_URL')
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
 | 
			
		||||
    user_metadata_client = UserMetadata(identifier=identifier)
 | 
			
		||||
 | 
			
		||||
    assert user_metadata_client.url == define_metadata_pointer_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_create_user_metadata(caplog,
 | 
			
		||||
                              create_activated_user,
 | 
			
		||||
                              define_metadata_pointer_url,
 | 
			
		||||
                              load_config,
 | 
			
		||||
                              mock_meta_post_response,
 | 
			
		||||
                              person_metadata):
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
 | 
			
		||||
    user_metadata_client = UserMetadata(identifier=identifier)
 | 
			
		||||
 | 
			
		||||
    with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
        request_mocker.register_uri(
 | 
			
		||||
            'POST',
 | 
			
		||||
            define_metadata_pointer_url,
 | 
			
		||||
            status_code=201,
 | 
			
		||||
            reason='CREATED',
 | 
			
		||||
            content=json.dumps(mock_meta_post_response).encode('utf-8')
 | 
			
		||||
        )
 | 
			
		||||
        user_metadata_client.create(data=person_metadata)
 | 
			
		||||
        assert 'Get signed material response status: 201' in caplog.text
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(RuntimeError) as error:
 | 
			
		||||
        with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
            request_mocker.register_uri(
 | 
			
		||||
                'POST',
 | 
			
		||||
                define_metadata_pointer_url,
 | 
			
		||||
                status_code=400,
 | 
			
		||||
                reason='BAD REQUEST'
 | 
			
		||||
            )
 | 
			
		||||
            user_metadata_client.create(data=person_metadata)
 | 
			
		||||
        assert str(error.value) == f'400 Client Error: BAD REQUEST for url: {define_metadata_pointer_url}'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_edit_user_metadata(caplog,
 | 
			
		||||
                            create_activated_user,
 | 
			
		||||
                            define_metadata_pointer_url,
 | 
			
		||||
                            load_config,
 | 
			
		||||
                            person_metadata,
 | 
			
		||||
                            setup_metadata_signer):
 | 
			
		||||
    Signer.gpg_passphrase = load_config.get('KEYS_PASSPHRASE')
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
 | 
			
		||||
    user_metadata_client = UserMetadata(identifier=identifier)
 | 
			
		||||
    with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
        request_mocker.register_uri(
 | 
			
		||||
            'PUT',
 | 
			
		||||
            define_metadata_pointer_url,
 | 
			
		||||
            status_code=200,
 | 
			
		||||
            reason='OK'
 | 
			
		||||
        )
 | 
			
		||||
        user_metadata_client.edit(data=person_metadata, engine='pgp')
 | 
			
		||||
        assert 'Signed content submission status: 200' in caplog.text
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(RuntimeError) as error:
 | 
			
		||||
        with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
            request_mocker.register_uri(
 | 
			
		||||
                'PUT',
 | 
			
		||||
                define_metadata_pointer_url,
 | 
			
		||||
                status_code=400,
 | 
			
		||||
                reason='BAD REQUEST'
 | 
			
		||||
            )
 | 
			
		||||
            user_metadata_client.edit(data=person_metadata, engine='pgp')
 | 
			
		||||
        assert str(error.value) == f'400 Client Error: BAD REQUEST for url: {define_metadata_pointer_url}'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_user_metadata(caplog,
 | 
			
		||||
                           create_activated_user,
 | 
			
		||||
                           define_metadata_pointer_url,
 | 
			
		||||
                           init_redis_cache,
 | 
			
		||||
                           load_config,
 | 
			
		||||
                           person_metadata,
 | 
			
		||||
                           setup_metadata_signer):
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
 | 
			
		||||
    user_metadata_client = UserMetadata(identifier=identifier)
 | 
			
		||||
    with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
        request_mocker.register_uri(
 | 
			
		||||
            'GET',
 | 
			
		||||
            define_metadata_pointer_url,
 | 
			
		||||
            status_code=200,
 | 
			
		||||
            content=json.dumps(person_metadata).encode('utf-8'),
 | 
			
		||||
            reason='OK'
 | 
			
		||||
        )
 | 
			
		||||
        user_metadata_client.query()
 | 
			
		||||
        assert 'Get latest data status: 200' in caplog.text
 | 
			
		||||
    key = generate_metadata_pointer(
 | 
			
		||||
        identifier=identifier,
 | 
			
		||||
        cic_type='cic.person'
 | 
			
		||||
    )
 | 
			
		||||
    cached_user_metadata = get_cached_data(key=key)
 | 
			
		||||
    assert cached_user_metadata
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(RuntimeError) as error:
 | 
			
		||||
        with requests_mock.Mocker(real_http=False) as request_mocker:
 | 
			
		||||
            request_mocker.register_uri(
 | 
			
		||||
                'GET',
 | 
			
		||||
                define_metadata_pointer_url,
 | 
			
		||||
                status_code=404,
 | 
			
		||||
                reason='NOT FOUND'
 | 
			
		||||
            )
 | 
			
		||||
            user_metadata_client.query()
 | 
			
		||||
            assert 'The data is not available and might need to be added.' in caplog.text
 | 
			
		||||
        assert str(error.value) == f'400 Client Error: NOT FOUND for url: {define_metadata_pointer_url}'
 | 
			
		||||
@ -5,7 +5,6 @@ import json
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.state_machine import UssdStateMachine
 | 
			
		||||
from cic_ussd.state_machine.logic.transaction import (has_sufficient_balance,
 | 
			
		||||
                                                      is_valid_recipient,
 | 
			
		||||
                                                      is_valid_transaction_amount,
 | 
			
		||||
@ -100,8 +99,8 @@ def test_process_transaction_request(create_valid_tx_recipient,
 | 
			
		||||
                                     create_valid_tx_sender,
 | 
			
		||||
                                     load_config,
 | 
			
		||||
                                     mock_outgoing_transactions,
 | 
			
		||||
                                     setup_chain_spec,
 | 
			
		||||
                                     ussd_session_data):
 | 
			
		||||
    UssdStateMachine.chain_str = load_config.get('CIC_CHAIN_SPEC')
 | 
			
		||||
    ussd_session_data['session_data'] = {
 | 
			
		||||
        'recipient_phone_number': create_valid_tx_recipient.phone_number,
 | 
			
		||||
        'transaction_amount': '50'
 | 
			
		||||
@ -0,0 +1,155 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
# third-party-imports
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.redis import InMemoryStore
 | 
			
		||||
from cic_ussd.state_machine.logic.user import (
 | 
			
		||||
    change_preferred_language_to_en,
 | 
			
		||||
    change_preferred_language_to_sw,
 | 
			
		||||
    edit_user_metadata_attribute,
 | 
			
		||||
    format_user_metadata,
 | 
			
		||||
    get_user_metadata,
 | 
			
		||||
    save_complete_user_metadata,
 | 
			
		||||
    process_gender_user_input,
 | 
			
		||||
    save_profile_attribute_to_session_data,
 | 
			
		||||
    update_account_status_to_active)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_change_preferred_language(create_pending_user, create_in_db_ussd_session):
 | 
			
		||||
    state_machine_data = ('', create_in_db_ussd_session, create_pending_user)
 | 
			
		||||
    assert create_pending_user.preferred_language is None
 | 
			
		||||
    change_preferred_language_to_en(state_machine_data)
 | 
			
		||||
    assert create_pending_user.preferred_language == 'en'
 | 
			
		||||
    change_preferred_language_to_sw(state_machine_data)
 | 
			
		||||
    assert create_pending_user.preferred_language == 'sw'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_update_account_status_to_active(create_pending_user, create_in_db_ussd_session):
 | 
			
		||||
    state_machine_data = ('', create_in_db_ussd_session, create_pending_user)
 | 
			
		||||
    update_account_status_to_active(state_machine_data)
 | 
			
		||||
    assert create_pending_user.get_account_status() == 'ACTIVE'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("current_state, expected_key, expected_result, user_input", [
 | 
			
		||||
    ("enter_given_name", "given_name", "John", "John"),
 | 
			
		||||
    ("enter_family_name", "family_name", "Doe", "Doe"),
 | 
			
		||||
    ("enter_gender", "gender", "Male", "1"),
 | 
			
		||||
    ("enter_location", "location", "Kangemi", "Kangemi"),
 | 
			
		||||
    ("enter_products", "products", "Mandazi", "Mandazi"),
 | 
			
		||||
])
 | 
			
		||||
def test_save_save_profile_attribute_to_session_data(current_state,
 | 
			
		||||
                                                     expected_key,
 | 
			
		||||
                                                     expected_result,
 | 
			
		||||
                                                     user_input,
 | 
			
		||||
                                                     celery_session_worker,
 | 
			
		||||
                                                     create_activated_user,
 | 
			
		||||
                                                     create_in_db_ussd_session,
 | 
			
		||||
                                                     create_in_redis_ussd_session):
 | 
			
		||||
    create_in_db_ussd_session.state = current_state
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = (user_input, serialized_in_db_ussd_session, create_activated_user)
 | 
			
		||||
    in_memory_ussd_session = InMemoryStore.cache.get('AT974186')
 | 
			
		||||
    in_memory_ussd_session = json.loads(in_memory_ussd_session)
 | 
			
		||||
    assert in_memory_ussd_session.get('session_data') == {}
 | 
			
		||||
    serialized_in_db_ussd_session['state'] = current_state
 | 
			
		||||
    save_profile_attribute_to_session_data(state_machine_data=state_machine_data)
 | 
			
		||||
 | 
			
		||||
    in_memory_ussd_session = InMemoryStore.cache.get('AT974186')
 | 
			
		||||
    in_memory_ussd_session = json.loads(in_memory_ussd_session)
 | 
			
		||||
 | 
			
		||||
    assert in_memory_ussd_session.get('session_data')[expected_key] == expected_result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("preferred_language, user_input, expected_gender_value", [
 | 
			
		||||
    ("en", "1", "Male"),
 | 
			
		||||
    ("en", "2", "Female"),
 | 
			
		||||
    ("sw", "1", "Mwanaume"),
 | 
			
		||||
    ("sw", "2", "Mwanamke"),
 | 
			
		||||
])
 | 
			
		||||
def test_process_gender_user_input(create_activated_user, expected_gender_value, preferred_language, user_input):
 | 
			
		||||
    create_activated_user.preferred_language = preferred_language
 | 
			
		||||
    gender = process_gender_user_input(user=create_activated_user, user_input=user_input)
 | 
			
		||||
    assert gender == expected_gender_value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_format_user_metadata(create_activated_user,
 | 
			
		||||
                              complete_user_metadata,
 | 
			
		||||
                              setup_chain_spec):
 | 
			
		||||
    from cic_types.models.person import Person
 | 
			
		||||
    formatted_user_metadata = format_user_metadata(metadata=complete_user_metadata, user=create_activated_user)
 | 
			
		||||
    person = Person()
 | 
			
		||||
    user_metadata = person.deserialize(metadata=formatted_user_metadata)
 | 
			
		||||
    assert formatted_user_metadata == user_metadata.serialize()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_save_complete_user_metadata(celery_session_worker,
 | 
			
		||||
                                        complete_user_metadata,
 | 
			
		||||
                                        create_activated_user,
 | 
			
		||||
                                        create_in_redis_ussd_session,
 | 
			
		||||
                                        mocker,
 | 
			
		||||
                                        setup_chain_spec,
 | 
			
		||||
                                        ussd_session_data):
 | 
			
		||||
    ussd_session = create_in_redis_ussd_session.get(ussd_session_data.get('external_session_id'))
 | 
			
		||||
    ussd_session = json.loads(ussd_session)
 | 
			
		||||
    ussd_session['session_data'] = complete_user_metadata
 | 
			
		||||
    user_metadata = format_user_metadata(metadata=ussd_session.get('session_data'), user=create_activated_user)
 | 
			
		||||
    state_machine_data = ('', ussd_session, create_activated_user)
 | 
			
		||||
    mocked_create_metadata_task = mocker.patch('cic_ussd.tasks.metadata.create_user_metadata.apply_async')
 | 
			
		||||
    save_complete_user_metadata(state_machine_data=state_machine_data)
 | 
			
		||||
    mocked_create_metadata_task.assert_called_with(
 | 
			
		||||
        (user_metadata, create_activated_user.blockchain_address),
 | 
			
		||||
        {},
 | 
			
		||||
        queue='cic-ussd'
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_edit_user_metadata_attribute(celery_session_worker,
 | 
			
		||||
                                      cached_user_metadata,
 | 
			
		||||
                                      create_activated_user,
 | 
			
		||||
                                      create_in_redis_ussd_session,
 | 
			
		||||
                                      init_redis_cache,
 | 
			
		||||
                                      mocker,
 | 
			
		||||
                                      person_metadata,
 | 
			
		||||
                                      setup_chain_spec,
 | 
			
		||||
                                      ussd_session_data):
 | 
			
		||||
    ussd_session = create_in_redis_ussd_session.get(ussd_session_data.get('external_session_id'))
 | 
			
		||||
    ussd_session = json.loads(ussd_session)
 | 
			
		||||
 | 
			
		||||
    assert person_metadata['location']['area_name'] == 'kayaba'
 | 
			
		||||
 | 
			
		||||
    # appropriately format session
 | 
			
		||||
    ussd_session['session_data'] = {
 | 
			
		||||
        'location': 'nairobi'
 | 
			
		||||
    }
 | 
			
		||||
    state_machine_data = ('', ussd_session, create_activated_user)
 | 
			
		||||
 | 
			
		||||
    mocked_edit_metadata = mocker.patch('cic_ussd.tasks.metadata.edit_user_metadata.apply_async')
 | 
			
		||||
    edit_user_metadata_attribute(state_machine_data=state_machine_data)
 | 
			
		||||
    person_metadata['location']['area_name'] = 'nairobi'
 | 
			
		||||
    mocked_edit_metadata.assert_called_with(
 | 
			
		||||
        (create_activated_user.blockchain_address, person_metadata, Chain.spec.engine()),
 | 
			
		||||
        {},
 | 
			
		||||
        queue='cic-ussd'
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_get_user_metadata_attribute(celery_session_worker,
 | 
			
		||||
                                     create_activated_user,
 | 
			
		||||
                                     create_in_redis_ussd_session,
 | 
			
		||||
                                     mocker,
 | 
			
		||||
                                     ussd_session_data):
 | 
			
		||||
    ussd_session = create_in_redis_ussd_session.get(ussd_session_data.get('external_session_id'))
 | 
			
		||||
    ussd_session = json.loads(ussd_session)
 | 
			
		||||
    state_machine_data = ('', ussd_session, create_activated_user)
 | 
			
		||||
 | 
			
		||||
    mocked_get_metadata = mocker.patch('cic_ussd.tasks.metadata.query_user_metadata.apply_async')
 | 
			
		||||
    get_user_metadata(state_machine_data=state_machine_data)
 | 
			
		||||
    mocked_get_metadata.assert_called_with(
 | 
			
		||||
        (create_activated_user.blockchain_address,),
 | 
			
		||||
        {},
 | 
			
		||||
        queue='cic-ussd'
 | 
			
		||||
    )
 | 
			
		||||
@ -0,0 +1,55 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import pytest
 | 
			
		||||
from cic_types.models.person import generate_metadata_pointer
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
 | 
			
		||||
from cic_ussd.redis import cache_data
 | 
			
		||||
from cic_ussd.state_machine.logic.validator import (is_valid_name,
 | 
			
		||||
                                                    is_valid_gender_selection,
 | 
			
		||||
                                                    has_cached_user_metadata)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("user_input, expected_result", [
 | 
			
		||||
    ("Arya", True),
 | 
			
		||||
    ("1234", False)
 | 
			
		||||
])
 | 
			
		||||
def test_is_valid_name(create_in_db_ussd_session, create_pending_user, user_input, expected_result):
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = (user_input, serialized_in_db_ussd_session, create_pending_user)
 | 
			
		||||
    result = is_valid_name(state_machine_data=state_machine_data)
 | 
			
		||||
    assert result is expected_result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_has_cached_user_metadata(create_in_db_ussd_session,
 | 
			
		||||
                                  create_activated_user,
 | 
			
		||||
                                  init_redis_cache,
 | 
			
		||||
                                  person_metadata):
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
 | 
			
		||||
    result = has_cached_user_metadata(state_machine_data=state_machine_data)
 | 
			
		||||
    assert result is False
 | 
			
		||||
    # cache metadata
 | 
			
		||||
    user = create_activated_user
 | 
			
		||||
    key = generate_metadata_pointer(
 | 
			
		||||
        identifier=blockchain_address_to_metadata_pointer(blockchain_address=user.blockchain_address),
 | 
			
		||||
        cic_type='cic.person'
 | 
			
		||||
    )
 | 
			
		||||
    cache_data(key=key, data=json.dumps(person_metadata))
 | 
			
		||||
    result = has_cached_user_metadata(state_machine_data=state_machine_data)
 | 
			
		||||
    assert result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("user_input, expected_result", [
 | 
			
		||||
    ("1", True),
 | 
			
		||||
    ("2", True),
 | 
			
		||||
    ("3", False)
 | 
			
		||||
])
 | 
			
		||||
def test_is_valid_gender_selection(create_in_db_ussd_session, create_pending_user, user_input, expected_result):
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = (user_input, serialized_in_db_ussd_session, create_pending_user)
 | 
			
		||||
    result = is_valid_gender_selection(state_machine_data=state_machine_data)
 | 
			
		||||
    assert result is expected_result
 | 
			
		||||
@ -10,7 +10,7 @@ import pytest
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.db.models.user import User
 | 
			
		||||
from cic_ussd.error import ActionDataNotFoundError
 | 
			
		||||
from cic_ussd.transactions import from_wei
 | 
			
		||||
from cic_ussd.conversions import from_wei
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
 | 
			
		||||
@ -155,6 +155,7 @@ def test_unsuccessful_incoming_transaction_recipient_not_found(celery_session_wo
 | 
			
		||||
def test_successful_incoming_transaction_sender_not_found(caplog,
 | 
			
		||||
                                                          celery_session_worker,
 | 
			
		||||
                                                          create_valid_tx_recipient,
 | 
			
		||||
                                                          mock_notifier_api,
 | 
			
		||||
                                                          successful_incoming_transfer_callback):
 | 
			
		||||
    result = successful_incoming_transfer_callback.get('RESULT')
 | 
			
		||||
    param = successful_incoming_transfer_callback.get('PARAM')
 | 
			
		||||
@ -15,7 +15,7 @@ def test_persist_session_to_db_task(
 | 
			
		||||
        create_in_redis_ussd_session):
 | 
			
		||||
    external_session_id = ussd_session_data.get('external_session_id')
 | 
			
		||||
    s_persist_session_to_db = celery.signature(
 | 
			
		||||
        'cic_ussd.tasks.ussd.persist_session_to_db',
 | 
			
		||||
        'cic_ussd.tasks.ussd_session.persist_session_to_db',
 | 
			
		||||
        [external_session_id]
 | 
			
		||||
    )
 | 
			
		||||
    result = s_persist_session_to_db.apply_async()
 | 
			
		||||
@ -38,7 +38,7 @@ def test_session_not_found_error(
 | 
			
		||||
    with pytest.raises(SessionNotFoundError) as error:
 | 
			
		||||
        external_session_id = 'SomeRandomValue'
 | 
			
		||||
        s_persist_session_to_db = celery.signature(
 | 
			
		||||
            'cic_ussd.tasks.ussd.persist_session_to_db',
 | 
			
		||||
            'cic_ussd.tasks.ussd_session.persist_session_to_db',
 | 
			
		||||
            [external_session_id]
 | 
			
		||||
        )
 | 
			
		||||
        result = s_persist_session_to_db.apply_async()
 | 
			
		||||
							
								
								
									
										20
									
								
								apps/cic-ussd/tests/cic_ussd/test_accounts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/cic-ussd/tests/cic_ussd/test_accounts.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.balance import BalanceManager
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_balance_manager(create_valid_tx_recipient, load_config, mocker, setup_chain_spec):
 | 
			
		||||
    chain_str = Chain.spec.__str__()
 | 
			
		||||
    balance_manager = BalanceManager(
 | 
			
		||||
        address=create_valid_tx_recipient.blockchain_address,
 | 
			
		||||
        chain_str=chain_str,
 | 
			
		||||
        token_symbol='SRF'
 | 
			
		||||
    )
 | 
			
		||||
    balance_manager.get_balances = mocker.MagicMock()
 | 
			
		||||
    balance_manager.get_balances()
 | 
			
		||||
 | 
			
		||||
    balance_manager.get_balances.assert_called_once()
 | 
			
		||||
@ -18,6 +18,7 @@ def test_send_sms_notification(celery_session_worker,
 | 
			
		||||
                               recipient,
 | 
			
		||||
                               set_locale_files,
 | 
			
		||||
                               mock_notifier_api):
 | 
			
		||||
 | 
			
		||||
    notifier = Notifier()
 | 
			
		||||
    notifier.queue = None
 | 
			
		||||
 | 
			
		||||
@ -27,3 +28,9 @@ def test_send_sms_notification(celery_session_worker,
 | 
			
		||||
    assert messages[0].get('message') == expected_message
 | 
			
		||||
    assert messages[0].get('recipient') == recipient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import uuid
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.db.models.task_tracker import TaskTracker
 | 
			
		||||
from cic_ussd.menu.ussd_menu import UssdMenu
 | 
			
		||||
from cic_ussd.operations import (add_tasks_to_tracker,
 | 
			
		||||
@ -17,13 +18,12 @@ from cic_ussd.operations import (add_tasks_to_tracker,
 | 
			
		||||
                                 get_latest_input,
 | 
			
		||||
                                 initiate_account_creation_request,
 | 
			
		||||
                                 process_current_menu,
 | 
			
		||||
                                 process_phone_number,
 | 
			
		||||
                                 process_menu_interaction_requests,
 | 
			
		||||
                                 cache_account_creation_task_id,
 | 
			
		||||
                                 get_user_by_phone_number,
 | 
			
		||||
                                 reset_pin,
 | 
			
		||||
                                 update_ussd_session,
 | 
			
		||||
                                 save_to_in_memory_ussd_session_data)
 | 
			
		||||
from cic_ussd.phone_number import get_user_by_phone_number,process_phone_number
 | 
			
		||||
from cic_ussd.transactions import truncate
 | 
			
		||||
from cic_ussd.redis import InMemoryStore
 | 
			
		||||
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
 | 
			
		||||
@ -99,6 +99,7 @@ def test_initiate_account_creation_request(account_creation_action_data,
 | 
			
		||||
                                           load_config,
 | 
			
		||||
                                           load_ussd_menu,
 | 
			
		||||
                                           mocker,
 | 
			
		||||
                                           setup_chain_spec,
 | 
			
		||||
                                           set_locale_files,
 | 
			
		||||
                                           ussd_session_data):
 | 
			
		||||
    external_session_id = ussd_session_data.get('external_session_id')
 | 
			
		||||
@ -112,7 +113,8 @@ def test_initiate_account_creation_request(account_creation_action_data,
 | 
			
		||||
    mocked_cache_function = mocker.patch('cic_ussd.operations.cache_account_creation_task_id')
 | 
			
		||||
    mocked_cache_function(phone_number, task_id)
 | 
			
		||||
 | 
			
		||||
    response = initiate_account_creation_request(chain_str=load_config.get('CIC_CHAIN_SPEC'),
 | 
			
		||||
    chain_str = Chain.spec.__str__()
 | 
			
		||||
    response = initiate_account_creation_request(chain_str=chain_str,
 | 
			
		||||
                                                 external_session_id=external_session_id,
 | 
			
		||||
                                                 phone_number=ussd_session_data.get('msisdn'),
 | 
			
		||||
                                                 service_code=ussd_session_data.get('service_code'),
 | 
			
		||||
@ -204,11 +206,13 @@ def test_process_menu_interaction_requests(external_session_id,
 | 
			
		||||
                                           load_ussd_menu,
 | 
			
		||||
                                           load_data_into_state_machine,
 | 
			
		||||
                                           load_config,
 | 
			
		||||
                                           setup_chain_spec,
 | 
			
		||||
                                           celery_session_worker,
 | 
			
		||||
                                           create_activated_user,
 | 
			
		||||
                                           create_in_db_ussd_session):
 | 
			
		||||
    chain_str = Chain.spec.__str__()
 | 
			
		||||
    response = process_menu_interaction_requests(
 | 
			
		||||
        chain_str=load_config.get('CIC_CHAIN_SPEC'),
 | 
			
		||||
        chain_str=chain_str,
 | 
			
		||||
        external_session_id=external_session_id,
 | 
			
		||||
        phone_number=phone_number,
 | 
			
		||||
        queue='cic-ussd',
 | 
			
		||||
@ -12,7 +12,7 @@ from cic_ussd.processor import (custom_display_text,
 | 
			
		||||
def test_process_pin_authorization(create_activated_user,
 | 
			
		||||
                                   load_ussd_menu,
 | 
			
		||||
                                   set_locale_files):
 | 
			
		||||
    ussd_menu = UssdMenu.find_by_name(name='name_management_pin_authorization')
 | 
			
		||||
    ussd_menu = UssdMenu.find_by_name(name='standard_pin_authorization')
 | 
			
		||||
    response = process_pin_authorization(
 | 
			
		||||
        display_key=ussd_menu.get('display_key'),
 | 
			
		||||
        user=create_activated_user
 | 
			
		||||
@ -4,6 +4,7 @@
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.transactions import OutgoingTransactionProcessor, truncate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,8 +12,9 @@ def test_outgoing_transaction_processor(load_config,
 | 
			
		||||
                                        create_valid_tx_recipient,
 | 
			
		||||
                                        create_valid_tx_sender,
 | 
			
		||||
                                        mock_outgoing_transactions):
 | 
			
		||||
    chain_str = Chain.spec.__str__()
 | 
			
		||||
    outgoing_tx_processor = OutgoingTransactionProcessor(
 | 
			
		||||
        chain_str=load_config.get('CIC_CHAIN_SPEC'),
 | 
			
		||||
        chain_str=chain_str,
 | 
			
		||||
        from_address=create_valid_tx_sender.blockchain_address,
 | 
			
		||||
        to_address=create_valid_tx_recipient.blockchain_address
 | 
			
		||||
    )
 | 
			
		||||
@ -1,3 +1,7 @@
 | 
			
		||||
# third-party imports
 | 
			
		||||
from cic_types.pytest import *
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from tests.fixtures.config import *
 | 
			
		||||
from tests.fixtures.db import *
 | 
			
		||||
@ -8,41 +12,3 @@ from tests.fixtures.redis import *
 | 
			
		||||
from tests.fixtures.callback import *
 | 
			
		||||
from tests.fixtures.requests import *
 | 
			
		||||
from tests.fixtures.mocks import *
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								apps/cic-ussd/tests/fixtures/celery.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								apps/cic-ussd/tests/fixtures/celery.py
									
									
									
									
										vendored
									
									
								
							@ -12,7 +12,8 @@ def celery_includes():
 | 
			
		||||
    return [
 | 
			
		||||
        'cic_ussd.tasks.ussd',
 | 
			
		||||
        'cic_ussd.tasks.callback_handler',
 | 
			
		||||
        'cic_notify.tasks.sms'
 | 
			
		||||
        'cic_notify.tasks.sms',
 | 
			
		||||
        'cic_ussd.tasks.metadata'
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								apps/cic-ussd/tests/fixtures/config.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								apps/cic-ussd/tests/fixtures/config.py
									
									
									
									
										vendored
									
									
								
							@ -2,18 +2,24 @@
 | 
			
		||||
import i18n
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import tempfile
 | 
			
		||||
 | 
			
		||||
# third party imports
 | 
			
		||||
import pytest
 | 
			
		||||
from chainlib.chain import ChainSpec
 | 
			
		||||
from confini import Config
 | 
			
		||||
from sqlalchemy import create_engine
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.chain import Chain
 | 
			
		||||
from cic_ussd.db import dsn_from_config
 | 
			
		||||
from cic_ussd.encoder import PasswordEncoder
 | 
			
		||||
from cic_ussd.files.local_files import create_local_file_data_stores, json_file_parser
 | 
			
		||||
from cic_ussd.menu.ussd_menu import UssdMenu
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
 | 
			
		||||
from cic_ussd.metadata.signer import Signer
 | 
			
		||||
from cic_ussd.metadata.user import UserMetadata
 | 
			
		||||
from cic_ussd.state_machine import UssdStateMachine
 | 
			
		||||
from cic_ussd.encoder import PasswordEncoder
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logg = logging.getLogger()
 | 
			
		||||
@ -102,3 +108,29 @@ def uwsgi_env():
 | 
			
		||||
        'uwsgi.node': b'mango-habanero'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def setup_metadata_signer(load_config):
 | 
			
		||||
    temp_dir = tempfile.mkdtemp(dir='/tmp')
 | 
			
		||||
    logg.debug(f'Created temp dir: {temp_dir}')
 | 
			
		||||
    Signer.gpg_path = temp_dir
 | 
			
		||||
    Signer.key_file_path = load_config.get('KEYS_PRIVATE')
 | 
			
		||||
    Signer.gpg_passphrase = load_config.get('KEYS_PASSPHRASE')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def define_metadata_pointer_url(load_config, create_activated_user):
 | 
			
		||||
    identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
 | 
			
		||||
    UserMetadata.base_url = load_config.get('CIC_META_URL')
 | 
			
		||||
    user_metadata_client = UserMetadata(identifier=identifier)
 | 
			
		||||
    return user_metadata_client.url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def setup_chain_spec(load_config):
 | 
			
		||||
    chain_spec = ChainSpec(
 | 
			
		||||
        common_name=load_config.get('CIC_COMMON_NAME'),
 | 
			
		||||
        engine=load_config.get('CIC_ENGINE'),
 | 
			
		||||
        network_id=load_config.get('CIC_NETWORK_ID')
 | 
			
		||||
    )
 | 
			
		||||
    Chain.spec = chain_spec
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								apps/cic-ussd/tests/fixtures/mocks.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								apps/cic-ussd/tests/fixtures/mocks.py
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,6 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
from io import StringIO
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import pytest
 | 
			
		||||
@ -8,7 +10,49 @@ from cic_ussd.translation import translation_for
 | 
			
		||||
from cic_ussd.transactions import truncate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function', autouse=True)
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def mock_meta_post_response():
 | 
			
		||||
    return {
 | 
			
		||||
        'name': 'cic',
 | 
			
		||||
        'version': '1',
 | 
			
		||||
        'ext': {
 | 
			
		||||
            'network': {
 | 
			
		||||
                'name': 'pgp',
 | 
			
		||||
                'version': '2'
 | 
			
		||||
            },
 | 
			
		||||
            'engine': {
 | 
			
		||||
                'name': 'automerge',
 | 
			
		||||
                'version': '0.14.1'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        'payload': '["~#iL",[["~#iM",["ops",["^0",[["^1",["action","set","obj","00000000-0000-0000-0000-000000000000",'
 | 
			
		||||
                   '"key","id","value","7e2f58335a69ac82f9a965a8fc35403c8585ea601946d858ee97684a285bf857"]],["^1",'
 | 
			
		||||
                   '["action","set","obj","00000000-0000-0000-0000-000000000000","key","timestamp","value",'
 | 
			
		||||
                   '1613487781]], '
 | 
			
		||||
                   '["^1",["action","set","obj","00000000-0000-0000-0000-000000000000","key","data","value",'
 | 
			
		||||
                   '"{\\"foo\\": '
 | 
			
		||||
                   '\\"bar\\", \\"xyzzy\\": 42}"]]]],"actor","2b738a75-2aad-4ac8-ae8d-294a5ea4afad","seq",1,"deps",'
 | 
			
		||||
                   '["^1", '
 | 
			
		||||
                   '[]],"message","Initialization","undoable",false]],["^1",["ops",["^0",[["^1",["action","makeMap",'
 | 
			
		||||
                   '"obj","a921a5ae-0554-497a-ac2e-4e829d8a12b6"]],["^1",["action","set","obj",'
 | 
			
		||||
                   '"a921a5ae-0554-497a-ac2e-4e829d8a12b6","key","digest","value","W10="]],["^1",["action","link",'
 | 
			
		||||
                   '"obj", '
 | 
			
		||||
                   '"00000000-0000-0000-0000-000000000000","key","signature","value",'
 | 
			
		||||
                   '"a921a5ae-0554-497a-ac2e-4e829d8a12b6"]]]],"actor","2b738a75-2aad-4ac8-ae8d-294a5ea4afad","seq",2,'
 | 
			
		||||
                   '"deps",["^1",[]],"message","sign"]]]]',
 | 
			
		||||
        'digest': 'W10='
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def mock_meta_get_response():
 | 
			
		||||
    return {
 | 
			
		||||
        "foo": "bar",
 | 
			
		||||
        "xyzzy": 42
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def mock_notifier_api(mocker):
 | 
			
		||||
    messages = []
 | 
			
		||||
 | 
			
		||||
@ -43,7 +87,7 @@ def mock_outgoing_transactions(mocker):
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def mock_balance(mocker):
 | 
			
		||||
    mocked_operational_balance = mocker.patch('cic_ussd.accounts.BalanceManager.get_operational_balance')
 | 
			
		||||
    mocked_operational_balance = mocker.patch('cic_ussd.accounts.BalanceManager.get_balances')
 | 
			
		||||
 | 
			
		||||
    def _mock_operational_balance(balance: int):
 | 
			
		||||
        mocked_operational_balance.return_value = truncate(value=balance, decimals=2)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								apps/cic-ussd/tests/fixtures/user.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								apps/cic-ussd/tests/fixtures/user.py
									
									
									
									
										vendored
									
									
								
							@ -1,13 +1,17 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
from random import randint
 | 
			
		||||
import json
 | 
			
		||||
import uuid
 | 
			
		||||
from random import randint
 | 
			
		||||
 | 
			
		||||
# third party imports
 | 
			
		||||
import pytest
 | 
			
		||||
from cic_types.models.person import generate_metadata_pointer
 | 
			
		||||
from faker import Faker
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.db.models.user import AccountStatus, User
 | 
			
		||||
from cic_ussd.redis import cache_data
 | 
			
		||||
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fake = Faker()
 | 
			
		||||
@ -92,3 +96,25 @@ def create_locked_accounts(init_database, set_fernet_key):
 | 
			
		||||
        user.account_status = AccountStatus.LOCKED.value
 | 
			
		||||
        user.session.add(user)
 | 
			
		||||
        user.session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def complete_user_metadata(create_activated_user):
 | 
			
		||||
    return {
 | 
			
		||||
        "date_registered": create_activated_user.created,
 | 
			
		||||
        "family_name": "Snow",
 | 
			
		||||
        "given_name": "Name",
 | 
			
		||||
        "gender": 'Male',
 | 
			
		||||
        "location": "Kangemi",
 | 
			
		||||
        "products": "Mandazi"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope='function')
 | 
			
		||||
def cached_user_metadata(create_activated_user, init_redis_cache, person_metadata):
 | 
			
		||||
    user_metadata = json.dumps(person_metadata)
 | 
			
		||||
    key = generate_metadata_pointer(
 | 
			
		||||
        identifier=blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address),
 | 
			
		||||
        cic_type='cic.person'
 | 
			
		||||
    )
 | 
			
		||||
    cache_data(key=key, data=user_metadata)
 | 
			
		||||
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
# third-party-imports
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.redis import InMemoryStore
 | 
			
		||||
from cic_ussd.state_machine.logic.user import (
 | 
			
		||||
    change_preferred_language_to_en,
 | 
			
		||||
    change_preferred_language_to_sw,
 | 
			
		||||
    save_profile_attribute_to_session_data,
 | 
			
		||||
    update_account_status_to_active)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_change_preferred_language(create_pending_user, create_in_db_ussd_session):
 | 
			
		||||
    state_machine_data = ('', create_in_db_ussd_session, create_pending_user)
 | 
			
		||||
    assert create_pending_user.preferred_language is None
 | 
			
		||||
    change_preferred_language_to_en(state_machine_data)
 | 
			
		||||
    assert create_pending_user.preferred_language == 'en'
 | 
			
		||||
    change_preferred_language_to_sw(state_machine_data)
 | 
			
		||||
    assert create_pending_user.preferred_language == 'sw'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_update_account_status_to_active(create_pending_user, create_in_db_ussd_session):
 | 
			
		||||
    state_machine_data = ('', create_in_db_ussd_session, create_pending_user)
 | 
			
		||||
    update_account_status_to_active(state_machine_data)
 | 
			
		||||
    assert create_pending_user.get_account_status() == 'ACTIVE'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("current_state, expected_key, expected_result, user_input", [
 | 
			
		||||
    ("enter_first_name", "first_name", "John", "John"),
 | 
			
		||||
    ("enter_last_name", "last_name", "Doe", "Doe"),
 | 
			
		||||
    ("enter_location", "location", "Kangemi", "Kangemi"),
 | 
			
		||||
    ("enter_business_profile", "business_profile", "Mandazi", "Mandazi")
 | 
			
		||||
])
 | 
			
		||||
def test_save_profile_attribute_to_session_data(current_state,
 | 
			
		||||
                                                expected_key,
 | 
			
		||||
                                                expected_result,
 | 
			
		||||
                                                user_input,
 | 
			
		||||
                                                celery_session_worker,
 | 
			
		||||
                                                create_activated_user,
 | 
			
		||||
                                                create_in_db_ussd_session,
 | 
			
		||||
                                                create_in_redis_ussd_session):
 | 
			
		||||
    create_in_db_ussd_session.state = current_state
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = (user_input, serialized_in_db_ussd_session, create_activated_user)
 | 
			
		||||
    in_memory_ussd_session = InMemoryStore.cache.get('AT974186')
 | 
			
		||||
    in_memory_ussd_session = json.loads(in_memory_ussd_session)
 | 
			
		||||
    assert in_memory_ussd_session.get('session_data') == {}
 | 
			
		||||
    serialized_in_db_ussd_session['state'] = current_state
 | 
			
		||||
    save_profile_attribute_to_session_data(state_machine_data=state_machine_data)
 | 
			
		||||
 | 
			
		||||
    in_memory_ussd_session = InMemoryStore.cache.get('AT974186')
 | 
			
		||||
    in_memory_ussd_session = json.loads(in_memory_ussd_session)
 | 
			
		||||
 | 
			
		||||
    assert in_memory_ussd_session.get('session_data')[expected_key] == expected_result
 | 
			
		||||
@ -1,67 +0,0 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.state_machine.logic.validator import (is_valid_name,
 | 
			
		||||
                                                    has_complete_profile_data,
 | 
			
		||||
                                                    has_empty_username_data,
 | 
			
		||||
                                                    has_empty_gender_data,
 | 
			
		||||
                                                    has_empty_location_data,
 | 
			
		||||
                                                    has_empty_business_profile_data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize("user_input, expected_result", [
 | 
			
		||||
    ("Arya", True),
 | 
			
		||||
    ("1234", False)
 | 
			
		||||
])
 | 
			
		||||
def test_is_valid_name(create_in_db_ussd_session, create_pending_user, user_input, expected_result):
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = (user_input, serialized_in_db_ussd_session, create_pending_user)
 | 
			
		||||
    result = is_valid_name(state_machine_data=state_machine_data)
 | 
			
		||||
    assert result is expected_result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_has_complete_profile_data(caplog,
 | 
			
		||||
                                   create_in_db_ussd_session,
 | 
			
		||||
                                   create_activated_user):
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
 | 
			
		||||
    has_complete_profile_data(state_machine_data=state_machine_data)
 | 
			
		||||
    assert 'This section requires implementation of user metadata.' in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_has_empty_username_data(caplog,
 | 
			
		||||
                                 create_in_db_ussd_session,
 | 
			
		||||
                                 create_activated_user):
 | 
			
		||||
    state_machine_data = ('', create_in_db_ussd_session, create_activated_user)
 | 
			
		||||
    has_empty_username_data(state_machine_data=state_machine_data)
 | 
			
		||||
    assert 'This section requires implementation of user metadata.' in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_has_empty_gender_data(caplog,
 | 
			
		||||
                               create_in_db_ussd_session,
 | 
			
		||||
                               create_activated_user):
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
 | 
			
		||||
    has_empty_gender_data(state_machine_data=state_machine_data)
 | 
			
		||||
    assert 'This section requires implementation of user metadata.' in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_has_empty_location_data(caplog,
 | 
			
		||||
                                 create_in_db_ussd_session,
 | 
			
		||||
                                 create_activated_user):
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
 | 
			
		||||
    has_empty_location_data(state_machine_data=state_machine_data)
 | 
			
		||||
    assert 'This section requires implementation of user metadata.' in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_has_empty_business_profile_data(caplog,
 | 
			
		||||
                                         create_in_db_ussd_session,
 | 
			
		||||
                                         create_activated_user):
 | 
			
		||||
    serialized_in_db_ussd_session = create_in_db_ussd_session.to_json()
 | 
			
		||||
    state_machine_data = ('', serialized_in_db_ussd_session, create_activated_user)
 | 
			
		||||
    has_empty_business_profile_data(state_machine_data=state_machine_data)
 | 
			
		||||
    assert 'This section requires implementation of user metadata.' in caplog.text
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
# standard imports
 | 
			
		||||
 | 
			
		||||
# third-party imports
 | 
			
		||||
 | 
			
		||||
# local imports
 | 
			
		||||
from cic_ussd.accounts import BalanceManager
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_balance_manager(mocker, load_config, create_valid_tx_recipient):
 | 
			
		||||
 | 
			
		||||
    balance_manager = BalanceManager(
 | 
			
		||||
        address=create_valid_tx_recipient.blockchain_address,
 | 
			
		||||
        chain_str=load_config.get('CIC_CHAIN_SPEC'),
 | 
			
		||||
        token_symbol='SRF'
 | 
			
		||||
    )
 | 
			
		||||
    balance_manager.get_operational_balance = mocker.MagicMock()
 | 
			
		||||
    balance_manager.get_operational_balance()
 | 
			
		||||
 | 
			
		||||
    balance_manager.get_operational_balance.assert_called_once()
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
   {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "account_management",
 | 
			
		||||
    "dest": "profile_management",
 | 
			
		||||
    "dest": "metadata_management",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@ -14,21 +14,44 @@
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "account_management",
 | 
			
		||||
    "dest": "mini_statement_pin_authorization",
 | 
			
		||||
    "dest": "account_balances_pin_authorization",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "mini_statement_pin_authorization",
 | 
			
		||||
    "dest": "exit",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.sms.process_mini_statement_request"
 | 
			
		||||
    "source": "account_balances_pin_authorization",
 | 
			
		||||
    "dest": "account_balances",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "account_balances_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_blocked_pin"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "account_management",
 | 
			
		||||
    "dest": "account_statement_pin_authorization",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_four_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "account_statement_pin_authorization",
 | 
			
		||||
    "dest": "first_transaction_set",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "account_statement_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.is_blocked_pin"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "account_management",
 | 
			
		||||
    "dest": "enter_current_pin",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_four_selected"
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_five_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								apps/cic-ussd/transitions/account_statement_transitions.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								apps/cic-ussd/transitions/account_statement_transitions.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "first_transaction_set",
 | 
			
		||||
    "dest": "middle_transaction_set",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "first_transaction_set",
 | 
			
		||||
    "dest": "start",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "first_transaction_set",
 | 
			
		||||
    "dest": "exit_invalid_menu_option"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "middle_transaction_set",
 | 
			
		||||
    "dest": "last_transaction_set",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "middle_transaction_set",
 | 
			
		||||
    "dest": "first_transaction_set",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "middle_transaction_set",
 | 
			
		||||
    "dest": "start",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "middle_transaction_set",
 | 
			
		||||
    "dest": "exit_invalid_menu_option"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "last_transaction_set",
 | 
			
		||||
    "dest": "middle_transaction_set",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "last_transaction_set",
 | 
			
		||||
    "dest": "start",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_zero_zero_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "last_transaction_set",
 | 
			
		||||
    "dest": "exit_invalid_menu_option"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
@ -1,49 +0,0 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_business_profile",
 | 
			
		||||
    "dest": "enter_first_name",
 | 
			
		||||
    "conditions": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.is_valid_name",
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.has_empty_username_data"
 | 
			
		||||
    ],
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_business_profile",
 | 
			
		||||
    "dest": "enter_location",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_location_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_business_profile",
 | 
			
		||||
    "dest": "enter_gender",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_gender_profile_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_business_profile",
 | 
			
		||||
    "dest": "business_profile_management_pin_authorization"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "business_profile_management_pin_authorization",
 | 
			
		||||
    "dest": "exit",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.persist_profile_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "business_profile_management_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_business_profile",
 | 
			
		||||
    "dest": "exit_invalid_input"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
@ -1,50 +1,38 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_gender",
 | 
			
		||||
    "dest": "enter_first_name",
 | 
			
		||||
    "conditions": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.is_valid_name",
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.has_empty_username_data"
 | 
			
		||||
    ],
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_gender",
 | 
			
		||||
    "dest": "enter_location",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_location_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.validator.is_valid_gender_selection"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_gender",
 | 
			
		||||
    "dest": "enter_business_profile",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_business_profile_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
    "dest": "standard_pin_authorization",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
 | 
			
		||||
    "conditions": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.has_cached_user_metadata",
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.is_valid_gender_selection"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_gender",
 | 
			
		||||
    "dest": "gender_management_pin_authorization",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.validator.has_complete_profile_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "gender_management_pin_authorization",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.persist_profile_data"
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute",
 | 
			
		||||
    "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "gender_management_pin_authorization",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_gender",
 | 
			
		||||
    "dest": "exit_invalid_input"
 | 
			
		||||
    "dest": "exit_invalid_menu_option"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
@ -2,49 +2,28 @@
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_location",
 | 
			
		||||
    "dest": "enter_first_name",
 | 
			
		||||
    "conditions": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.is_valid_name",
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.has_empty_username_data"
 | 
			
		||||
    ],
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
    "dest": "enter_products",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_location",
 | 
			
		||||
    "dest": "enter_gender",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_gender_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
    "dest": "standard_pin_authorization",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_location",
 | 
			
		||||
    "dest": "enter_business_profile",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_business_profile_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_location",
 | 
			
		||||
    "dest": "location_management_pin_authorization",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.validator.has_complete_profile_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "location_management_pin_authorization",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.persist_profile_data"
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute",
 | 
			
		||||
    "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "location_management_pin_authorization",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_location",
 | 
			
		||||
    "dest": "exit_invalid_input"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
@ -1,54 +1,56 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_first_name",
 | 
			
		||||
    "dest": "enter_last_name",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.is_valid_name",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
    "source": "enter_given_name",
 | 
			
		||||
    "dest": "enter_family_name",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_last_name",
 | 
			
		||||
    "dest": "enter_gender",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_gender_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
    "source": "enter_given_name",
 | 
			
		||||
    "dest": "standard_pin_authorization",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_last_name",
 | 
			
		||||
    "dest": "enter_location",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_location_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_last_name",
 | 
			
		||||
    "dest": "enter_business_profile",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.validator.has_empty_business_profile_data",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_last_name",
 | 
			
		||||
    "dest": "name_management_pin_authorization",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.validator.has_complete_profile_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "name_management_pin_authorization",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.persist_profile_data"
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute",
 | 
			
		||||
    "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "name_management_pin_authorization",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_last_name",
 | 
			
		||||
    "dest": "exit_invalid_input"
 | 
			
		||||
    "source": "enter_family_name",
 | 
			
		||||
    "dest": "enter_gender",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_family_name",
 | 
			
		||||
    "dest": "standard_pin_authorization",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute",
 | 
			
		||||
    "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										32
									
								
								apps/cic-ussd/transitions/products_setting_transitions.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								apps/cic-ussd/transitions/products_setting_transitions.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_products",
 | 
			
		||||
    "dest": "standard_pin_authorization",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_products",
 | 
			
		||||
    "dest": "start",
 | 
			
		||||
    "after": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
 | 
			
		||||
      "cic_ussd.state_machine.logic.user.save_complete_user_metadata"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.user.edit_user_metadata_attribute"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
]
 | 
			
		||||
@ -40,10 +40,25 @@
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "initial_pin_confirmation",
 | 
			
		||||
    "dest": "start",
 | 
			
		||||
    "conditions": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.pin.pins_match",
 | 
			
		||||
      "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
 | 
			
		||||
    ],
 | 
			
		||||
    "after": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.pin.complete_pin_change",
 | 
			
		||||
      "cic_ussd.state_machine.logic.user.update_account_status_to_active",
 | 
			
		||||
      "cic_ussd.state_machine.logic.sms.send_terms_to_user_if_required"
 | 
			
		||||
      "cic_ussd.state_machine.logic.user.get_user_metadata",
 | 
			
		||||
      "cic_ussd.state_machine.logic.user.update_account_status_to_active"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "initial_pin_confirmation",
 | 
			
		||||
    "unless": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.pins_match",
 | 
			
		||||
    "dest": "enter_given_name",
 | 
			
		||||
    "after": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.pin.complete_pin_change",
 | 
			
		||||
      "cic_ussd.state_machine.logic.user.update_account_status_to_active"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,10 @@
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "enter_transaction_recipient",
 | 
			
		||||
    "dest": "enter_transaction_amount",
 | 
			
		||||
    "after": "cic_ussd.state_machine.logic.transaction.save_recipient_phone_to_session_data",
 | 
			
		||||
    "after": [
 | 
			
		||||
      "cic_ussd.state_machine.logic.transaction.save_recipient_phone_to_session_data",
 | 
			
		||||
      "cic_ussd.state_machine.logic.transaction.retrieve_recipient_metadata"
 | 
			
		||||
    ],
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.transaction.is_valid_recipient"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
 | 
			
		||||
@ -1,49 +1,49 @@
 | 
			
		||||
[
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "profile_management",
 | 
			
		||||
    "dest": "enter_first_name",
 | 
			
		||||
    "source": "metadata_management",
 | 
			
		||||
    "dest": "enter_given_name",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "profile_management",
 | 
			
		||||
    "dest": "enter_gender",
 | 
			
		||||
    "source": "metadata_management",
 | 
			
		||||
    "dest": "enter_age",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "profile_management",
 | 
			
		||||
    "source": "metadata_management",
 | 
			
		||||
    "dest": "enter_location",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "profile_management",
 | 
			
		||||
    "dest": "edit_business_profile",
 | 
			
		||||
    "source": "metadata_management",
 | 
			
		||||
    "dest": "enter_products",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_four_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "profile_management",
 | 
			
		||||
    "dest": "view_profile_pin_authorization",
 | 
			
		||||
    "source": "metadata_management",
 | 
			
		||||
    "dest": "standard_pin_authorization",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.menu.menu_five_selected"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "view_profile_pin_authorization",
 | 
			
		||||
    "dest": "display_user_profile_data",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "display_user_metadata",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "view_profile_pin_authorization",
 | 
			
		||||
    "source": "standard_pin_authorization",
 | 
			
		||||
    "dest": "exit_pin_blocked",
 | 
			
		||||
    "conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "trigger": "scan_data",
 | 
			
		||||
    "source": "profile_management",
 | 
			
		||||
    "source": "metadata_management",
 | 
			
		||||
    "dest": "exit_invalid_menu_option"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
en:
 | 
			
		||||
  account_successfully_created: |-
 | 
			
		||||
    Hello %{first_name} you have been registered on Sarafu Network! Your balance is %{balance} %{token_symbol}. To use dial *483*46#. For help 0757628885.
 | 
			
		||||
    Hello, you have been registered on Sarafu Network! Your balance is %{balance} %{token_symbol}. To use dial *483*46#. For help 0757628885.
 | 
			
		||||
  received_tokens: |-
 | 
			
		||||
    Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp}. New balance is %{balance} %{token_symbol}.
 | 
			
		||||
  terms: |-
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
sw:
 | 
			
		||||
  account_successfully_created: |-
 | 
			
		||||
    Habari %{first_name}, umesajiliwa kwa huduma ya sarafu! Salio lako ni %{token_symbol} %{balance}. Kutumia bonyeza *483*46#. Kwa Usaidizi 0757628885.
 | 
			
		||||
    Habari, umesajiliwa kwa huduma ya sarafu! Salio lako ni %{token_symbol} %{balance}. Kutumia bonyeza *483*46#. Kwa Usaidizi 0757628885.
 | 
			
		||||
  received_tokens: |-
 | 
			
		||||
    Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp}. Salio la %{token_symbol} ni %{balance}.
 | 
			
		||||
  terms: |-
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,23 @@ en:
 | 
			
		||||
    initial_pin_confirmation: |-
 | 
			
		||||
      CON Enter your PIN again
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_given_name: |-
 | 
			
		||||
      CON Enter first name
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_family_name: |-
 | 
			
		||||
      CON Enter last name
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_gender: |-
 | 
			
		||||
      CON Enter gender
 | 
			
		||||
      1. Male
 | 
			
		||||
      2. Female
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_location: |-
 | 
			
		||||
      CON Enter location
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_products: |-
 | 
			
		||||
      CON Please enter a product or service you offer
 | 
			
		||||
      0. Back
 | 
			
		||||
    start: |-
 | 
			
		||||
      CON Balance %{account_balance} %{account_token_name}
 | 
			
		||||
      1. Send
 | 
			
		||||
@ -27,40 +44,23 @@ en:
 | 
			
		||||
      1. My profile
 | 
			
		||||
      2. Change language
 | 
			
		||||
      3. Check balance
 | 
			
		||||
      4. Change PIN
 | 
			
		||||
      4. Check statement
 | 
			
		||||
      5. Change PIN
 | 
			
		||||
      0. Back
 | 
			
		||||
    profile_management: |-
 | 
			
		||||
    metadata_management: |-
 | 
			
		||||
      CON My profile
 | 
			
		||||
      1. Edit name
 | 
			
		||||
      2. Edit gender
 | 
			
		||||
      3. Edit location
 | 
			
		||||
      4. Edit business
 | 
			
		||||
      4. Edit products
 | 
			
		||||
      5. View my profile
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_first_name: |-
 | 
			
		||||
      CON Enter first name
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_last_name: |-
 | 
			
		||||
      CON Enter last name
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_gender: |-
 | 
			
		||||
      CON Enter gender
 | 
			
		||||
      1. Male
 | 
			
		||||
      2. Female
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_location: |-
 | 
			
		||||
      CON Enter location
 | 
			
		||||
      0. Back
 | 
			
		||||
    enter_business_profile: |-
 | 
			
		||||
      CON Please enter a product or service you offer
 | 
			
		||||
      0. Back
 | 
			
		||||
    display_user_profile_data: |-
 | 
			
		||||
      END Your details are:
 | 
			
		||||
        Name: %{full_name}
 | 
			
		||||
        Gender: %{gender}
 | 
			
		||||
        Location: %{location}
 | 
			
		||||
        You sell: %{business_profile}
 | 
			
		||||
      If any details are missing, please use my profile to add your details.
 | 
			
		||||
        You sell: %{products}
 | 
			
		||||
      0. Back
 | 
			
		||||
    select_preferred_language: |-
 | 
			
		||||
      CON Choose language
 | 
			
		||||
@ -85,48 +85,46 @@ en:
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
 | 
			
		||||
        0. Back
 | 
			
		||||
    name_management_pin_authorization:
 | 
			
		||||
    standard_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Please enter your PIN.
 | 
			
		||||
        0. Back
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
 | 
			
		||||
        0. Back
 | 
			
		||||
    gender_management_pin_authorization:
 | 
			
		||||
    account_balances_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Please enter your PIN.
 | 
			
		||||
        CON Please enter your PIN to view balances.
 | 
			
		||||
        0. Back
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
 | 
			
		||||
        0. Back
 | 
			
		||||
    location_management_pin_authorization:
 | 
			
		||||
    account_statement_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Please enter your PIN.
 | 
			
		||||
        0. Back
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
 | 
			
		||||
        0. Back
 | 
			
		||||
    business_profile_management_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Please enter your PIN.
 | 
			
		||||
        0. Back
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
 | 
			
		||||
        0. Back
 | 
			
		||||
    view_profile_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Please enter your PIN.
 | 
			
		||||
        0. Back
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
 | 
			
		||||
        0. Back
 | 
			
		||||
    mini_statement_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Please enter your PIN.
 | 
			
		||||
        CON Please enter your PIN to view statement.
 | 
			
		||||
        0. Back
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
 | 
			
		||||
        0. Back
 | 
			
		||||
    account_balances: |-
 | 
			
		||||
      CON Your balances are as follows:
 | 
			
		||||
        balance: %{operational_balance} %{token_symbol}
 | 
			
		||||
        taxes: %{tax} %{token_symbol}
 | 
			
		||||
        bonsuses: %{bonus} %{token_symbol}
 | 
			
		||||
      0. Back
 | 
			
		||||
    first_transaction_set: |-
 | 
			
		||||
      CON %{first_transaction_set}
 | 
			
		||||
      1. Next
 | 
			
		||||
      00. Exit
 | 
			
		||||
    middle_transaction_set: |-
 | 
			
		||||
      CON %{middle_transaction_set}
 | 
			
		||||
      1. Next
 | 
			
		||||
      2. Previous
 | 
			
		||||
      00. Exit
 | 
			
		||||
    last_transaction_set: |-
 | 
			
		||||
      CON %{last_transaction_set}
 | 
			
		||||
      2. Previous
 | 
			
		||||
      00. Exit
 | 
			
		||||
    exit: |-
 | 
			
		||||
      END Thank you for using the service.
 | 
			
		||||
    exit_invalid_request: |-
 | 
			
		||||
@ -168,5 +166,5 @@ en:
 | 
			
		||||
      CON Your request has been sent. You will receive an SMS shortly.
 | 
			
		||||
      00. Back
 | 
			
		||||
      99. Exit
 | 
			
		||||
    account_creation_prompt: >
 | 
			
		||||
    account_creation_prompt: |-
 | 
			
		||||
      Your account is being created. You will receive an SMS when your account is ready.
 | 
			
		||||
@ -11,6 +11,23 @@ sw:
 | 
			
		||||
    initial_pin_confirmation: |-
 | 
			
		||||
      CON Weka PIN yako tena
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_given_name: |-
 | 
			
		||||
      CON Weka jina lako la kwanza
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_family_name: |-
 | 
			
		||||
      CON Weka jina lako la mwisho
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_gender: |-
 | 
			
		||||
      CON Weka jinsia yako
 | 
			
		||||
      1. Mwanaume
 | 
			
		||||
      2. Mwanamke
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_location: |-
 | 
			
		||||
      CON Weka eneo lako
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_products: |-
 | 
			
		||||
      CON Tafadhali weka bidhaa ama huduma unauza
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    start: |-
 | 
			
		||||
      CON Salio %{account_balance} %{account_token_name}
 | 
			
		||||
      1. Tuma
 | 
			
		||||
@ -27,39 +44,23 @@ sw:
 | 
			
		||||
      1. Wasifu wangu
 | 
			
		||||
      2. Chagua lugha utakayotumia
 | 
			
		||||
      3. Angalia salio
 | 
			
		||||
      4. Badilisha nambari ya siri
 | 
			
		||||
      4. Angalia taarifa ya matumizi
 | 
			
		||||
      5. Badilisha nambari ya siri
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    profile_management: |-
 | 
			
		||||
    metadata_management: |-
 | 
			
		||||
      CON Wasifu wangu
 | 
			
		||||
      1. Weka jina
 | 
			
		||||
      2. Weka jinsia
 | 
			
		||||
      3. Weka eneo
 | 
			
		||||
      4. Weka biashara
 | 
			
		||||
      4. Weka bidhaa
 | 
			
		||||
      5. Angalia wasifu wako
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_first_name: |-
 | 
			
		||||
      CON Weka jina lako la kwanza
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_last_name: |-
 | 
			
		||||
      CON Weka jina lako la mwisho
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_gender: |-
 | 
			
		||||
      CON Weka jinsia yako
 | 
			
		||||
      1. Mwanaume
 | 
			
		||||
      2. Mwanamke
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_location: |-
 | 
			
		||||
      CON Weka eneo lako
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    enter_business_profile: |-
 | 
			
		||||
      CON Tafadhali weka bidhaa ama huduma unauza
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    display_user_profile_data: |-
 | 
			
		||||
    display_user_metadata: |-
 | 
			
		||||
      END Wasifu wako una maelezo yafuatayo:
 | 
			
		||||
        Jina: %{full_name}
 | 
			
		||||
        Jinsia: %{gender}
 | 
			
		||||
        Eneo: %{location}
 | 
			
		||||
        Unauza: %{user_bio}
 | 
			
		||||
      Iwapo hakuna, enda kwa wasifu wako uweke maelezo zaidi.
 | 
			
		||||
      0. Nyuma
 | 
			
		||||
    select_preferred_language: |-
 | 
			
		||||
      CON Chagua lugha
 | 
			
		||||
@ -84,48 +85,46 @@ sw:
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Weka nambari ya siri. Una majaribio %{remaining_attempts} yaliyobaki.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
    name_management_pin_authorization:
 | 
			
		||||
    standard_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
    gender_management_pin_authorization:
 | 
			
		||||
    account_balances_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako.
 | 
			
		||||
        CON Tafadhali weka PIN yako kuona salio.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
    location_management_pin_authorization:
 | 
			
		||||
    account_statement_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
    business_profile_management_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
    view_profile_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
    mini_statement_pin_authorization:
 | 
			
		||||
      first: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako.
 | 
			
		||||
        CON Tafadhali weka PIN yako kuona taarifa ya matumizi.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
      retry: |-
 | 
			
		||||
        CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
 | 
			
		||||
        0. Nyuma
 | 
			
		||||
    account_balances: |-
 | 
			
		||||
      CON Salio zako ni zifuatazo:
 | 
			
		||||
        salio: %{operational_balance}
 | 
			
		||||
        ushuru: %{tax}
 | 
			
		||||
        tuzo: %{bonus}
 | 
			
		||||
      0. Back
 | 
			
		||||
    first_transaction_set: |-
 | 
			
		||||
      CON %{first_transaction_set}
 | 
			
		||||
      1. Mbele
 | 
			
		||||
      00. Ondoka
 | 
			
		||||
    middle_transaction_set: |-
 | 
			
		||||
      CON %{middle_transaction_set}
 | 
			
		||||
      1. Mbele
 | 
			
		||||
      2. Nyuma
 | 
			
		||||
      00. Ondoka
 | 
			
		||||
    last_transaction_set: |-
 | 
			
		||||
      CON %{last_transaction_set}
 | 
			
		||||
      2. Nyuma
 | 
			
		||||
      00. Ondoka
 | 
			
		||||
    exit: |-
 | 
			
		||||
      END Asante kwa kutumia huduma.
 | 
			
		||||
    exit_invalid_request: |-
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user