Minor refactors:
- Renames s_assemble to s_brief - Link s_local to s_brief
This commit is contained in:
parent
21c9d95c4b
commit
1e7fff0133
@ -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"
|
||||
]
|
@ -4,3 +4,4 @@ pytest-celery==0.0.0a1
|
||||
pytest-cov==2.10.1
|
||||
pytest-mock==3.3.1
|
||||
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.
|
||||
CON Please enter your PIN to view statement.
|
||||
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.
|
||||
0. Back
|
||||
retry: |-
|
||||
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
||||
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