Merge branch 'lash/fix-tx-list' into 'master'
Make tx listing tasks work properly See merge request grassrootseconomics/cic-internal-integration!45
This commit is contained in:
commit
aa99b16ad2
@ -149,6 +149,9 @@ def tx_collate(tx_batches, chain_str, offset, limit, newest_first=True):
|
|||||||
txs_by_block = {}
|
txs_by_block = {}
|
||||||
chain_spec = ChainSpec.from_chain_str(chain_str)
|
chain_spec = ChainSpec.from_chain_str(chain_str)
|
||||||
|
|
||||||
|
if isinstance(tx_batches, dict):
|
||||||
|
tx_batches = [tx_batches]
|
||||||
|
|
||||||
for b in tx_batches:
|
for b in tx_batches:
|
||||||
for v in b.values():
|
for v in b.values():
|
||||||
tx = None
|
tx = None
|
||||||
|
@ -10,7 +10,7 @@ version = (
|
|||||||
0,
|
0,
|
||||||
10,
|
10,
|
||||||
0,
|
0,
|
||||||
'alpha.37',
|
'alpha.38',
|
||||||
)
|
)
|
||||||
|
|
||||||
version_object = semver.VersionInfo(
|
version_object = semver.VersionInfo(
|
||||||
|
@ -12,5 +12,3 @@ MENU_FILE=/usr/local/lib/python3.8/site-packages/cic_ussd/db/ussd_menu.json
|
|||||||
[statemachine]
|
[statemachine]
|
||||||
STATES=/usr/src/cic-ussd/states/
|
STATES=/usr/src/cic-ussd/states/
|
||||||
TRANSITIONS=/usr/src/cic-ussd/transitions/
|
TRANSITIONS=/usr/src/cic-ussd/transitions/
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
[cic]
|
[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#
|
SERVICE_CODE=*483*46#
|
||||||
|
|
||||||
[ussd]
|
[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]
|
[statemachine]
|
||||||
STATES=states/
|
STATES=/usr/src/cic-ussd/states/
|
||||||
TRANSITIONS=transitions/
|
TRANSITIONS=/usr/src/cic-ussd/transitions/
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
[cic]
|
[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": {
|
"ussd_menu": {
|
||||||
"1": {
|
"1": {
|
||||||
"description": "The self signup process has been initiated and the account is being created",
|
"description": "Entry point for users to select their preferred language.",
|
||||||
"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",
|
|
||||||
"display_key": "ussd.kenya.initial_language_selection",
|
"display_key": "ussd.kenya.initial_language_selection",
|
||||||
"name": "initial_language_selection",
|
"name": "initial_language_selection",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"3": {
|
"2": {
|
||||||
"description": "PIN setup entry menu",
|
"description": "Entry point for users to enter a pin to secure their account.",
|
||||||
"display_key": "ussd.kenya.initial_pin_entry",
|
"display_key": "ussd.kenya.initial_pin_entry",
|
||||||
"name": "initial_pin_entry",
|
"name": "initial_pin_entry",
|
||||||
"parent": "initial_language_selection"
|
"parent": null
|
||||||
},
|
},
|
||||||
"4": {
|
"3": {
|
||||||
"description": "Confirm new PIN menu",
|
"description": "Pin confirmation entry menu.",
|
||||||
"display_key": "ussd.kenya.initial_pin_confirmation",
|
"display_key": "ussd.kenya.initial_pin_confirmation",
|
||||||
"name": "initial_pin_confirmation",
|
"name": "initial_pin_confirmation",
|
||||||
"parent": "initial_pin_entry"
|
"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": {
|
"5": {
|
||||||
"description": "Start menu. This is the entry point for activated users",
|
"description": "Entry point for activated users.",
|
||||||
"display_key": "ussd.kenya.start",
|
"display_key": "ussd.kenya.start",
|
||||||
"name": "start",
|
"name": "start",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"6": {
|
"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",
|
"display_key": "ussd.kenya.enter_transaction_recipient",
|
||||||
"name": "enter_transaction_recipient",
|
"name": "enter_transaction_recipient",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"7": {
|
"14": {
|
||||||
"description": "Send Token amount prompt menu",
|
"description": "Send Token amount prompt menu.",
|
||||||
"display_key": "ussd.kenya.enter_transaction_amount",
|
"display_key": "ussd.kenya.enter_transaction_amount",
|
||||||
"name": "enter_transaction_amount",
|
"name": "enter_transaction_amount",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"8": {
|
"15": {
|
||||||
"description": "PIN entry for authorization to send token",
|
"description": "Pin entry for authorization to send token.",
|
||||||
"display_key": "ussd.kenya.transaction_pin_authorization",
|
"display_key": "ussd.kenya.transaction_pin_authorization",
|
||||||
"name": "transaction_pin_authorization",
|
"name": "transaction_pin_authorization",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"9": {
|
"16": {
|
||||||
"description": "Terminal of a menu flow where an SMS is expected after.",
|
"description": "Manage account menu.",
|
||||||
"display_key": "ussd.kenya.complete",
|
"display_key": "ussd.kenya.account_management",
|
||||||
"name": "complete",
|
"name": "account_management",
|
||||||
"parent": null
|
|
||||||
},
|
|
||||||
"10": {
|
|
||||||
"description": "Help menu",
|
|
||||||
"display_key": "ussd.kenya.help",
|
|
||||||
"name": "help",
|
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"11": {
|
"17": {
|
||||||
"description": "Manage account menu",
|
"description": "Manage metadata menu.",
|
||||||
"display_key": "ussd.kenya.profile_management",
|
"display_key": "ussd.kenya.metadata_management",
|
||||||
"name": "profile_management",
|
"name": "metadata_management",
|
||||||
"parent": "start"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"12": {
|
"18": {
|
||||||
"description": "Manage business directory info",
|
"description": "Manage user's preferred language menu.",
|
||||||
"display_key": "ussd.kenya.select_preferred_language",
|
"display_key": "ussd.kenya.select_preferred_language",
|
||||||
"name": "select_preferred_language",
|
"name": "select_preferred_language",
|
||||||
"parent": "account_management"
|
"parent": "account_management"
|
||||||
},
|
},
|
||||||
"13": {
|
"19": {
|
||||||
"description": "About business directory info",
|
"description": "Retrieve mini-statement menu.",
|
||||||
"display_key": "ussd.kenya.mini_statement_pin_authorization",
|
"display_key": "ussd.kenya.mini_statement_pin_authorization",
|
||||||
"name": "mini_statement_pin_authorization",
|
"name": "mini_statement_pin_authorization",
|
||||||
"parent": "account_management"
|
"parent": "account_management"
|
||||||
},
|
},
|
||||||
"14": {
|
"20": {
|
||||||
"description": "Change business directory info",
|
"description": "Manage user's pin menu.",
|
||||||
"display_key": "ussd.kenya.enter_current_pin",
|
"display_key": "ussd.kenya.enter_current_pin",
|
||||||
"name": "enter_current_pin",
|
"name": "enter_current_pin",
|
||||||
"parent": "account_management"
|
"parent": "account_management"
|
||||||
},
|
},
|
||||||
"15": {
|
"21": {
|
||||||
"description": "New PIN entry menu",
|
"description": "New pin entry menu.",
|
||||||
"display_key": "ussd.kenya.enter_new_pin",
|
"display_key": "ussd.kenya.enter_new_pin",
|
||||||
"name": "enter_new_pin",
|
"name": "enter_new_pin",
|
||||||
"parent": "account_management"
|
"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": {
|
"22": {
|
||||||
"description": "Pin authorization to change name",
|
"description": "Pin entry menu.",
|
||||||
"display_key": "ussd.kenya.name_management_pin_authorization",
|
"display_key": "ussd.kenya.standard_pin_authorization",
|
||||||
"name": "name_management_pin_authorization",
|
"name": "standard_pin_authorization",
|
||||||
"parent": "profile_management"
|
"parent": "start"
|
||||||
},
|
},
|
||||||
"23": {
|
"23": {
|
||||||
"description": "Pin authorization to change gender",
|
"description": "Exit menu.",
|
||||||
"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",
|
|
||||||
"display_key": "ussd.kenya.exit",
|
"display_key": "ussd.kenya.exit",
|
||||||
"name": "exit",
|
"name": "exit",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"28": {
|
"24": {
|
||||||
"description": "Invalid menu option",
|
"description": "Invalid menu option.",
|
||||||
"display_key": "ussd.kenya.exit_invalid_menu_option",
|
"display_key": "ussd.kenya.exit_invalid_menu_option",
|
||||||
"name": "exit_invalid_menu_option",
|
"name": "exit_invalid_menu_option",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"29": {
|
"25": {
|
||||||
"description": "PIN policy violation",
|
"description": "Pin policy violation.",
|
||||||
"display_key": "ussd.kenya.exit_invalid_pin",
|
"display_key": "ussd.kenya.exit_invalid_pin",
|
||||||
"name": "exit_invalid_pin",
|
"name": "exit_invalid_pin",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"30": {
|
"26": {
|
||||||
"description": "PIN mismatch. New PIN and the new PIN confirmation do not match",
|
"description": "Pin mismatch. New pin and the new pin confirmation do not match",
|
||||||
"display_key": "ussd.kenya.exit_pin_mismatch",
|
"display_key": "ussd.kenya.exit_pin_mismatch",
|
||||||
"name": "exit_pin_mismatch",
|
"name": "exit_pin_mismatch",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"31": {
|
"27": {
|
||||||
"description": "Ussd PIN Blocked Menu",
|
"description": "Ussd pin blocked Menu",
|
||||||
"display_key": "ussd.kenya.exit_pin_blocked",
|
"display_key": "ussd.kenya.exit_pin_blocked",
|
||||||
"name": "exit_pin_blocked",
|
"name": "exit_pin_blocked",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"32": {
|
"28": {
|
||||||
"description": "Key params missing in request",
|
"description": "Key params missing in request.",
|
||||||
"display_key": "ussd.kenya.exit_invalid_request",
|
"display_key": "ussd.kenya.exit_invalid_request",
|
||||||
"name": "exit_invalid_request",
|
"name": "exit_invalid_request",
|
||||||
"parent": null
|
"parent": null
|
||||||
},
|
},
|
||||||
"33": {
|
"29": {
|
||||||
"description": "The user did not select a choice",
|
"description": "The user did not select a choice.",
|
||||||
"display_key": "ussd.kenya.exit_invalid_input",
|
"display_key": "ussd.kenya.exit_invalid_input",
|
||||||
"name": "exit_invalid_input",
|
"name": "exit_invalid_input",
|
||||||
"parent": null
|
"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.",
|
"description": "Exit following a successful transaction.",
|
||||||
"display_key": "ussd.kenya.exit_successful_transaction",
|
"display_key": "ussd.kenya.exit_successful_transaction",
|
||||||
"name": "exit_successful_transaction",
|
"name": "exit_successful_transaction",
|
||||||
"parent": null
|
"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": {
|
"35": {
|
||||||
"description": "Manage account menu",
|
"description": "Menu to display account balances.",
|
||||||
"display_key": "ussd.kenya.account_management",
|
"display_key": "ussd.kenya.account_balances",
|
||||||
"name": "account_management",
|
"name": "account_balances",
|
||||||
"parent": "start"
|
"parent": "account_management"
|
||||||
},
|
},
|
||||||
"36": {
|
"36": {
|
||||||
"description": "Exit following insufficient balance to perform a transaction.",
|
"description": "Menu to display first set of transactions in statement.",
|
||||||
"display_key": "ussd.kenya.exit_insufficient_balance",
|
"display_key": "ussd.kenya.first_transaction_set",
|
||||||
"name": "exit_insufficient_balance",
|
"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
|
"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"""
|
"""Raised when action data matching a specific task uuid is not found in the redis cache"""
|
||||||
pass
|
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
|
# third party imports
|
||||||
import celery
|
import celery
|
||||||
import i18n
|
import i18n
|
||||||
import phonenumbers
|
|
||||||
from cic_eth.api.api_task import Api
|
from cic_eth.api.api_task import Api
|
||||||
from tinydb.table import Document
|
from tinydb.table import Document
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@ -239,7 +238,7 @@ def persist_session_to_db_task(external_session_id: str, queue: str):
|
|||||||
:type queue: str
|
:type queue: str
|
||||||
"""
|
"""
|
||||||
s_persist_session_to_db = celery.signature(
|
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]
|
[external_session_id]
|
||||||
)
|
)
|
||||||
s_persist_session_to_db.apply_async(queue=queue)
|
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)
|
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
|
# standard imports
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# third party imports
|
# third party imports
|
||||||
|
import celery
|
||||||
|
from cic_types.models.person import Person
|
||||||
from tinydb.table import Document
|
from tinydb.table import Document
|
||||||
|
|
||||||
# local imports
|
# 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.user import AccountStatus, User
|
||||||
from cic_ussd.db.models.ussd_session import UssdSession
|
from cic_ussd.db.models.ussd_session import UssdSession
|
||||||
from cic_ussd.menu.ussd_menu import UssdMenu
|
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.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
|
from cic_ussd.translation import translation_for
|
||||||
|
|
||||||
logg = logging.getLogger(__name__)
|
logg = logging.getLogger(__name__)
|
||||||
@ -57,17 +66,17 @@ def process_exit_insufficient_balance(display_key: str, user: User, ussd_session
|
|||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
# get account balance
|
# get account balance
|
||||||
balance_manager = BalanceManager(address=user.blockchain_address,
|
operational_balance = get_cached_operational_balance(blockchain_address=user.blockchain_address)
|
||||||
chain_str=UssdStateMachine.chain_str,
|
|
||||||
token_symbol='SRF')
|
|
||||||
balance = balance_manager.get_operational_balance()
|
|
||||||
|
|
||||||
# compile response data
|
# compile response data
|
||||||
user_input = ussd_session.get('user_input').split('*')[-1]
|
user_input = ussd_session.get('user_input').split('*')[-1]
|
||||||
transaction_amount = to_wei(value=int(user_input))
|
transaction_amount = to_wei(value=int(user_input))
|
||||||
token_symbol = 'SRF'
|
token_symbol = 'SRF'
|
||||||
|
|
||||||
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
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(
|
return translation_for(
|
||||||
key=display_key,
|
key=display_key,
|
||||||
@ -75,7 +84,7 @@ def process_exit_insufficient_balance(display_key: str, user: User, ussd_session
|
|||||||
amount=from_wei(transaction_amount),
|
amount=from_wei(transaction_amount),
|
||||||
token_symbol=token_symbol,
|
token_symbol=token_symbol,
|
||||||
recipient_information=tx_recipient_information,
|
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
|
# compile response data
|
||||||
recipient_phone_number = ussd_session.get('session_data').get('recipient_phone_number')
|
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_sender_information = user.phone_number
|
tx_recipient_information = define_account_tx_metadata(user=recipient)
|
||||||
logg.debug('Requires integration with cic-meta to get user name.')
|
tx_sender_information = define_account_tx_metadata(user=user)
|
||||||
|
|
||||||
token_symbol = 'SRF'
|
token_symbol = 'SRF'
|
||||||
user_input = ussd_session.get('user_input').split('*')[-1]
|
user_input = ussd_session.get('user_input').split('*')[-1]
|
||||||
transaction_amount = to_wei(value=int(user_input))
|
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):
|
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
|
"""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
|
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
|
:return: Corresponding translation text response
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
balance_manager = BalanceManager(address=user.blockchain_address,
|
chain_str = Chain.spec.__str__()
|
||||||
chain_str=UssdStateMachine.chain_str,
|
blockchain_address = user.blockchain_address
|
||||||
|
balance_manager = BalanceManager(address=blockchain_address,
|
||||||
|
chain_str=chain_str,
|
||||||
token_symbol='SRF')
|
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'
|
token_symbol = 'SRF'
|
||||||
logg.debug("Requires integration to determine user's balance and token.")
|
|
||||||
return translation_for(
|
return translation_for(
|
||||||
key=display_key,
|
key=display_key,
|
||||||
preferred_language=user.preferred_language,
|
preferred_language=user.preferred_language,
|
||||||
account_balance=balance,
|
account_balance=operational_balance,
|
||||||
account_token_name=token_symbol
|
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)
|
return process_exit_successful_transaction(display_key=display_key, user=user, ussd_session=ussd_session)
|
||||||
elif menu_name == 'start':
|
elif menu_name == 'start':
|
||||||
return process_start_menu(display_key=display_key, user=user)
|
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:
|
else:
|
||||||
return translation_for(key=display_key, preferred_language=user.preferred_language)
|
return translation_for(key=display_key, preferred_language=user.preferred_language)
|
||||||
|
@ -1,6 +1,52 @@
|
|||||||
|
# standard imports
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
|
||||||
# third-party imports
|
# third-party imports
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
|
logg = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
class InMemoryStore:
|
class InMemoryStore:
|
||||||
cache: Redis = None
|
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
|
# third-party imports
|
||||||
from confini import Config
|
from confini import Config
|
||||||
|
from chainlib.chain import ChainSpec
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
|
from cic_ussd.chain import Chain
|
||||||
from cic_ussd.db import dsn_from_config
|
from cic_ussd.db import dsn_from_config
|
||||||
from cic_ussd.db.models.base import SessionBase
|
from cic_ussd.db.models.base import SessionBase
|
||||||
from cic_ussd.encoder import PasswordEncoder
|
from cic_ussd.encoder import PasswordEncoder
|
||||||
from cic_ussd.files.local_files import create_local_file_data_stores, json_file_parser
|
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.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,
|
from cic_ussd.operations import (define_response_with_content,
|
||||||
process_menu_interaction_requests,
|
process_menu_interaction_requests,
|
||||||
define_multilingual_responses)
|
define_multilingual_responses)
|
||||||
@ -59,6 +63,7 @@ config.censor('PASSWORD', 'DATABASE')
|
|||||||
# define log levels
|
# define log levels
|
||||||
if args.vv:
|
if args.vv:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
|
||||||
elif args.v:
|
elif args.v:
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
@ -92,6 +97,14 @@ InMemoryStore.cache = redis.StrictRedis(host=config.get('REDIS_HOSTNAME'),
|
|||||||
decode_responses=True)
|
decode_responses=True)
|
||||||
InMemoryUssdSession.redis_cache = InMemoryStore.cache
|
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
|
# initialize celery app
|
||||||
celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
|
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'))
|
states = json_file_parser(filepath=config.get('STATEMACHINE_STATES'))
|
||||||
transitions = json_file_parser(filepath=config.get('STATEMACHINE_TRANSITIONS'))
|
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.states = states
|
||||||
UssdStateMachine.transitions = transitions
|
UssdStateMachine.transitions = transitions
|
||||||
|
|
||||||
@ -152,7 +171,8 @@ def application(env, start_response):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# handle menu interaction requests
|
# 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,
|
external_session_id=external_session_id,
|
||||||
phone_number=phone_number,
|
phone_number=phone_number,
|
||||||
queue=args.q,
|
queue=args.q,
|
||||||
|
@ -12,6 +12,8 @@ from confini import Config
|
|||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.db import dsn_from_config
|
from cic_ussd.db import dsn_from_config
|
||||||
from cic_ussd.db.models.base import SessionBase
|
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.redis import InMemoryStore
|
||||||
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
||||||
|
|
||||||
@ -37,6 +39,7 @@ config.censor('PASSWORD', 'DATABASE')
|
|||||||
# define log levels
|
# define log levels
|
||||||
if args.vv:
|
if args.vv:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('sqlalchemy.engine').setLevel(logging.DEBUG)
|
||||||
elif args.v:
|
elif args.v:
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
@ -59,6 +62,14 @@ InMemoryStore.cache = redis.StrictRedis(host=config.get('REDIS_HOSTNAME'),
|
|||||||
decode_responses=True)
|
decode_responses=True)
|
||||||
InMemoryUssdSession.redis_cache = InMemoryStore.cache
|
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
|
# set up celery
|
||||||
current_app = celery.Celery(__name__)
|
current_app = celery.Celery(__name__)
|
||||||
|
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
# third party imports
|
# third party imports
|
||||||
|
import celery
|
||||||
|
|
||||||
# local imports
|
# 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.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.operations import save_to_in_memory_ussd_session_data
|
||||||
from cic_ussd.state_machine.state_machine import UssdStateMachine
|
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
|
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)
|
recipient = get_user_by_phone_number(phone_number=user_input)
|
||||||
is_not_initiator = user_input != user.phone_number
|
is_not_initiator = user_input != user.phone_number
|
||||||
has_active_account_status = user.get_account_status() == AccountStatus.ACTIVE.name
|
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 and recipient is not None
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, User]) -> bool:
|
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
|
user_input, ussd_session, user = state_machine_data
|
||||||
balance_manager = BalanceManager(address=user.blockchain_address,
|
balance_manager = BalanceManager(address=user.blockchain_address,
|
||||||
chain_str=UssdStateMachine.chain_str,
|
chain_str=Chain.spec.__str__(),
|
||||||
token_symbol='SRF')
|
token_symbol='SRF')
|
||||||
balance = balance_manager.get_operational_balance()
|
# get cached balance
|
||||||
return int(user_input) <= 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]):
|
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)
|
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]):
|
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.
|
"""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.
|
: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
|
to_address = recipient.blockchain_address
|
||||||
from_address = user.blockchain_address
|
from_address = user.blockchain_address
|
||||||
amount = int(ussd_session.get('session_data').get('transaction_amount'))
|
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,
|
from_address=from_address,
|
||||||
to_address=to_address)
|
to_address=to_address)
|
||||||
outgoing_tx_processor.process_outgoing_transfer_transaction(amount=amount)
|
outgoing_tx_processor.process_outgoing_transfer_transaction(amount=amount)
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
# standard imports
|
# standard imports
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Tuple
|
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
|
# local imports
|
||||||
|
from cic_ussd.chain import Chain
|
||||||
from cic_ussd.db.models.user import User
|
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.operations import save_to_in_memory_ussd_session_data
|
||||||
|
from cic_ussd.redis import get_cached_data
|
||||||
|
|
||||||
logg = logging.getLogger(__file__)
|
logg = logging.getLogger(__file__)
|
||||||
|
|
||||||
@ -42,7 +52,29 @@ def update_account_status_to_active(state_machine_data: Tuple[str, dict, User]):
|
|||||||
User.session.commit()
|
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.
|
"""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.
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
:type state_machine_data: tuple
|
: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
|
# define session data key from current state
|
||||||
key = ''
|
key = ''
|
||||||
if 'first_name' in current_state:
|
if 'given_name' in current_state:
|
||||||
key = 'first_name'
|
key = 'given_name'
|
||||||
elif 'last_name' in current_state:
|
elif 'family_name' in current_state:
|
||||||
key = 'last_name'
|
key = 'family_name'
|
||||||
elif 'gender' in current_state:
|
elif 'gender' in current_state:
|
||||||
key = 'gender'
|
key = 'gender'
|
||||||
|
user_input = process_gender_user_input(user=user, user_input=user_input)
|
||||||
elif 'location' in current_state:
|
elif 'location' in current_state:
|
||||||
key = 'location'
|
key = 'location'
|
||||||
elif 'business_profile' in current_state:
|
elif 'products' in current_state:
|
||||||
key = 'business_profile'
|
key = 'products'
|
||||||
|
|
||||||
# check if there is existing session data
|
# check if there is existing session data
|
||||||
if ussd_session.get('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)
|
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]):
|
def format_user_metadata(metadata: dict, user: User):
|
||||||
"""This function persists elements of the user profile stored in session data
|
"""
|
||||||
|
: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.
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
:type state_machine_data: tuple
|
:type state_machine_data: tuple
|
||||||
"""
|
"""
|
||||||
user_input, ussd_session, user = state_machine_data
|
user_input, ussd_session, user = state_machine_data
|
||||||
|
|
||||||
# get session data
|
# get session data
|
||||||
profile_data = ussd_session.get('session_data')
|
metadata = ussd_session.get('session_data')
|
||||||
logg.debug('This section requires implementation of user metadata.')
|
|
||||||
|
|
||||||
|
# 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
|
import re
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
from cic_types.models.person import generate_metadata_pointer
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.db.models.user import User
|
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()
|
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.
|
"""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.
|
:param state_machine_data: A tuple containing user input, a ussd session and user object.
|
||||||
:type state_machine_data: str
|
:type state_machine_data: str
|
||||||
"""
|
"""
|
||||||
user_input, ussd_session, user = state_machine_data
|
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),
|
||||||
def has_empty_username_data(state_machine_data: Tuple[str, dict, User]):
|
cic_type='cic.person'
|
||||||
"""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.
|
user_metadata = get_cached_data(key=key)
|
||||||
:type state_machine_data: str
|
return user_metadata is not None
|
||||||
"""
|
|
||||||
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.')
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_name(state_machine_data: Tuple[str, dict, User]):
|
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
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
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.
|
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
|
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.
|
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.
|
:cvar states: A list of pre-defined states.
|
||||||
:type states: list
|
:type states: list
|
||||||
:cvar transitions: A list of pre-defined transitions.
|
:cvar transitions: A list of pre-defined transitions.
|
||||||
:type transitions: list
|
:type transitions: list
|
||||||
"""
|
"""
|
||||||
chain_str = None
|
|
||||||
states = []
|
states = []
|
||||||
transitions = []
|
transitions = []
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import celery
|
|||||||
|
|
||||||
celery_app = celery.current_app
|
celery_app = celery.current_app
|
||||||
# export external celery task modules
|
# export external celery task modules
|
||||||
from .foo import log_it_plz
|
from .logger import *
|
||||||
from .ussd import persist_session_to_db
|
from .ussd_session import *
|
||||||
from .callback_handler import process_account_creation_callback
|
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
|
# standard imports
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
# third-party imports
|
# third-party imports
|
||||||
import celery
|
import celery
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
|
from cic_ussd.conversions import from_wei
|
||||||
from cic_ussd.db.models.base import SessionBase
|
from cic_ussd.db.models.base import SessionBase
|
||||||
from cic_ussd.db.models.user import User
|
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.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
|
from cic_ussd.transactions import IncomingTransactionProcessor
|
||||||
|
|
||||||
logg = logging.getLogger(__file__)
|
logg = logging.getLogger(__file__)
|
||||||
celery_app = celery.current_app
|
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):
|
def process_account_creation_callback(self, result: str, url: str, status_code: int):
|
||||||
"""This function defines a task that creates a user and
|
"""This function defines a task that creates a user and
|
||||||
:param result: The blockchain address for the created account
|
: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)
|
user = User(blockchain_address=result, phone_number=phone_number)
|
||||||
session.add(user)
|
session.add(user)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
# expire cache
|
# expire cache
|
||||||
cache.expire(task_id, timedelta(seconds=30))
|
cache.expire(task_id, timedelta(seconds=180))
|
||||||
session.close()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
cache.expire(task_id, timedelta(seconds=30))
|
|
||||||
session.close()
|
session.close()
|
||||||
|
cache.expire(task_id, timedelta(seconds=180))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
session.close()
|
session.close()
|
||||||
@ -65,9 +68,8 @@ def process_account_creation_callback(self, result: str, url: str, status_code:
|
|||||||
|
|
||||||
@celery_app.task
|
@celery_app.task
|
||||||
def process_incoming_transfer_callback(result: dict, param: str, status_code: int):
|
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()
|
session = SessionBase.create_session()
|
||||||
if result and status_code == 0:
|
if status_code == 0:
|
||||||
|
|
||||||
# collect result data
|
# collect result data
|
||||||
recipient_blockchain_address = result.get('recipient')
|
recipient_blockchain_address = result.get('recipient')
|
||||||
@ -93,22 +95,125 @@ def process_incoming_transfer_callback(result: dict, param: str, status_code: in
|
|||||||
value=value)
|
value=value)
|
||||||
|
|
||||||
if param == 'tokengift':
|
if param == 'tokengift':
|
||||||
logg.debug('Name information would require integration with cic meta.')
|
incoming_tx_processor.process_token_gift_incoming_transactions()
|
||||||
incoming_tx_processor.process_token_gift_incoming_transactions(first_name="")
|
|
||||||
elif param == 'transfer':
|
elif param == 'transfer':
|
||||||
logg.debug('Name information would require integration with cic meta.')
|
|
||||||
if sender_user:
|
if sender_user:
|
||||||
sender_information = f'{sender_user.phone_number}, {""}, {""}'
|
sender_information = define_account_tx_metadata(user=sender_user)
|
||||||
incoming_tx_processor.process_transfer_incoming_transaction(sender_information=sender_information)
|
incoming_tx_processor.process_transfer_incoming_transaction(
|
||||||
|
sender_information=sender_information,
|
||||||
|
recipient_blockchain_address=recipient_blockchain_address
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logg.warning(
|
logg.warning(
|
||||||
f'Tx with sender: {sender_blockchain_address} was received but has no matching user in the system.'
|
f'Tx with sender: {sender_blockchain_address} was received but has no matching user in the system.'
|
||||||
)
|
)
|
||||||
incoming_tx_processor.process_transfer_incoming_transaction(
|
incoming_tx_processor.process_transfer_incoming_transaction(
|
||||||
sender_information=sender_blockchain_address)
|
sender_information='GRASSROOTS ECONOMICS',
|
||||||
|
recipient_blockchain_address=recipient_blockchain_address
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
session.close()
|
session.close()
|
||||||
raise ValueError(f'Unexpected transaction param: {param}.')
|
raise ValueError(f'Unexpected transaction param: {param}.')
|
||||||
else:
|
else:
|
||||||
session.close()
|
session.close()
|
||||||
raise ValueError(f'Unexpected status code: {status_code}.')
|
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.db.models.ussd_session import UssdSession
|
||||||
from cic_ussd.error import SessionNotFoundError
|
from cic_ussd.error import SessionNotFoundError
|
||||||
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
||||||
|
from cic_ussd.tasks.base import CriticalSQLAlchemyTask
|
||||||
|
|
||||||
celery_app = celery.current_app
|
celery_app = celery.current_app
|
||||||
logg = get_logger(__file__)
|
logg = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
@celery_app.task(base=CriticalSQLAlchemyTask)
|
||||||
def persist_session_to_db(external_session_id: str):
|
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.
|
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)
|
in_db_ussd_session.set_data(key=key, value=value, session=session)
|
||||||
|
|
||||||
session.add(in_db_ussd_session)
|
session.add(in_db_ussd_session)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
InMemoryUssdSession.redis_cache.expire(external_session_id, timedelta(minutes=1))
|
InMemoryUssdSession.redis_cache.expire(external_session_id, timedelta(minutes=1))
|
||||||
else:
|
else:
|
||||||
session.close()
|
session.close()
|
||||||
raise SessionNotFoundError('Session does not exist!')
|
raise SessionNotFoundError('Session does not exist!')
|
||||||
|
|
||||||
session.commit()
|
|
||||||
session.close()
|
|
||||||
|
|
@ -7,6 +7,7 @@ from datetime import datetime
|
|||||||
from cic_eth.api import Api
|
from cic_eth.api import Api
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
|
from cic_ussd.balance import get_cached_operational_balance
|
||||||
from cic_ussd.notifications import Notifier
|
from cic_ussd.notifications import Notifier
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ def from_wei(value: int) -> float:
|
|||||||
:return: SRF equivalent of value in Wei
|
:return: SRF equivalent of value in Wei
|
||||||
:rtype: float
|
:rtype: float
|
||||||
"""
|
"""
|
||||||
value = float(value) / 1e+18
|
value = float(value) / 1e+6
|
||||||
return truncate(value=value, decimals=2)
|
return truncate(value=value, decimals=2)
|
||||||
|
|
||||||
|
|
||||||
@ -67,11 +68,9 @@ class IncomingTransactionProcessor:
|
|||||||
self.token_symbol = token_symbol
|
self.token_symbol = token_symbol
|
||||||
self.value = value
|
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
|
"""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.
|
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)
|
balance = from_wei(value=self.value)
|
||||||
@ -80,20 +79,22 @@ class IncomingTransactionProcessor:
|
|||||||
phone_number=self.phone_number,
|
phone_number=self.phone_number,
|
||||||
preferred_language=self.preferred_language,
|
preferred_language=self.preferred_language,
|
||||||
balance=balance,
|
balance=balance,
|
||||||
first_name=first_name,
|
|
||||||
token_symbol=self.token_symbol)
|
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
|
"""This function processes incoming transactions with the "transfer" param and issues notifications to users
|
||||||
about reception of funds into their accounts.
|
about reception of funds into their accounts.
|
||||||
:param sender_information: A string with a user's full name and phone number.
|
:param sender_information: A string with a user's full name and phone number.
|
||||||
:type sender_information: str
|
:type sender_information: str
|
||||||
|
:param recipient_blockchain_address:
|
||||||
|
type recipient_blockchain_address: str
|
||||||
"""
|
"""
|
||||||
key = 'sms.received_tokens'
|
key = 'sms.received_tokens'
|
||||||
amount = from_wei(value=self.value)
|
amount = from_wei(value=self.value)
|
||||||
timestamp = datetime.now().strftime('%d-%m-%y, %H:%M %p')
|
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,
|
notifier.send_sms_notification(key=key,
|
||||||
phone_number=self.phone_number,
|
phone_number=self.phone_number,
|
||||||
preferred_language=self.preferred_language,
|
preferred_language=self.preferred_language,
|
||||||
@ -101,7 +102,7 @@ class IncomingTransactionProcessor:
|
|||||||
token_symbol=self.token_symbol,
|
token_symbol=self.token_symbol,
|
||||||
tx_sender_information=sender_information,
|
tx_sender_information=sender_information,
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
balance='')
|
balance=operational_balance)
|
||||||
|
|
||||||
|
|
||||||
class OutgoingTransactionProcessor:
|
class OutgoingTransactionProcessor:
|
||||||
|
@ -110,7 +110,7 @@ def validate_phone_number(phone: str):
|
|||||||
|
|
||||||
|
|
||||||
def validate_response_type(processor_response: str) -> bool:
|
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
|
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
|
determines whether the response should prompt the end of a ussd session or the
|
||||||
:param processor_response: A ussd menu's text value.
|
:param processor_response: A ussd menu's text value.
|
||||||
|
@ -6,10 +6,12 @@ betterpath==0.2.2
|
|||||||
billiard==3.6.3.0
|
billiard==3.6.3.0
|
||||||
celery==4.4.7
|
celery==4.4.7
|
||||||
cffi==1.14.3
|
cffi==1.14.3
|
||||||
cic-eth~=0.10.0a22
|
chainlib~=0.0.1a15
|
||||||
|
cic-eth==0.10.0a38
|
||||||
cic-notify==0.3.1
|
cic-notify==0.3.1
|
||||||
|
cic-types==0.1.0a8
|
||||||
click==7.1.2
|
click==7.1.2
|
||||||
confini~=0.3.6a1
|
confini==0.3.5
|
||||||
cryptography==3.2.1
|
cryptography==3.2.1
|
||||||
faker==4.17.1
|
faker==4.17.1
|
||||||
iniconfig==1.1.1
|
iniconfig==1.1.1
|
||||||
@ -34,6 +36,7 @@ python-i18n==0.3.9
|
|||||||
pytz==2020.1
|
pytz==2020.1
|
||||||
PyYAML==5.3.1
|
PyYAML==5.3.1
|
||||||
redis==3.5.3
|
redis==3.5.3
|
||||||
|
requests==2.24.0
|
||||||
semver==2.13.0
|
semver==2.13.0
|
||||||
six==1.15.0
|
six==1.15.0
|
||||||
SQLAlchemy==1.3.20
|
SQLAlchemy==1.3.20
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
[
|
[
|
||||||
"account_management",
|
"account_management",
|
||||||
"profile_management",
|
"metadata_management",
|
||||||
"select_preferred_language",
|
"select_preferred_language",
|
||||||
"enter_current_pin",
|
"enter_current_pin",
|
||||||
"mini_statement_inquiry_pin_authorization",
|
|
||||||
"enter_new_pin",
|
"enter_new_pin",
|
||||||
"new_pin_confirmation",
|
"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-cov==2.10.1
|
||||||
pytest-mock==3.3.1
|
pytest-mock==3.3.1
|
||||||
pytest-redis==2.0.0
|
pytest-redis==2.0.0
|
||||||
|
requests-mock==1.8.0
|
30
apps/cic-ussd/tests/cic_ussd/db/test_db.py
Normal file
30
apps/cic-ussd/tests/cic_ussd/db/test_db.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# standard imports
|
||||||
|
import os
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_ussd.db import dsn_from_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_dsn_from_config(load_config):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
# test dsn for sqlite engine
|
||||||
|
dsn = dsn_from_config(load_config)
|
||||||
|
scheme = f'{load_config.get("DATABASE_ENGINE")}+{load_config.get("DATABASE_DRIVER")}'
|
||||||
|
assert dsn == f'{scheme}:///{load_config.get("DATABASE_NAME")}'
|
||||||
|
|
||||||
|
# test dsn for other db formats
|
||||||
|
overrides = {
|
||||||
|
'DATABASE_PASSWORD': 'password',
|
||||||
|
'DATABASE_DRIVER': 'psycopg2',
|
||||||
|
'DATABASE_ENGINE': 'postgresql'
|
||||||
|
}
|
||||||
|
load_config.dict_override(dct=overrides, dct_description='Override values to test different db formats.')
|
||||||
|
|
||||||
|
scheme = f'{load_config.get("DATABASE_ENGINE")}+{load_config.get("DATABASE_DRIVER")}'
|
||||||
|
|
||||||
|
dsn = dsn_from_config(load_config)
|
||||||
|
assert dsn == f"{scheme}://{load_config.get('DATABASE_USER')}:{load_config.get('DATABASE_PASSWORD')}@{load_config.get('DATABASE_HOST')}:{load_config.get('DATABASE_PORT')}/{load_config.get('DATABASE_NAME')}"
|
||||||
|
|
80
apps/cic-ussd/tests/cic_ussd/metadata/test_metadata.py
Normal file
80
apps/cic-ussd/tests/cic_ussd/metadata/test_metadata.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# standard imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
import requests_mock
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_ussd.error import UnsupportedMethodError
|
||||||
|
from cic_ussd.metadata import blockchain_address_to_metadata_pointer, make_request
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_request(define_metadata_pointer_url, mock_meta_get_response, mock_meta_post_response, person_metadata):
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'GET',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=200,
|
||||||
|
reason='OK',
|
||||||
|
content=json.dumps(mock_meta_get_response).encode('utf-8')
|
||||||
|
)
|
||||||
|
response = make_request(method='GET', url=define_metadata_pointer_url)
|
||||||
|
assert response.content == requests.get(define_metadata_pointer_url).content
|
||||||
|
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'POST',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=201,
|
||||||
|
reason='CREATED',
|
||||||
|
content=json.dumps(mock_meta_post_response).encode('utf-8')
|
||||||
|
)
|
||||||
|
response = make_request(
|
||||||
|
method='POST',
|
||||||
|
url=define_metadata_pointer_url,
|
||||||
|
data=json.dumps(person_metadata).encode('utf-8'),
|
||||||
|
headers={
|
||||||
|
'X-CIC-AUTOMERGE': 'server',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert response.content == requests.post(define_metadata_pointer_url).content
|
||||||
|
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'PUT',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=200,
|
||||||
|
reason='OK'
|
||||||
|
)
|
||||||
|
response = make_request(
|
||||||
|
method='PUT',
|
||||||
|
url=define_metadata_pointer_url,
|
||||||
|
data=json.dumps(person_metadata).encode('utf-8'),
|
||||||
|
headers={
|
||||||
|
'X-CIC-AUTOMERGE': 'server',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert response.content == requests.put(define_metadata_pointer_url).content
|
||||||
|
|
||||||
|
with pytest.raises(UnsupportedMethodError) as error:
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'DELETE',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=200,
|
||||||
|
reason='OK'
|
||||||
|
)
|
||||||
|
make_request(
|
||||||
|
method='DELETE',
|
||||||
|
url=define_metadata_pointer_url
|
||||||
|
)
|
||||||
|
assert str(error.value) == 'Unsupported method: DELETE'
|
||||||
|
|
||||||
|
|
||||||
|
def test_blockchain_address_to_metadata_pointer(create_activated_user):
|
||||||
|
blockchain_address = create_activated_user.blockchain_address
|
||||||
|
assert type(blockchain_address_to_metadata_pointer(blockchain_address)) == bytes
|
34
apps/cic-ussd/tests/cic_ussd/metadata/test_signer.py
Normal file
34
apps/cic-ussd/tests/cic_ussd/metadata/test_signer.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# standard imports
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_ussd.metadata.signer import Signer
|
||||||
|
|
||||||
|
|
||||||
|
def test_client(load_config, setup_metadata_signer, person_metadata):
|
||||||
|
signer = Signer()
|
||||||
|
# get gpg used
|
||||||
|
digest = 'a4337bc45a8fc544c03f52dc550cd6e1e87021bc896588bd79e901e2'
|
||||||
|
person_metadata['digest'] = digest
|
||||||
|
gpg = signer.gpg
|
||||||
|
|
||||||
|
# check that key data was loaded
|
||||||
|
assert signer.key_data is not None
|
||||||
|
|
||||||
|
# check that correct operational key is returned
|
||||||
|
gpg.import_keys(key_data=signer.key_data)
|
||||||
|
gpg_keys = gpg.list_keys()
|
||||||
|
assert signer.get_operational_key() == gpg_keys[0]
|
||||||
|
|
||||||
|
# check that correct signature is returned
|
||||||
|
key_id = signer.get_operational_key().get('keyid')
|
||||||
|
signature = gpg.sign(message=digest, passphrase=load_config.get('KEYS_PASSPHRASE'), keyid=key_id)
|
||||||
|
assert str(signature) == signer.sign_digest(data=person_metadata)
|
||||||
|
|
||||||
|
# remove tmp gpg file
|
||||||
|
shutil.rmtree(Signer.gpg_path)
|
||||||
|
|
||||||
|
|
||||||
|
|
123
apps/cic-ussd/tests/cic_ussd/metadata/test_user_metadata.py
Normal file
123
apps/cic-ussd/tests/cic_ussd/metadata/test_user_metadata.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# standard imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# third-party imports
|
||||||
|
import pytest
|
||||||
|
import requests_mock
|
||||||
|
from cic_types.models.person import generate_metadata_pointer
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
from cic_ussd.metadata import blockchain_address_to_metadata_pointer
|
||||||
|
from cic_ussd.metadata.signer import Signer
|
||||||
|
from cic_ussd.metadata.user import UserMetadata
|
||||||
|
from cic_ussd.redis import get_cached_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_metadata(create_activated_user, define_metadata_pointer_url, load_config):
|
||||||
|
UserMetadata.base_url = load_config.get('CIC_META_URL')
|
||||||
|
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
||||||
|
user_metadata_client = UserMetadata(identifier=identifier)
|
||||||
|
|
||||||
|
assert user_metadata_client.url == define_metadata_pointer_url
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user_metadata(caplog,
|
||||||
|
create_activated_user,
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
load_config,
|
||||||
|
mock_meta_post_response,
|
||||||
|
person_metadata):
|
||||||
|
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
||||||
|
user_metadata_client = UserMetadata(identifier=identifier)
|
||||||
|
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'POST',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=201,
|
||||||
|
reason='CREATED',
|
||||||
|
content=json.dumps(mock_meta_post_response).encode('utf-8')
|
||||||
|
)
|
||||||
|
user_metadata_client.create(data=person_metadata)
|
||||||
|
assert 'Get signed material response status: 201' in caplog.text
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError) as error:
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'POST',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=400,
|
||||||
|
reason='BAD REQUEST'
|
||||||
|
)
|
||||||
|
user_metadata_client.create(data=person_metadata)
|
||||||
|
assert str(error.value) == f'400 Client Error: BAD REQUEST for url: {define_metadata_pointer_url}'
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_user_metadata(caplog,
|
||||||
|
create_activated_user,
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
load_config,
|
||||||
|
person_metadata,
|
||||||
|
setup_metadata_signer):
|
||||||
|
Signer.gpg_passphrase = load_config.get('KEYS_PASSPHRASE')
|
||||||
|
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
||||||
|
user_metadata_client = UserMetadata(identifier=identifier)
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'PUT',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=200,
|
||||||
|
reason='OK'
|
||||||
|
)
|
||||||
|
user_metadata_client.edit(data=person_metadata, engine='pgp')
|
||||||
|
assert 'Signed content submission status: 200' in caplog.text
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError) as error:
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'PUT',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=400,
|
||||||
|
reason='BAD REQUEST'
|
||||||
|
)
|
||||||
|
user_metadata_client.edit(data=person_metadata, engine='pgp')
|
||||||
|
assert str(error.value) == f'400 Client Error: BAD REQUEST for url: {define_metadata_pointer_url}'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_metadata(caplog,
|
||||||
|
create_activated_user,
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
init_redis_cache,
|
||||||
|
load_config,
|
||||||
|
person_metadata,
|
||||||
|
setup_metadata_signer):
|
||||||
|
identifier = blockchain_address_to_metadata_pointer(blockchain_address=create_activated_user.blockchain_address)
|
||||||
|
user_metadata_client = UserMetadata(identifier=identifier)
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'GET',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=200,
|
||||||
|
content=json.dumps(person_metadata).encode('utf-8'),
|
||||||
|
reason='OK'
|
||||||
|
)
|
||||||
|
user_metadata_client.query()
|
||||||
|
assert 'Get latest data status: 200' in caplog.text
|
||||||
|
key = generate_metadata_pointer(
|
||||||
|
identifier=identifier,
|
||||||
|
cic_type='cic.person'
|
||||||
|
)
|
||||||
|
cached_user_metadata = get_cached_data(key=key)
|
||||||
|
assert cached_user_metadata
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError) as error:
|
||||||
|
with requests_mock.Mocker(real_http=False) as request_mocker:
|
||||||
|
request_mocker.register_uri(
|
||||||
|
'GET',
|
||||||
|
define_metadata_pointer_url,
|
||||||
|
status_code=404,
|
||||||
|
reason='NOT FOUND'
|
||||||
|
)
|
||||||
|
user_metadata_client.query()
|
||||||
|
assert 'The data is not available and might need to be added.' in caplog.text
|
||||||
|
assert str(error.value) == f'400 Client Error: NOT FOUND for url: {define_metadata_pointer_url}'
|
@ -5,7 +5,6 @@ import json
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.state_machine import UssdStateMachine
|
|
||||||
from cic_ussd.state_machine.logic.transaction import (has_sufficient_balance,
|
from cic_ussd.state_machine.logic.transaction import (has_sufficient_balance,
|
||||||
is_valid_recipient,
|
is_valid_recipient,
|
||||||
is_valid_transaction_amount,
|
is_valid_transaction_amount,
|
||||||
@ -100,8 +99,8 @@ def test_process_transaction_request(create_valid_tx_recipient,
|
|||||||
create_valid_tx_sender,
|
create_valid_tx_sender,
|
||||||
load_config,
|
load_config,
|
||||||
mock_outgoing_transactions,
|
mock_outgoing_transactions,
|
||||||
|
setup_chain_spec,
|
||||||
ussd_session_data):
|
ussd_session_data):
|
||||||
UssdStateMachine.chain_str = load_config.get('CIC_CHAIN_SPEC')
|
|
||||||
ussd_session_data['session_data'] = {
|
ussd_session_data['session_data'] = {
|
||||||
'recipient_phone_number': create_valid_tx_recipient.phone_number,
|
'recipient_phone_number': create_valid_tx_recipient.phone_number,
|
||||||
'transaction_amount': '50'
|
'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
|
# local imports
|
||||||
from cic_ussd.db.models.user import User
|
from cic_ussd.db.models.user import User
|
||||||
from cic_ussd.error import ActionDataNotFoundError
|
from cic_ussd.error import ActionDataNotFoundError
|
||||||
from cic_ussd.transactions import from_wei
|
from cic_ussd.conversions import from_wei
|
||||||
|
|
||||||
logg = logging.getLogger()
|
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,
|
def test_successful_incoming_transaction_sender_not_found(caplog,
|
||||||
celery_session_worker,
|
celery_session_worker,
|
||||||
create_valid_tx_recipient,
|
create_valid_tx_recipient,
|
||||||
|
mock_notifier_api,
|
||||||
successful_incoming_transfer_callback):
|
successful_incoming_transfer_callback):
|
||||||
result = successful_incoming_transfer_callback.get('RESULT')
|
result = successful_incoming_transfer_callback.get('RESULT')
|
||||||
param = successful_incoming_transfer_callback.get('PARAM')
|
param = successful_incoming_transfer_callback.get('PARAM')
|
@ -15,7 +15,7 @@ def test_persist_session_to_db_task(
|
|||||||
create_in_redis_ussd_session):
|
create_in_redis_ussd_session):
|
||||||
external_session_id = ussd_session_data.get('external_session_id')
|
external_session_id = ussd_session_data.get('external_session_id')
|
||||||
s_persist_session_to_db = celery.signature(
|
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]
|
[external_session_id]
|
||||||
)
|
)
|
||||||
result = s_persist_session_to_db.apply_async()
|
result = s_persist_session_to_db.apply_async()
|
||||||
@ -38,7 +38,7 @@ def test_session_not_found_error(
|
|||||||
with pytest.raises(SessionNotFoundError) as error:
|
with pytest.raises(SessionNotFoundError) as error:
|
||||||
external_session_id = 'SomeRandomValue'
|
external_session_id = 'SomeRandomValue'
|
||||||
s_persist_session_to_db = celery.signature(
|
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]
|
[external_session_id]
|
||||||
)
|
)
|
||||||
result = s_persist_session_to_db.apply_async()
|
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,
|
recipient,
|
||||||
set_locale_files,
|
set_locale_files,
|
||||||
mock_notifier_api):
|
mock_notifier_api):
|
||||||
|
|
||||||
notifier = Notifier()
|
notifier = Notifier()
|
||||||
notifier.queue = None
|
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('message') == expected_message
|
||||||
assert messages[0].get('recipient') == recipient
|
assert messages[0].get('recipient') == recipient
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -6,6 +6,7 @@ import uuid
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
|
from cic_ussd.chain import Chain
|
||||||
from cic_ussd.db.models.task_tracker import TaskTracker
|
from cic_ussd.db.models.task_tracker import TaskTracker
|
||||||
from cic_ussd.menu.ussd_menu import UssdMenu
|
from cic_ussd.menu.ussd_menu import UssdMenu
|
||||||
from cic_ussd.operations import (add_tasks_to_tracker,
|
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,
|
get_latest_input,
|
||||||
initiate_account_creation_request,
|
initiate_account_creation_request,
|
||||||
process_current_menu,
|
process_current_menu,
|
||||||
process_phone_number,
|
|
||||||
process_menu_interaction_requests,
|
process_menu_interaction_requests,
|
||||||
cache_account_creation_task_id,
|
cache_account_creation_task_id,
|
||||||
get_user_by_phone_number,
|
|
||||||
reset_pin,
|
reset_pin,
|
||||||
update_ussd_session,
|
update_ussd_session,
|
||||||
save_to_in_memory_ussd_session_data)
|
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.transactions import truncate
|
||||||
from cic_ussd.redis import InMemoryStore
|
from cic_ussd.redis import InMemoryStore
|
||||||
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
|
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_config,
|
||||||
load_ussd_menu,
|
load_ussd_menu,
|
||||||
mocker,
|
mocker,
|
||||||
|
setup_chain_spec,
|
||||||
set_locale_files,
|
set_locale_files,
|
||||||
ussd_session_data):
|
ussd_session_data):
|
||||||
external_session_id = ussd_session_data.get('external_session_id')
|
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 = mocker.patch('cic_ussd.operations.cache_account_creation_task_id')
|
||||||
mocked_cache_function(phone_number, 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,
|
external_session_id=external_session_id,
|
||||||
phone_number=ussd_session_data.get('msisdn'),
|
phone_number=ussd_session_data.get('msisdn'),
|
||||||
service_code=ussd_session_data.get('service_code'),
|
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_ussd_menu,
|
||||||
load_data_into_state_machine,
|
load_data_into_state_machine,
|
||||||
load_config,
|
load_config,
|
||||||
|
setup_chain_spec,
|
||||||
celery_session_worker,
|
celery_session_worker,
|
||||||
create_activated_user,
|
create_activated_user,
|
||||||
create_in_db_ussd_session):
|
create_in_db_ussd_session):
|
||||||
|
chain_str = Chain.spec.__str__()
|
||||||
response = process_menu_interaction_requests(
|
response = process_menu_interaction_requests(
|
||||||
chain_str=load_config.get('CIC_CHAIN_SPEC'),
|
chain_str=chain_str,
|
||||||
external_session_id=external_session_id,
|
external_session_id=external_session_id,
|
||||||
phone_number=phone_number,
|
phone_number=phone_number,
|
||||||
queue='cic-ussd',
|
queue='cic-ussd',
|
@ -12,7 +12,7 @@ from cic_ussd.processor import (custom_display_text,
|
|||||||
def test_process_pin_authorization(create_activated_user,
|
def test_process_pin_authorization(create_activated_user,
|
||||||
load_ussd_menu,
|
load_ussd_menu,
|
||||||
set_locale_files):
|
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(
|
response = process_pin_authorization(
|
||||||
display_key=ussd_menu.get('display_key'),
|
display_key=ussd_menu.get('display_key'),
|
||||||
user=create_activated_user
|
user=create_activated_user
|
@ -4,6 +4,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
|
from cic_ussd.chain import Chain
|
||||||
from cic_ussd.transactions import OutgoingTransactionProcessor, truncate
|
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_recipient,
|
||||||
create_valid_tx_sender,
|
create_valid_tx_sender,
|
||||||
mock_outgoing_transactions):
|
mock_outgoing_transactions):
|
||||||
|
chain_str = Chain.spec.__str__()
|
||||||
outgoing_tx_processor = OutgoingTransactionProcessor(
|
outgoing_tx_processor = OutgoingTransactionProcessor(
|
||||||
chain_str=load_config.get('CIC_CHAIN_SPEC'),
|
chain_str=chain_str,
|
||||||
from_address=create_valid_tx_sender.blockchain_address,
|
from_address=create_valid_tx_sender.blockchain_address,
|
||||||
to_address=create_valid_tx_recipient.blockchain_address
|
to_address=create_valid_tx_recipient.blockchain_address
|
||||||
)
|
)
|
@ -1,3 +1,7 @@
|
|||||||
|
# third-party imports
|
||||||
|
from cic_types.pytest import *
|
||||||
|
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from tests.fixtures.config import *
|
from tests.fixtures.config import *
|
||||||
from tests.fixtures.db import *
|
from tests.fixtures.db import *
|
||||||
@ -8,41 +12,3 @@ from tests.fixtures.redis import *
|
|||||||
from tests.fixtures.callback import *
|
from tests.fixtures.callback import *
|
||||||
from tests.fixtures.requests import *
|
from tests.fixtures.requests import *
|
||||||
from tests.fixtures.mocks 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 [
|
return [
|
||||||
'cic_ussd.tasks.ussd',
|
'cic_ussd.tasks.ussd',
|
||||||
'cic_ussd.tasks.callback_handler',
|
'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 i18n
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
# third party imports
|
# third party imports
|
||||||
import pytest
|
import pytest
|
||||||
|
from chainlib.chain import ChainSpec
|
||||||
from confini import Config
|
from confini import Config
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
|
from cic_ussd.chain import Chain
|
||||||
from cic_ussd.db import dsn_from_config
|
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.files.local_files import create_local_file_data_stores, json_file_parser
|
||||||
from cic_ussd.menu.ussd_menu import UssdMenu
|
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.state_machine import UssdStateMachine
|
||||||
from cic_ussd.encoder import PasswordEncoder
|
|
||||||
|
|
||||||
|
|
||||||
logg = logging.getLogger()
|
logg = logging.getLogger()
|
||||||
@ -102,3 +108,29 @@ def uwsgi_env():
|
|||||||
'uwsgi.node': b'mango-habanero'
|
'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
|
# standard imports
|
||||||
|
import json
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
# third-party imports
|
# third-party imports
|
||||||
import pytest
|
import pytest
|
||||||
@ -8,7 +10,49 @@ from cic_ussd.translation import translation_for
|
|||||||
from cic_ussd.transactions import truncate
|
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):
|
def mock_notifier_api(mocker):
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
@ -43,7 +87,7 @@ def mock_outgoing_transactions(mocker):
|
|||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def mock_balance(mocker):
|
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):
|
def _mock_operational_balance(balance: int):
|
||||||
mocked_operational_balance.return_value = truncate(value=balance, decimals=2)
|
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
|
# standard imports
|
||||||
from random import randint
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
from random import randint
|
||||||
|
|
||||||
# third party imports
|
# third party imports
|
||||||
import pytest
|
import pytest
|
||||||
|
from cic_types.models.person import generate_metadata_pointer
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
from cic_ussd.db.models.user import AccountStatus, User
|
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()
|
fake = Faker()
|
||||||
@ -92,3 +96,25 @@ def create_locked_accounts(init_database, set_fernet_key):
|
|||||||
user.account_status = AccountStatus.LOCKED.value
|
user.account_status = AccountStatus.LOCKED.value
|
||||||
user.session.add(user)
|
user.session.add(user)
|
||||||
user.session.commit()
|
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",
|
"trigger": "scan_data",
|
||||||
"source": "account_management",
|
"source": "account_management",
|
||||||
"dest": "profile_management",
|
"dest": "metadata_management",
|
||||||
"conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
|
"conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -14,21 +14,44 @@
|
|||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "account_management",
|
"source": "account_management",
|
||||||
"dest": "mini_statement_pin_authorization",
|
"dest": "account_balances_pin_authorization",
|
||||||
"conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
|
"conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "mini_statement_pin_authorization",
|
"source": "account_balances_pin_authorization",
|
||||||
"dest": "exit",
|
"dest": "account_balances",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
|
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin"
|
||||||
"after": "cic_ussd.state_machine.logic.sms.process_mini_statement_request"
|
},
|
||||||
|
{
|
||||||
|
"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",
|
"trigger": "scan_data",
|
||||||
"source": "account_management",
|
"source": "account_management",
|
||||||
"dest": "enter_current_pin",
|
"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",
|
"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",
|
"trigger": "scan_data",
|
||||||
"source": "enter_gender",
|
"source": "enter_gender",
|
||||||
"dest": "enter_location",
|
"dest": "enter_location",
|
||||||
"conditions": "cic_ussd.state_machine.validator.has_empty_location_data",
|
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
|
||||||
"after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
|
"conditions": "cic_ussd.state_machine.logic.validator.is_valid_gender_selection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "enter_gender",
|
"source": "enter_gender",
|
||||||
"dest": "enter_business_profile",
|
"dest": "standard_pin_authorization",
|
||||||
"conditions": "cic_ussd.state_machine.validator.has_empty_business_profile_data",
|
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
|
||||||
"after": "cic_ussd.state_machine.logic.user.save_profile_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",
|
"trigger": "scan_data",
|
||||||
"source": "enter_gender",
|
"source": "standard_pin_authorization",
|
||||||
"dest": "gender_management_pin_authorization",
|
|
||||||
"conditions": "cic_ussd.state_machine.logic.validator.has_complete_profile_data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"trigger": "scan_data",
|
|
||||||
"source": "gender_management_pin_authorization",
|
|
||||||
"dest": "exit",
|
"dest": "exit",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
|
"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",
|
"trigger": "scan_data",
|
||||||
"source": "gender_management_pin_authorization",
|
"source": "standard_pin_authorization",
|
||||||
"dest": "exit_pin_blocked",
|
"dest": "exit_pin_blocked",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
|
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "enter_gender",
|
"source": "enter_gender",
|
||||||
"dest": "exit_invalid_input"
|
"dest": "exit_invalid_menu_option"
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -2,49 +2,28 @@
|
|||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "enter_location",
|
"source": "enter_location",
|
||||||
"dest": "enter_first_name",
|
"dest": "enter_products",
|
||||||
"conditions": [
|
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
|
||||||
"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",
|
"trigger": "scan_data",
|
||||||
"source": "enter_location",
|
"source": "enter_location",
|
||||||
"dest": "enter_gender",
|
"dest": "standard_pin_authorization",
|
||||||
"conditions": "cic_ussd.state_machine.validator.has_empty_gender_data",
|
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
|
||||||
"after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
|
"conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "enter_location",
|
"source": "standard_pin_authorization",
|
||||||
"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",
|
|
||||||
"dest": "exit",
|
"dest": "exit",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
|
"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",
|
"trigger": "scan_data",
|
||||||
"source": "location_management_pin_authorization",
|
"source": "standard_pin_authorization",
|
||||||
"dest": "exit_pin_blocked",
|
"dest": "exit_pin_blocked",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
|
"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",
|
"trigger": "scan_data",
|
||||||
"source": "enter_first_name",
|
"source": "enter_given_name",
|
||||||
"dest": "enter_last_name",
|
"dest": "enter_family_name",
|
||||||
"conditions": "cic_ussd.state_machine.logic.is_valid_name",
|
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data"
|
||||||
"after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "enter_last_name",
|
"source": "enter_given_name",
|
||||||
"dest": "enter_gender",
|
"dest": "standard_pin_authorization",
|
||||||
"conditions": "cic_ussd.state_machine.validator.has_empty_gender_data",
|
"after": "cic_ussd.state_machine.logic.user.save_metadata_attribute_to_session_data",
|
||||||
"after": "cic_ussd.state_machine.logic.user.save_profile_attribute_to_session_data"
|
"conditions": "cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "enter_last_name",
|
"source": "standard_pin_authorization",
|
||||||
"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",
|
|
||||||
"dest": "exit",
|
"dest": "exit",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin",
|
"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",
|
"trigger": "scan_data",
|
||||||
"source": "name_management_pin_authorization",
|
"source": "standard_pin_authorization",
|
||||||
"dest": "exit_pin_blocked",
|
"dest": "exit_pin_blocked",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
|
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "enter_last_name",
|
"source": "enter_family_name",
|
||||||
"dest": "exit_invalid_input"
|
"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",
|
"trigger": "scan_data",
|
||||||
"source": "initial_pin_confirmation",
|
"source": "initial_pin_confirmation",
|
||||||
"dest": "start",
|
"dest": "start",
|
||||||
|
"conditions": [
|
||||||
|
"cic_ussd.state_machine.logic.pin.pins_match",
|
||||||
|
"cic_ussd.state_machine.logic.validator.has_cached_user_metadata"
|
||||||
|
],
|
||||||
"after": [
|
"after": [
|
||||||
"cic_ussd.state_machine.logic.pin.complete_pin_change",
|
"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.user.get_user_metadata",
|
||||||
"cic_ussd.state_machine.logic.sms.send_terms_to_user_if_required"
|
"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",
|
"trigger": "scan_data",
|
||||||
"source": "enter_transaction_recipient",
|
"source": "enter_transaction_recipient",
|
||||||
"dest": "enter_transaction_amount",
|
"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"
|
"conditions": "cic_ussd.state_machine.logic.transaction.is_valid_recipient"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,49 +1,49 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "profile_management",
|
"source": "metadata_management",
|
||||||
"dest": "enter_first_name",
|
"dest": "enter_given_name",
|
||||||
"conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
|
"conditions": "cic_ussd.state_machine.logic.menu.menu_one_selected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "profile_management",
|
"source": "metadata_management",
|
||||||
"dest": "enter_gender",
|
"dest": "enter_age",
|
||||||
"conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
|
"conditions": "cic_ussd.state_machine.logic.menu.menu_two_selected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "profile_management",
|
"source": "metadata_management",
|
||||||
"dest": "enter_location",
|
"dest": "enter_location",
|
||||||
"conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
|
"conditions": "cic_ussd.state_machine.logic.menu.menu_three_selected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "profile_management",
|
"source": "metadata_management",
|
||||||
"dest": "edit_business_profile",
|
"dest": "enter_products",
|
||||||
"conditions": "cic_ussd.state_machine.logic.menu.menu_four_selected"
|
"conditions": "cic_ussd.state_machine.logic.menu.menu_four_selected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "profile_management",
|
"source": "metadata_management",
|
||||||
"dest": "view_profile_pin_authorization",
|
"dest": "standard_pin_authorization",
|
||||||
"conditions": "cic_ussd.state_machine.logic.menu.menu_five_selected"
|
"conditions": "cic_ussd.state_machine.logic.menu.menu_five_selected"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "view_profile_pin_authorization",
|
"source": "standard_pin_authorization",
|
||||||
"dest": "display_user_profile_data",
|
"dest": "display_user_metadata",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin"
|
"conditions": "cic_ussd.state_machine.logic.pin.is_authorized_pin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "view_profile_pin_authorization",
|
"source": "standard_pin_authorization",
|
||||||
"dest": "exit_pin_blocked",
|
"dest": "exit_pin_blocked",
|
||||||
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
|
"conditions": "cic_ussd.state_machine.logic.pin.is_locked_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"trigger": "scan_data",
|
"trigger": "scan_data",
|
||||||
"source": "profile_management",
|
"source": "metadata_management",
|
||||||
"dest": "exit_invalid_menu_option"
|
"dest": "exit_invalid_menu_option"
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -1,6 +1,6 @@
|
|||||||
en:
|
en:
|
||||||
account_successfully_created: |-
|
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: |-
|
received_tokens: |-
|
||||||
Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp}. New balance is %{balance} %{token_symbol}.
|
Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp}. New balance is %{balance} %{token_symbol}.
|
||||||
terms: |-
|
terms: |-
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
sw:
|
sw:
|
||||||
account_successfully_created: |-
|
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: |-
|
received_tokens: |-
|
||||||
Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp}. Salio la %{token_symbol} ni %{balance}.
|
Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp}. Salio la %{token_symbol} ni %{balance}.
|
||||||
terms: |-
|
terms: |-
|
||||||
|
@ -11,6 +11,23 @@ en:
|
|||||||
initial_pin_confirmation: |-
|
initial_pin_confirmation: |-
|
||||||
CON Enter your PIN again
|
CON Enter your PIN again
|
||||||
0. Back
|
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: |-
|
start: |-
|
||||||
CON Balance %{account_balance} %{account_token_name}
|
CON Balance %{account_balance} %{account_token_name}
|
||||||
1. Send
|
1. Send
|
||||||
@ -27,40 +44,23 @@ en:
|
|||||||
1. My profile
|
1. My profile
|
||||||
2. Change language
|
2. Change language
|
||||||
3. Check balance
|
3. Check balance
|
||||||
4. Change PIN
|
4. Check statement
|
||||||
|
5. Change PIN
|
||||||
0. Back
|
0. Back
|
||||||
profile_management: |-
|
metadata_management: |-
|
||||||
CON My profile
|
CON My profile
|
||||||
1. Edit name
|
1. Edit name
|
||||||
2. Edit gender
|
2. Edit gender
|
||||||
3. Edit location
|
3. Edit location
|
||||||
4. Edit business
|
4. Edit products
|
||||||
5. View my profile
|
5. View my profile
|
||||||
0. Back
|
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: |-
|
display_user_profile_data: |-
|
||||||
END Your details are:
|
END Your details are:
|
||||||
Name: %{full_name}
|
Name: %{full_name}
|
||||||
Gender: %{gender}
|
Gender: %{gender}
|
||||||
Location: %{location}
|
Location: %{location}
|
||||||
You sell: %{business_profile}
|
You sell: %{products}
|
||||||
If any details are missing, please use my profile to add your details.
|
|
||||||
0. Back
|
0. Back
|
||||||
select_preferred_language: |-
|
select_preferred_language: |-
|
||||||
CON Choose language
|
CON Choose language
|
||||||
@ -85,48 +85,46 @@ en:
|
|||||||
retry: |-
|
retry: |-
|
||||||
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
||||||
0. Back
|
0. Back
|
||||||
name_management_pin_authorization:
|
standard_pin_authorization:
|
||||||
first: |-
|
first: |-
|
||||||
CON Please enter your PIN.
|
CON Please enter your PIN.
|
||||||
0. Back
|
0. Back
|
||||||
retry: |-
|
retry: |-
|
||||||
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
||||||
0. Back
|
0. Back
|
||||||
gender_management_pin_authorization:
|
account_balances_pin_authorization:
|
||||||
first: |-
|
first: |-
|
||||||
CON Please enter your PIN.
|
CON Please enter your PIN to view balances.
|
||||||
0. Back
|
0. Back
|
||||||
retry: |-
|
retry: |-
|
||||||
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
||||||
0. Back
|
0. Back
|
||||||
location_management_pin_authorization:
|
account_statement_pin_authorization:
|
||||||
first: |-
|
first: |-
|
||||||
CON Please enter your PIN.
|
CON Please enter your PIN to view statement.
|
||||||
0. Back
|
0. Back
|
||||||
retry: |-
|
retry: |-
|
||||||
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
CON Please enter your PIN. You have %{remaining_attempts} attempts remaining.
|
||||||
0. Back
|
0. Back
|
||||||
business_profile_management_pin_authorization:
|
account_balances: |-
|
||||||
first: |-
|
CON Your balances are as follows:
|
||||||
CON Please enter your PIN.
|
balance: %{operational_balance} %{token_symbol}
|
||||||
0. Back
|
taxes: %{tax} %{token_symbol}
|
||||||
retry: |-
|
bonsuses: %{bonus} %{token_symbol}
|
||||||
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.
|
|
||||||
0. Back
|
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: |-
|
exit: |-
|
||||||
END Thank you for using the service.
|
END Thank you for using the service.
|
||||||
exit_invalid_request: |-
|
exit_invalid_request: |-
|
||||||
@ -168,5 +166,5 @@ en:
|
|||||||
CON Your request has been sent. You will receive an SMS shortly.
|
CON Your request has been sent. You will receive an SMS shortly.
|
||||||
00. Back
|
00. Back
|
||||||
99. Exit
|
99. Exit
|
||||||
account_creation_prompt: >
|
account_creation_prompt: |-
|
||||||
Your account is being created. You will receive an SMS when your account is ready.
|
Your account is being created. You will receive an SMS when your account is ready.
|
@ -11,6 +11,23 @@ sw:
|
|||||||
initial_pin_confirmation: |-
|
initial_pin_confirmation: |-
|
||||||
CON Weka PIN yako tena
|
CON Weka PIN yako tena
|
||||||
0. Nyuma
|
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: |-
|
start: |-
|
||||||
CON Salio %{account_balance} %{account_token_name}
|
CON Salio %{account_balance} %{account_token_name}
|
||||||
1. Tuma
|
1. Tuma
|
||||||
@ -27,39 +44,23 @@ sw:
|
|||||||
1. Wasifu wangu
|
1. Wasifu wangu
|
||||||
2. Chagua lugha utakayotumia
|
2. Chagua lugha utakayotumia
|
||||||
3. Angalia salio
|
3. Angalia salio
|
||||||
4. Badilisha nambari ya siri
|
4. Angalia taarifa ya matumizi
|
||||||
|
5. Badilisha nambari ya siri
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
profile_management: |-
|
metadata_management: |-
|
||||||
CON Wasifu wangu
|
CON Wasifu wangu
|
||||||
1. Weka jina
|
1. Weka jina
|
||||||
2. Weka jinsia
|
2. Weka jinsia
|
||||||
3. Weka eneo
|
3. Weka eneo
|
||||||
4. Weka biashara
|
4. Weka bidhaa
|
||||||
|
5. Angalia wasifu wako
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
enter_first_name: |-
|
display_user_metadata: |-
|
||||||
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: |-
|
|
||||||
END Wasifu wako una maelezo yafuatayo:
|
END Wasifu wako una maelezo yafuatayo:
|
||||||
Jina: %{full_name}
|
Jina: %{full_name}
|
||||||
Jinsia: %{gender}
|
Jinsia: %{gender}
|
||||||
Eneo: %{location}
|
Eneo: %{location}
|
||||||
Unauza: %{user_bio}
|
Unauza: %{user_bio}
|
||||||
Iwapo hakuna, enda kwa wasifu wako uweke maelezo zaidi.
|
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
select_preferred_language: |-
|
select_preferred_language: |-
|
||||||
CON Chagua lugha
|
CON Chagua lugha
|
||||||
@ -84,48 +85,46 @@ sw:
|
|||||||
retry: |-
|
retry: |-
|
||||||
CON Weka nambari ya siri. Una majaribio %{remaining_attempts} yaliyobaki.
|
CON Weka nambari ya siri. Una majaribio %{remaining_attempts} yaliyobaki.
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
name_management_pin_authorization:
|
standard_pin_authorization:
|
||||||
first: |-
|
first: |-
|
||||||
CON Tafadhali weka PIN yako.
|
CON Tafadhali weka PIN yako.
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
retry: |-
|
retry: |-
|
||||||
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
|
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
gender_management_pin_authorization:
|
account_balances_pin_authorization:
|
||||||
first: |-
|
first: |-
|
||||||
CON Tafadhali weka PIN yako.
|
CON Tafadhali weka PIN yako kuona salio.
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
retry: |-
|
retry: |-
|
||||||
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
|
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
location_management_pin_authorization:
|
account_statement_pin_authorization:
|
||||||
first: |-
|
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
|
|
||||||
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.
|
|
||||||
0. Nyuma
|
0. Nyuma
|
||||||
retry: |-
|
retry: |-
|
||||||
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
|
CON Tafadhali weka PIN yako. Una majaribio %{remaining_attempts} yaliyobaki.
|
||||||
0. Nyuma
|
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: |-
|
exit: |-
|
||||||
END Asante kwa kutumia huduma.
|
END Asante kwa kutumia huduma.
|
||||||
exit_invalid_request: |-
|
exit_invalid_request: |-
|
||||||
|
Loading…
Reference in New Issue
Block a user