Compare commits

..

16 Commits

Author SHA1 Message Date
3dcfbe59fe Merge branch 'philip/translations-v1' into 'master'
Adds updated translations.

See merge request grassrootseconomics/cic-internal-integration!328
2022-01-10 07:31:04 +00:00
0dd21f3970 Adds updated translations. 2022-01-10 07:31:03 +00:00
6ca8632cde Merge branch 'sohail/help-menu-fix' into 'master'
fix: ussd help menu

See merge request grassrootseconomics/cic-internal-integration!327
2022-01-10 06:56:52 +00:00
b7dc290992 fix: ussd help menu 2022-01-10 06:56:52 +00:00
e69801ea08 Merge branch 'philip/ussd-poler' into 'master'
Philip/ussd poler

See merge request grassrootseconomics/cic-internal-integration!326
2022-01-10 04:44:51 +00:00
50a596e707 Philip/ussd poler 2022-01-10 04:44:50 +00:00
cb61e45e4c Merge branch 'philip/ussd-data-files' into 'master'
Move cic-ussd datafiles to apps/cic-ussd/cic_ussd/data/

See merge request grassrootseconomics/cic-internal-integration!325
2022-01-07 10:50:49 +00:00
e5b06b18d7 Move cic-ussd datafiles to apps/cic-ussd/cic_ussd/data/ 2022-01-07 10:50:49 +00:00
William Luke
5d1a30021a Merge branch 'fix/docs-link' into 'master'
fix: link to docs

See merge request grassrootseconomics/cic-internal-integration!324
2022-01-05 12:22:15 +00:00
William Luke
ade3f4e917 fix: link to docs 2022-01-05 15:19:11 +03:00
c3e924ae8f Merge branch 'sohail/alt-build' into 'master'
feat (devops): add local image build for releases

See merge request grassrootseconomics/cic-internal-integration!322
2022-01-05 08:11:38 +00:00
ff4c42dc24 feat (devops): add local image build for releases 2022-01-05 08:11:38 +00:00
b8cd7eec56 Merge branch 'philip/bump-test-coverage' into 'master'
Rehabilitate test coverage in ussd and cic-notify

See merge request grassrootseconomics/cic-internal-integration!323
2022-01-04 16:51:02 +00:00
837e8da650 Rehabilitate test coverage in ussd and cic-notify 2022-01-04 16:51:02 +00:00
fe3f2c2549 Merge branch 'philip/cleanup-hardening' into 'master'
USSD Hardening and Cleanups

See merge request grassrootseconomics/cic-internal-integration!320
2022-01-04 16:16:01 +00:00
46f25e5678 USSD Hardening and Cleanups 2022-01-04 16:16:00 +00:00
89 changed files with 3285 additions and 1478 deletions

View File

@ -15,5 +15,5 @@ To get started see [./apps/contract-migration/README.md](./apps/contract-migrati
## Documentation ## Documentation
[https://docs.grassecon.org/cic_stack/](https://docs.grassecon.org/cic_stack/) [https://docs.grassecon.org/software/](https://docs.grassecon.org/software/)

View File

@ -17,7 +17,7 @@ from cic_eth_registry.error import UnknownContractError
# local imports # local imports
from cic_eth.error import SeppukuError from cic_eth.error import SeppukuError
from cic_eth.db.models.base import SessionBase from cic_eth.db.models.base import SessionBase
from cic_eth.eth.util import CacheGasOracle from cic_eth.eth.util import CacheGasOracle, MaxGasOracle
#logg = logging.getLogger().getChild(__name__) #logg = logging.getLogger().getChild(__name__)
logg = logging.getLogger() logg = logging.getLogger()
@ -41,21 +41,24 @@ class BaseTask(celery.Task):
def create_gas_oracle(self, conn, address=None, *args, **kwargs): def create_gas_oracle(self, conn, address=None, *args, **kwargs):
if address == None: x = None
return RPCGasOracle( if address is None:
x = RPCGasOracle(
conn, conn,
code_callback=kwargs.get('code_callback', self.get_min_fee_limit), code_callback=kwargs.get('code_callback', self.get_min_fee_limit),
min_price=self.min_fee_price, min_price=self.min_fee_price,
id_generator=kwargs.get('id_generator'), id_generator=kwargs.get('id_generator'),
) )
else:
return CacheGasOracle( x = MaxGasOracle(conn)
conn, x.code_callback = x.get_fee_units
address,
method=kwargs.get('method'), return x
min_price=self.min_fee_price,
id_generator=kwargs.get('id_generator'),
) def get_min_fee_limit(self, code):
return self.min_fee_limit
def get_min_fee_limit(self, code): def get_min_fee_limit(self, code):

View File

@ -1,10 +1,9 @@
[DATABASE] [database]
user = postgres name=cic_notify_test
user=
password= password=
host=localhost host=localhost
port = 5432 port=
name = /tmp/cic-notify.db
#engine = postgresql
#driver = psycopg2
engine=sqlite engine=sqlite
driver=pysqlite driver=pysqlite
debug=0

View File

@ -0,0 +1,7 @@
[report]
omit =
venv/*
scripts/*
cic_notify/db/migrations/*
cic_notify/runnable/*
cic_notify/version.py

View File

@ -3,6 +3,7 @@ import logging
import re import re
# third-party imports # third-party imports
import cic_notify.tasks.sms.db
from celery.app.control import Inspect from celery.app.control import Inspect
import celery import celery
@ -13,45 +14,16 @@ app = celery.current_app
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger() logg = logging.getLogger()
sms_tasks_matcher = r"^(cic_notify.tasks.sms)(\.\w+)?"
re_q = r'^cic-notify'
def get_sms_queue_tasks(app, task_prefix='cic_notify.tasks.sms.'):
host_queues = []
i = Inspect(app=app)
qs = i.active_queues()
for host in qs.keys():
for q in qs[host]:
if re.match(re_q, q['name']):
host_queues.append((host, q['name'],))
task_prefix_len = len(task_prefix)
queue_tasks = []
for (host, queue) in host_queues:
i = Inspect(app=app, destination=[host])
for tasks in i.registered_tasks().values():
for task in tasks:
if len(task) >= task_prefix_len and task[:task_prefix_len] == task_prefix:
queue_tasks.append((queue, task,))
return queue_tasks
class Api: class Api:
# TODO: Implement callback strategy def __init__(self, queue: any = 'cic-notify'):
def __init__(self, queue=None):
""" """
:param queue: The queue on which to execute notification tasks :param queue: The queue on which to execute notification tasks
:type queue: str :type queue: str
""" """
self.queue = queue self.queue = queue
self.sms_tasks = get_sms_queue_tasks(app)
logg.debug('sms tasks {}'.format(self.sms_tasks))
def sms(self, message: str, recipient: str):
def sms(self, message, recipient):
"""This function chains all sms tasks in order to send a message, log and persist said data to disk """This function chains all sms tasks in order to send a message, log and persist said data to disk
:param message: The message to be sent to the recipient. :param message: The message to be sent to the recipient.
:type message: str :type message: str
@ -60,24 +32,9 @@ class Api:
:return: a celery Task :return: a celery Task
:rtype: Celery.Task :rtype: Celery.Task
""" """
signatures = [] s_send = celery.signature('cic_notify.tasks.sms.africastalking.send', [message, recipient], queue=self.queue)
for q in self.sms_tasks: s_log = celery.signature('cic_notify.tasks.sms.log.log', [message, recipient], queue=self.queue)
s_persist_notification = celery.signature(
if not self.queue: 'cic_notify.tasks.sms.db.persist_notification', [message, recipient], queue=self.queue)
queue = q[0] signatures = [s_send, s_log, s_persist_notification]
else: return celery.group(signatures)()
queue = self.queue
signature = celery.signature(
q[1],
[
message,
recipient,
],
queue=queue,
)
signatures.append(signature)
t = celery.group(signatures)()
return t

View File

@ -2,7 +2,7 @@
[alembic] [alembic]
# path to migration scripts # path to migration scripts
script_location = migrations script_location = .
# template used to generate migration files # template used to generate migration files
# file_template = %%(rev)s_%%(slug)s # file_template = %%(rev)s_%%(slug)s
@ -27,28 +27,17 @@ script_location = migrations
# sourceless = false # sourceless = false
# version location specification; this defaults # version location specification; this defaults
# to migrations/versions. When using multiple version # to ./versions. When using multiple version
# directories, initial revisions must be specified with --version-path # directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat migrations/versions # version_locations = %(here)s/bar %(here)s/bat ./versions
# the output encoding used when revision files # the output encoding used when revision files
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = postgres+psycopg2://postgres@localhost/cic-notify sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration # Logging configuration
[loggers] [loggers]
keys = root,sqlalchemy,alembic keys = root,sqlalchemy,alembic

View File

@ -11,7 +11,7 @@ config = context.config
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
# This line sets up loggers basically. # This line sets up loggers basically.
fileConfig(config.config_file_name) fileConfig(config.config_file_name, disable_existing_loggers=True)
# add your model's MetaData object here # add your model's MetaData object here
# for 'autogenerate' support # for 'autogenerate' support
@ -56,8 +56,11 @@ def run_migrations_online():
and associate a connection with the context. and associate a connection with the context.
""" """
connectable = context.config.attributes.get("connection", None)
if connectable is None:
connectable = engine_from_config( connectable = engine_from_config(
config.get_section(config.config_ini_section), context.config.get_section(context.config.config_ini_section),
prefix="sqlalchemy.", prefix="sqlalchemy.",
poolclass=pool.NullPool, poolclass=pool.NullPool,
) )

View File

@ -7,7 +7,7 @@ import celery
celery_app = celery.current_app celery_app = celery.current_app
logg = celery_app.log.get_default_logger() logg = celery_app.log.get_default_logger()
local_logg = logging.getLogger(__name__) local_logg = logging.getLogger()
@celery_app.task @celery_app.task

View File

@ -1,5 +1,9 @@
pytest~=6.0.1 Faker==11.1.0
pytest-celery~=0.0.0a1 faker-e164==0.1.0
pytest-mock~=3.3.1 pytest==6.2.5
pysqlite3~=0.4.3 pytest-celery~=0.0.0
pytest-cov==2.10.1 pytest-mock==3.6.1
pysqlite3~=0.4.6
pytest-cov==3.0.0
pytest-alembic==0.7.0
requests-mock==1.9.3

View File

@ -0,0 +1,28 @@
import pytest
def test_single_head_revision(alembic_runner):
heads = alembic_runner.heads
head_count = len(heads)
assert head_count == 1
def test_upgrade(alembic_runner):
try:
alembic_runner.migrate_up_to("head")
except RuntimeError:
pytest.fail('Failed to upgrade to the head revision.')
def test_up_down_consistency(alembic_runner):
try:
for revision in alembic_runner.history.revisions:
alembic_runner.migrate_up_to(revision)
except RuntimeError:
pytest.fail('Failed to upgrade through each revision individually.')
try:
for revision in reversed(alembic_runner.history.revisions):
alembic_runner.migrate_down_to(revision)
except RuntimeError:
pytest.fail('Failed to downgrade through each revision individually.')

View File

@ -0,0 +1,27 @@
# standard imports
# external imports
from faker import Faker
from faker_e164.providers import E164Provider
# local imports
from cic_notify.db.enum import NotificationStatusEnum, NotificationTransportEnum
from cic_notify.db.models.notification import Notification
# test imports
from tests.helpers.phone import phone_number
def test_notification(init_database):
message = 'Hello world'
recipient = phone_number()
notification = Notification(NotificationTransportEnum.SMS, recipient, message)
init_database.add(notification)
init_database.commit()
notification = init_database.query(Notification).get(1)
assert notification.status == NotificationStatusEnum.UNKNOWN
assert notification.recipient == recipient
assert notification.message == message
assert notification.transport == NotificationTransportEnum.SMS

View File

@ -0,0 +1,38 @@
# standard imports
import os
# third-party imports
# local imports
from cic_notify.db import dsn_from_config
def test_dsn_from_config(load_config):
"""
"""
# 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')}"
# undoes overrides to revert engine and drivers to sqlite
overrides = {
'DATABASE_PASSWORD': '',
'DATABASE_DRIVER': 'pysqlite',
'DATABASE_ENGINE': 'sqlite'
}
load_config.dict_override(dct=overrides, dct_description='Override values to test different db formats.')
# 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")}'

View File

@ -0,0 +1,75 @@
# standard imports
import logging
import os
# external imports
import pytest
import requests_mock
# local imports
from cic_notify.error import NotInitializedError, AlreadyInitializedError, NotificationSendError
from cic_notify.tasks.sms.africastalking import AfricasTalkingNotifier
# test imports
from tests.helpers.phone import phone_number
def test_africas_talking_notifier(africastalking_response, caplog):
caplog.set_level(logging.DEBUG)
with pytest.raises(NotInitializedError) as error:
AfricasTalkingNotifier()
assert str(error.value) == ''
api_key = os.urandom(24).hex()
sender_id = 'bar'
username = 'sandbox'
AfricasTalkingNotifier.initialize(username, api_key, sender_id)
africastalking_notifier = AfricasTalkingNotifier()
assert africastalking_notifier.sender_id == sender_id
assert africastalking_notifier.initiated is True
with pytest.raises(AlreadyInitializedError) as error:
AfricasTalkingNotifier.initialize(username, api_key, sender_id)
assert str(error.value) == ''
with requests_mock.Mocker(real_http=False) as request_mocker:
message = 'Hello world.'
recipient = phone_number()
africastalking_response.get('SMSMessageData').get('Recipients')[0]['number'] = recipient
request_mocker.register_uri(method='POST',
headers={'content-type': 'application/json'},
json=africastalking_response,
url='https://api.sandbox.africastalking.com/version1/messaging',
status_code=200)
africastalking_notifier.send(message, recipient)
assert f'Africastalking response sender-id {africastalking_response}' in caplog.text
africastalking_notifier.sender_id = None
africastalking_notifier.send(message, recipient)
assert f'africastalking response no-sender-id {africastalking_response}' in caplog.text
with pytest.raises(NotificationSendError) as error:
status = 'InvalidPhoneNumber'
status_code = 403
africastalking_response.get('SMSMessageData').get('Recipients')[0]['status'] = status
africastalking_response.get('SMSMessageData').get('Recipients')[0]['statusCode'] = status_code
request_mocker.register_uri(method='POST',
headers={'content-type': 'application/json'},
json=africastalking_response,
url='https://api.sandbox.africastalking.com/version1/messaging',
status_code=200)
africastalking_notifier.send(message, recipient)
assert str(error.value) == f'Sending notification failed due to: {status}'
with pytest.raises(NotificationSendError) as error:
recipients = []
status = 'InsufficientBalance'
africastalking_response.get('SMSMessageData')['Recipients'] = recipients
africastalking_response.get('SMSMessageData')['Message'] = status
request_mocker.register_uri(method='POST',
headers={'content-type': 'application/json'},
json=africastalking_response,
url='https://api.sandbox.africastalking.com/version1/messaging',
status_code=200)
africastalking_notifier.send(message, recipient)
assert str(error.value) == f'Unexpected number of recipients: {len(recipients)}. Status: {status}'

View File

@ -0,0 +1,26 @@
# standard imports
# external imports
import celery
# local imports
from cic_notify.db.enum import NotificationStatusEnum, NotificationTransportEnum
from cic_notify.db.models.notification import Notification
# test imports
from tests.helpers.phone import phone_number
def test_persist_notification(celery_session_worker, init_database):
message = 'Hello world.'
recipient = phone_number()
s_persist_notification = celery.signature(
'cic_notify.tasks.sms.db.persist_notification', (message, recipient)
)
s_persist_notification.apply_async().get()
notification = Notification.session.query(Notification).filter_by(recipient=recipient).first()
assert notification.status == NotificationStatusEnum.UNKNOWN
assert notification.recipient == recipient
assert notification.message == message
assert notification.transport == NotificationTransportEnum.SMS

View File

@ -0,0 +1,21 @@
# standard imports
import logging
# external imports
import celery
# local imports
# test imports
from tests.helpers.phone import phone_number
def test_log(caplog, celery_session_worker):
message = 'Hello world.'
recipient = phone_number()
caplog.set_level(logging.INFO)
s_log = celery.signature(
'cic_notify.tasks.sms.log.log', [message, recipient]
)
s_log.apply_async().get()
assert f'message to {recipient}: {message}' in caplog.text

View File

@ -0,0 +1,24 @@
# standard imports
# external imports
import celery
# local imports
from cic_notify.api import Api
# test imports
from tests.helpers.phone import phone_number
def test_api(celery_session_worker, mocker):
mocked_group = mocker.patch('celery.group')
message = 'Hello world.'
recipient = phone_number()
s_send = celery.signature('cic_notify.tasks.sms.africastalking.send', [message, recipient], queue=None)
s_log = celery.signature('cic_notify.tasks.sms.log.log', [message, recipient], queue=None)
s_persist_notification = celery.signature(
'cic_notify.tasks.sms.db.persist_notification', [message, recipient], queue=None)
signatures = [s_send, s_log, s_persist_notification]
api = Api(queue=None)
api.sms(message, recipient)
mocked_group.assert_called_with(signatures)

View File

@ -1,31 +1,13 @@
# standard imports # standard imports
import sys
import os
import pytest
import logging import logging
# third party imports # third party imports
import confini
script_dir = os.path.dirname(os.path.realpath(__file__))
root_dir = os.path.dirname(script_dir)
sys.path.insert(0, root_dir)
# local imports # local imports
from cic_notify.db.models.base import SessionBase
#from transport.notification import AfricastalkingNotification
# fixtures # test imports
from tests.fixtures_config import *
from tests.fixtures_celery import *
from tests.fixtures_database import *
logg = logging.getLogger() from .fixtures.celery import *
from .fixtures.config import *
from .fixtures.database import *
#@pytest.fixture(scope='session') from .fixtures.result import *
#def africastalking_notification(
# load_config,
# ):
# return AfricastalkingNotificationTransport(load_config)
#

View File

@ -37,12 +37,6 @@ def celery_config():
shutil.rmtree(rq) shutil.rmtree(rq)
@pytest.fixture(scope='session')
def celery_worker_parameters():
return {
# 'queues': ('cic-notify'),
}
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def celery_enable_logging(): def celery_enable_logging():
return True return True

View File

@ -0,0 +1,32 @@
# standard imports
import os
import logging
# external imports
import pytest
from confini import Config
logg = logging.getLogger(__file__)
fixtures_dir = os.path.dirname(__file__)
root_directory = os.path.dirname(os.path.dirname(fixtures_dir))
@pytest.fixture(scope='session')
def alembic_config():
migrations_directory = os.path.join(root_directory, 'cic_notify', 'db', 'migrations', 'default')
file = os.path.join(migrations_directory, 'alembic.ini')
return {
'file': file,
'script_location': migrations_directory
}
@pytest.fixture(scope='session')
def load_config():
config_directory = os.path.join(root_directory, '.config/test')
config = Config(default_dir=config_directory)
config.process()
logg.debug('config loaded\n{}'.format(config))
return config

View File

@ -0,0 +1,54 @@
# standard imports
import os
# third-party imports
import pytest
import alembic
from alembic.config import Config as AlembicConfig
# local imports
from cic_notify.db import dsn_from_config
from cic_notify.db.models.base import SessionBase, create_engine
from .config import root_directory
@pytest.fixture(scope='session')
def alembic_engine(load_config):
data_source_name = dsn_from_config(load_config)
return create_engine(data_source_name)
@pytest.fixture(scope='session')
def database_engine(load_config):
if load_config.get('DATABASE_ENGINE') == 'sqlite':
try:
os.unlink(load_config.get('DATABASE_NAME'))
except FileNotFoundError:
pass
dsn = dsn_from_config(load_config)
SessionBase.connect(dsn)
return dsn
@pytest.fixture(scope='function')
def init_database(load_config, database_engine):
db_directory = os.path.join(root_directory, 'cic_notify', 'db')
migrations_directory = os.path.join(db_directory, 'migrations', load_config.get('DATABASE_ENGINE'))
if not os.path.isdir(migrations_directory):
migrations_directory = os.path.join(db_directory, 'migrations', 'default')
session = SessionBase.create_session()
alembic_config = AlembicConfig(os.path.join(migrations_directory, 'alembic.ini'))
alembic_config.set_main_option('sqlalchemy.url', database_engine)
alembic_config.set_main_option('script_location', migrations_directory)
alembic.command.downgrade(alembic_config, 'base')
alembic.command.upgrade(alembic_config, 'head')
yield session
session.commit()
session.close()

View File

@ -0,0 +1,24 @@
# standard imports
# external imports
import pytest
# local imports
# test imports
@pytest.fixture(scope="function")
def africastalking_response():
return {
"SMSMessageData": {
"Message": "Sent to 1/1 Total Cost: KES 0.8000",
"Recipients": [{
"statusCode": 101,
"number": "+254711XXXYYY",
"status": "Success",
"cost": "KES 0.8000",
"messageId": "ATPid_SampleTxnId123"
}]
}
}

View File

@ -1,20 +0,0 @@
# standard imports
import os
import logging
# third-party imports
import pytest
import confini
script_dir = os.path.dirname(os.path.realpath(__file__))
root_dir = os.path.dirname(script_dir)
logg = logging.getLogger(__file__)
@pytest.fixture(scope='session')
def load_config():
config_dir = os.path.join(root_dir, '.config/test')
conf = confini.Config(config_dir, 'CICTEST')
conf.process()
logg.debug('config {}'.format(conf))
return conf

View File

@ -1,48 +0,0 @@
# standard imports
import os
# third-party imports
import pytest
import alembic
from alembic.config import Config as AlembicConfig
# local imports
from cic_notify.db import SessionBase
from cic_notify.db import dsn_from_config
@pytest.fixture(scope='session')
def database_engine(
load_config,
):
dsn = dsn_from_config(load_config)
SessionBase.connect(dsn)
return dsn
@pytest.fixture(scope='function')
def init_database(
load_config,
database_engine,
):
rootdir = os.path.dirname(os.path.dirname(__file__))
dbdir = os.path.join(rootdir, 'cic_notify', 'db')
migrationsdir = os.path.join(dbdir, 'migrations', load_config.get('DATABASE_ENGINE'))
if not os.path.isdir(migrationsdir):
migrationsdir = os.path.join(dbdir, 'migrations', 'default')
session = SessionBase.create_session()
ac = AlembicConfig(os.path.join(migrationsdir, 'alembic.ini'))
ac.set_main_option('sqlalchemy.url', database_engine)
ac.set_main_option('script_location', migrationsdir)
alembic.command.downgrade(ac, 'base')
alembic.command.upgrade(ac, 'head')
yield session
session.commit()
session.close()

View File

@ -0,0 +1,16 @@
# standard imports
# external imports
from faker import Faker
from faker_e164.providers import E164Provider
# local imports
# test imports
fake = Faker()
fake.add_provider(E164Provider)
def phone_number() -> str:
return fake.e164('KE')

View File

@ -1,34 +0,0 @@
# standard imports
import json
# third party imports
import pytest
import celery
# local imports
from cic_notify.tasks.sms import db
from cic_notify.tasks.sms import log
def test_log_notification(
celery_session_worker,
):
recipient = '+25412121212'
content = 'bar'
s_log = celery.signature('cic_notify.tasks.sms.log.log')
t = s_log.apply_async(args=[recipient, content])
r = t.get()
def test_db_notification(
init_database,
celery_session_worker,
):
recipient = '+25412121213'
content = 'foo'
s_db = celery.signature('cic_notify.tasks.sms.db.persist_notification')
t = s_db.apply_async(args=[recipient, content])
r = t.get()

View File

@ -4,3 +4,4 @@ omit =
scripts/* scripts/*
cic_ussd/db/migrations/* cic_ussd/db/migrations/*
cic_ussd/runnable/* cic_ussd/runnable/*
cic_ussd/version.py

View File

@ -14,7 +14,7 @@ from cic_ussd.account.chain import Chain
from cic_ussd.cache import cache_data, cache_data_key, get_cached_data from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError, SeppukuError from cic_ussd.error import CachedDataNotFoundError, SeppukuError
from cic_ussd.metadata.tokens import query_token_info, query_token_metadata from cic_ussd.metadata.tokens import query_token_info, query_token_metadata
from cic_ussd.processor.util import wait_for_cache from cic_ussd.processor.poller import wait_for_cache
logg = logging.getLogger(__file__) logg = logging.getLogger(__file__)

View File

@ -14,7 +14,7 @@ class Cache:
store: Redis = None store: Redis = None
def cache_data(key: str, data: str): def cache_data(key: str, data: [bytes, float, int, str]):
""" """
:param key: :param key:
:type key: :type key:

View File

@ -0,0 +1,9 @@
+254707628499
+254757628885
+254757628900
+254792048646
+254792048228
+254792048490
+254792048902
+254727806655
+254790079966

View File

@ -0,0 +1,19 @@
keys,en,sw,kam,kik,miji,luo,bor
female,Female,Mwanamke,Mundumuka,Mutumia,Muche,Dhako,Uwole
from,From,Kutoka kwa,Kuma kwa,Kuuma kwa,Ulaako,Kowuok kuom,ira
male,Male,Mwanaume,Mundume,Mundurume,Mulume,Dichuo,Dir
not_provided,Not provided,Haijawekwa,Inenganitwe,Ndiikiritwo,Kaphana,Okoketi,Kes inkan
no_language_list,No language list,Hamna lugha ya kuchagua,Vai luka ya kusakwa,Hatire ruthiomi rwakucagurwo,Kahana luga irio orodeshwa,Onge dhok miyiero,Afaan chaguad injirt
no_transaction_history,No transaction history,Hamna ripoti ya matumizi,Vai livoti ya utumii,Hatire riboti ya mahuthira,Kahana repoti ya mahumizi,Onge ripot mar tiyo,Odhuu jalkaban injirt
no_tokens_list,No more Sarafu,Hamna sarafu zingine,Vai Sarafu ingi,Hatire Sarafu inge,Kahana Sarafu zaidi,Onge Sarafu moko,Sarafu dibii injirt
other,Other,Nyingine,Ingi,Inge,Nyinjine,Moko,Ta dibii
received,Received,Ulipokea,Niwakwatie,Niuramukirire ,Hokera,Niyudo,Argat
sent,Sent,Ulituma,Niwatumie,Niuratumire,Humwa,Nioro,Ergan
to,To,Kwa,Kwa,Hare,Kwa,Ne,Es
guardians_list_header,Your PIN guards are:,PIN Walinzi uliowaongeza ni:,PIN Atetheesya ala wongelile ni:,Agiteri a PIN yaku ni:,PIN Aimirizi urioika ni:,PIN Jorit magi gin:,PIN Naam at korkorad:
no_guardians_list,No PIN guardians set,Hamna PIN walinzi walioongezwa,Vai atetheesya mongelwa,Hartire agiteri meekeretwo,Kahana aimirizi adzoikwa,Onge jorit moketi,Nam an korkorad injirt
error.no_phone_number_provided,No phone number was provided,Nambari ya simu haijawekwa,Namba ya simu inaikiwa,Namba ya thimu ndihianetwo,Kahana namba ya simu idzopewa,Namba mar simu okoketi,Namba simu kees inkaan
error.no_matching_account,The number provided is not registered,Nambari uliyoweka haijasajiliwa,Namba ya simu ila wekiya ti mbandikithye,Namba iria wekera ndiandekithetwo,Namba idzopewa kaidzagwe kusajiliwa,Namba mar simu miketo pok ondiki,Namba ka at kekeet sajiil incab
error.is_initiator,Phone number cannot be your own,Nambari yafaa kuwa tofauti na yako,Namba ya simu yaile ithiwa itavwanene na yaku,Namba ifatie gukorwo ina utiganu na yaku,Namba yasimu kaidima kukala niyako,Namba onego obed mopogre gimari,Namba simu tete tau mal
error.is_existent_guardian,This phone number is already added as a PIN guardian,Nambari hii tayari imeongezwa kama mlinzi wa nambari ya siri,Namba ii niyongeletwe tayari ta mutethesya wa kusovya pin,Namba ino niyongereirwo ta murugamereri ya namba ya thiri,Nambari ii yasimu yaikwa kare Muimirizi,Nambani oseketi kaka jarit,Namba tana yayu nam korkoradi taat
error.is_not_existent_guardian,Phone number not set as PIN guardian,Nambari hii haijaongezwa kama mlinzi wa nambari ya PIN,Namba ii iyongeletwe ta mutethesya wa kusovya PIN,Namba ino ndiongereirwo ta mugiteri wa PIN,Nambari ii yasimu kaiikika kugaluza PIN zda mwimirizi,Nambani pok omed kaka jarit,Namba simu ta nam korkorad indharan
1 keys en sw kam kik miji luo bor
2 female Female Mwanamke Mundumuka Mutumia Muche Dhako Uwole
3 from From Kutoka kwa Kuma kwa Kuuma kwa Ulaako Kowuok kuom ira
4 male Male Mwanaume Mundume Mundurume Mulume Dichuo Dir
5 not_provided Not provided Haijawekwa Inenganitwe Ndiikiritwo Kaphana Okoketi Kes inkan
6 no_language_list No language list Hamna lugha ya kuchagua Vai luka ya kusakwa Hatire ruthiomi rwakucagurwo Kahana luga irio orodeshwa Onge dhok miyiero Afaan chaguad injirt
7 no_transaction_history No transaction history Hamna ripoti ya matumizi Vai livoti ya utumii Hatire riboti ya mahuthira Kahana repoti ya mahumizi Onge ripot mar tiyo Odhuu jalkaban injirt
8 no_tokens_list No more Sarafu Hamna sarafu zingine Vai Sarafu ingi Hatire Sarafu inge Kahana Sarafu zaidi Onge Sarafu moko Sarafu dibii injirt
9 other Other Nyingine Ingi Inge Nyinjine Moko Ta dibii
10 received Received Ulipokea Niwakwatie Niuramukirire Hokera Niyudo Argat
11 sent Sent Ulituma Niwatumie Niuratumire Humwa Nioro Ergan
12 to To Kwa Kwa Hare Kwa Ne Es
13 guardians_list_header Your PIN guards are: PIN Walinzi uliowaongeza ni: PIN Atetheesya ala wongelile ni: Agiteri a PIN yaku ni: PIN Aimirizi urioika ni: PIN Jorit magi gin: PIN Naam at korkorad:
14 no_guardians_list No PIN guardians set Hamna PIN walinzi walioongezwa Vai atetheesya mongelwa Hartire agiteri meekeretwo Kahana aimirizi adzoikwa Onge jorit moketi Nam an korkorad injirt
15 error.no_phone_number_provided No phone number was provided Nambari ya simu haijawekwa Namba ya simu inaikiwa Namba ya thimu ndihianetwo Kahana namba ya simu idzopewa Namba mar simu okoketi Namba simu kees inkaan
16 error.no_matching_account The number provided is not registered Nambari uliyoweka haijasajiliwa Namba ya simu ila wekiya ti mbandikithye Namba iria wekera ndiandekithetwo Namba idzopewa kaidzagwe kusajiliwa Namba mar simu miketo pok ondiki Namba ka at kekeet sajiil incab
17 error.is_initiator Phone number cannot be your own Nambari yafaa kuwa tofauti na yako Namba ya simu yaile ithiwa itavwanene na yaku Namba ifatie gukorwo ina utiganu na yaku Namba yasimu kaidima kukala niyako Namba onego obed mopogre gimari Namba simu tete tau mal
18 error.is_existent_guardian This phone number is already added as a PIN guardian Nambari hii tayari imeongezwa kama mlinzi wa nambari ya siri Namba ii niyongeletwe tayari ta mutethesya wa kusovya pin Namba ino niyongereirwo ta murugamereri ya namba ya thiri Nambari ii yasimu yaikwa kare Muimirizi Nambani oseketi kaka jarit Namba tana yayu nam korkoradi taat
19 error.is_not_existent_guardian Phone number not set as PIN guardian Nambari hii haijaongezwa kama mlinzi wa nambari ya PIN Namba ii iyongeletwe ta mutethesya wa kusovya PIN Namba ino ndiongereirwo ta mugiteri wa PIN Nambari ii yasimu kaiikika kugaluza PIN zda mwimirizi Nambani pok omed kaka jarit Namba simu ta nam korkorad indharan

View File

@ -0,0 +1,7 @@
keys,en,sw,kam,kik,miji,luo,bor
account_successfully_created,You have been registered on Sarafu Network! To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone},Umesajiliwa kwa Sarafu Network! Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone},Niwayandikithya na Sarafu Network! Safaricom kuna namba ii *384*96# mitandao ingi *483*96#. Utethyo ungi kuna %{support_phone},Niweyandekithia kwe Sarafu Network! Kuhuthira hihinya *384*96# he Safaricom na *483*46# he mitambo ingi Uteithio %{support_phone},Usajiliwa Sarafu Network! kuhumira hopya *384*96# Saf *483*96# mtandao mnjine. Kuvizwa %{support_phone},Osendiki e Sarafu Network! Kidwatiyogo to dii *384*96# Safaricom kata *483*46# e netwak mamoko. Kuom kony %{support_phone},Yaayu sirejestan Sarafu Network! Kuches *384*96# Safaricom *483*46# Airtel
received_tokens,Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp} to %{tx_recipient_information} Balance %{balance} %{token_symbol},Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp} kuendea %{tx_recipient_information} Salio %{balance} %{token_symbol},Niwakwata %{amount} %{token_symbol} kuma %{tx_sender_information} %{timestamp} kuvikia %{tx_recipient_information} Mbalansi %{balance} %{token_symbol},Wamukira %{amount} %{token_symbol} kuuma kwa %{tx_sender_information} %{timestamp} to %{tx_recipient_information} Watigaria %{balance} %{token_symbol},Uphokera %{amount} %{token_symbol} kula %{tx_sender_information} %{timestamp} Kwenda %{tx_recipient_information}. Sazoro %{balance} %{token_symbol},Iyudo %{amount} %{token_symbol} kowuok kuom %{tx_sender_information} %{timestamp} odhi ne %{tx_recipient_information}. Dong mari en %{balance} %{token_symbol},Yaargat %{amount} %{token_symbol} ira %{tx_sender_information} %{timestamp} Es %{tx_recipient_information} Balansi %{balance} %{token_symbol}
sent_tokens,Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp} from %{tx_sender_information} Balance %{balance} %{token_symbol},Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kutoka kwa %{tx_sender_information} Salio %{balance} %{token_symbol},Niwatuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kuma %{tx_sender_information} Mbalansi %{balance} %{token_symbol}.,Watuma %{amount} %{token_symbol} kwe %{tx_recipient_information} %{timestamp} kuuma %{tx_sender_information} Watigaria %{balance} %{token_symbol},Uhuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kula %{tx_sender_information} Sazoro %{balance} %{token_symbol},Ioro %{amount} %{token_symbol} ne %{tx_recipient_information} %{timestamp} kowuok kuom %{tx_sender_information}. Dong mari en %{balance} %{token_symbol},yaergat %{amount} %{token_symbol} Es %{tx_recipient_information} %{timestamp} ira %{tx_sender_information} Balansi hareetin %{balance} %{token_symbol}
terms,By using the service you agree to the terms and conditions at http://grassecon.org/tos,Kwa kutumia hii huduma umekubali sheria na masharti yafuatayo http://grassecon.org/tos,Kwa kutumia mutandao uu niwetikilana na miyao na masharti ma http://grassecon.org/tos,"Kuhuthira mitambo ino , niuraetekania na mawatho na mutaratara wa http://grassecon.org/tos","Kuhumira huduma,Ukubali sheria na malagizo http://grassecon.org/tos ",Kuom tiyo gi huduma ni iyie chike kod weche mantie http://grassecon.org/tos,Oja service tun tumiith yaayuu kubalt one chuf at http://grassecon.org/tos
upsell_unregistered_recipient,%{tx_sender_information} tried to send you %{token_symbol}. Dial *384*96# on Safaricom and *483*96# on others For help %{support_phone},%{tx_sender_information} amejaribu kutuma %{token_symbol} na hujasajili. Bonyeza*384*96# Saf au*483*46# kwa mitandao tofauti. Usaidizi %{support_phone},%{tx_sender_information} niwatata kuutumia %{token_symbol} lakini ndwimwandikithye. Safaricom kuna *384*96# laini ingi *483*96# Utethyo %{support_phone},%{tx_sender_information} ekugeretie gugutumira %{token_symbol} no ndeyandikithetie. Hihinya *384*96# he Safaricom na *483*96# mitambo ingi. Uteithio %{support_phone},%{tx_sender_information} Yuhuma %{token_symbol} Kudzasajiliwa. Humira hopya *384*96# Safaricom au *483*96# mtandao mnjine. Kuvizwa %{support_phone},%{tx_sender_information} otemo oro ni %{token_symbol} to pok ondiki. Tiyo go dii *384*96# Safaricom gi *483*96# e netwak mamoko. E kony %{support_phone},%{tx_sender_information} yaa si ergu jariib %{token_symbol} ammo atin insajilan.Tumiitu kuches *384*96# Safaricom *483*96# dibii Qarqars %{support_phone}
pin_reset_initiated,%{pin_initiator} has sent a request to initiate your PIN reset,%{pin_initiator} ametuma ombi la kubadilisha PIN yako,%{pin_initiator} niwatuma wendi waku wa kwambiisya kusovya PIN yaku,%{pin_initiator} Niatuma ihoya ria guchengia PIN yaku,%{pin_initiator} yuhuma voyo kurekebisha piniyo.,%{pin_initiator} ooro kwayo mar loko nambani mopondo,%{pin_initiator} pin Tate badilishadu feet
1 keys en sw kam kik miji luo bor
2 account_successfully_created You have been registered on Sarafu Network! To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone} Umesajiliwa kwa Sarafu Network! Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone} Niwayandikithya na Sarafu Network! Safaricom kuna namba ii *384*96# mitandao ingi *483*96#. Utethyo ungi kuna %{support_phone} Niweyandekithia kwe Sarafu Network! Kuhuthira hihinya *384*96# he Safaricom na *483*46# he mitambo ingi Uteithio %{support_phone} Usajiliwa Sarafu Network! kuhumira hopya *384*96# Saf *483*96# mtandao mnjine. Kuvizwa %{support_phone} Osendiki e Sarafu Network! Kidwatiyogo to dii *384*96# Safaricom kata *483*46# e netwak mamoko. Kuom kony %{support_phone} Yaayu sirejestan Sarafu Network! Kuches *384*96# Safaricom *483*46# Airtel
3 received_tokens Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp} to %{tx_recipient_information} Balance %{balance} %{token_symbol} Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp} kuendea %{tx_recipient_information} Salio %{balance} %{token_symbol} Niwakwata %{amount} %{token_symbol} kuma %{tx_sender_information} %{timestamp} kuvikia %{tx_recipient_information} Mbalansi %{balance} %{token_symbol} Wamukira %{amount} %{token_symbol} kuuma kwa %{tx_sender_information} %{timestamp} to %{tx_recipient_information} Watigaria %{balance} %{token_symbol} Uphokera %{amount} %{token_symbol} kula %{tx_sender_information} %{timestamp} Kwenda %{tx_recipient_information}. Sazoro %{balance} %{token_symbol} Iyudo %{amount} %{token_symbol} kowuok kuom %{tx_sender_information} %{timestamp} odhi ne %{tx_recipient_information}. Dong mari en %{balance} %{token_symbol} Yaargat %{amount} %{token_symbol} ira %{tx_sender_information} %{timestamp} Es %{tx_recipient_information} Balansi %{balance} %{token_symbol}
4 sent_tokens Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp} from %{tx_sender_information} Balance %{balance} %{token_symbol} Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kutoka kwa %{tx_sender_information} Salio %{balance} %{token_symbol} Niwatuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kuma %{tx_sender_information} Mbalansi %{balance} %{token_symbol}. Watuma %{amount} %{token_symbol} kwe %{tx_recipient_information} %{timestamp} kuuma %{tx_sender_information} Watigaria %{balance} %{token_symbol} Uhuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kula %{tx_sender_information} Sazoro %{balance} %{token_symbol} Ioro %{amount} %{token_symbol} ne %{tx_recipient_information} %{timestamp} kowuok kuom %{tx_sender_information}. Dong mari en %{balance} %{token_symbol} yaergat %{amount} %{token_symbol} Es %{tx_recipient_information} %{timestamp} ira %{tx_sender_information} Balansi hareetin %{balance} %{token_symbol}
5 terms By using the service you agree to the terms and conditions at http://grassecon.org/tos Kwa kutumia hii huduma umekubali sheria na masharti yafuatayo http://grassecon.org/tos Kwa kutumia mutandao uu niwetikilana na miyao na masharti ma http://grassecon.org/tos Kuhuthira mitambo ino , niuraetekania na mawatho na mutaratara wa http://grassecon.org/tos Kuhumira huduma,Ukubali sheria na malagizo http://grassecon.org/tos Kuom tiyo gi huduma ni iyie chike kod weche mantie http://grassecon.org/tos Oja service tun tumiith yaayuu kubalt one chuf at http://grassecon.org/tos
6 upsell_unregistered_recipient %{tx_sender_information} tried to send you %{token_symbol}. Dial *384*96# on Safaricom and *483*96# on others For help %{support_phone} %{tx_sender_information} amejaribu kutuma %{token_symbol} na hujasajili. Bonyeza*384*96# Saf au*483*46# kwa mitandao tofauti. Usaidizi %{support_phone} %{tx_sender_information} niwatata kuutumia %{token_symbol} lakini ndwimwandikithye. Safaricom kuna *384*96# laini ingi *483*96# Utethyo %{support_phone} %{tx_sender_information} ekugeretie gugutumira %{token_symbol} no ndeyandikithetie. Hihinya *384*96# he Safaricom na *483*96# mitambo ingi. Uteithio %{support_phone} %{tx_sender_information} Yuhuma %{token_symbol} Kudzasajiliwa. Humira hopya *384*96# Safaricom au *483*96# mtandao mnjine. Kuvizwa %{support_phone} %{tx_sender_information} otemo oro ni %{token_symbol} to pok ondiki. Tiyo go dii *384*96# Safaricom gi *483*96# e netwak mamoko. E kony %{support_phone} %{tx_sender_information} yaa si ergu jariib %{token_symbol} ammo atin insajilan.Tumiitu kuches *384*96# Safaricom *483*96# dibii Qarqars %{support_phone}
7 pin_reset_initiated %{pin_initiator} has sent a request to initiate your PIN reset %{pin_initiator} ametuma ombi la kubadilisha PIN yako %{pin_initiator} niwatuma wendi waku wa kwambiisya kusovya PIN yaku %{pin_initiator} Niatuma ihoya ria guchengia PIN yaku %{pin_initiator} yuhuma voyo kurekebisha piniyo. %{pin_initiator} ooro kwayo mar loko nambani mopondo %{pin_initiator} pin Tate badilishadu feet

File diff suppressed because it is too large Load Diff

View File

@ -63,10 +63,7 @@ class Account(SessionBase):
def remove_guardian(self, phone_number: str): def remove_guardian(self, phone_number: str):
set_guardians = self.guardians.split(',') set_guardians = self.guardians.split(',')
set_guardians.remove(phone_number) set_guardians.remove(phone_number)
if len(set_guardians) > 1:
self.guardians = ','.join(set_guardians) self.guardians = ','.join(set_guardians)
else:
self.guardians = set_guardians[0]
def get_guardians(self) -> list: def get_guardians(self) -> list:
return self.guardians.split(',') if self.guardians else [] return self.guardians.split(',') if self.guardians else []

View File

@ -52,4 +52,5 @@ class UnknownUssdRecipient(Exception):
"""Raised when a recipient of a transaction is not known to the ussd application.""" """Raised when a recipient of a transaction is not known to the ussd application."""
class MaxRetryReached(Exception):
"""Raised when the maximum number of retries defined for polling for the availability of a resource."""

View File

@ -7,3 +7,4 @@ from .custom import CustomMetadata
from .person import PersonMetadata from .person import PersonMetadata
from .phone import PhonePointerMetadata from .phone import PhonePointerMetadata
from .preferences import PreferencesMetadata from .preferences import PreferencesMetadata
from .tokens import TokenMetadata

View File

@ -31,7 +31,8 @@ from cic_ussd.cache import cache_data_key, cache_data, get_cached_data
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.phone_number import Support from cic_ussd.phone_number import Support
from cic_ussd.processor.util import parse_person_metadata, ussd_menu_list, wait_for_session_data from cic_ussd.processor.poller import wait_for_session_data
from cic_ussd.processor.util import parse_person_metadata, ussd_menu_list
from cic_ussd.session.ussd_session import save_session_data from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for

View File

@ -0,0 +1,104 @@
# standard imports
import logging
import time
from queue import Queue
from typing import Callable, Dict, Optional, Tuple, Union
# external imports
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.error import MaxRetryReached
logg = logging.getLogger()
# adapted from https://github.com/justiniso/polling/blob/master/polling.py
# opted not to use the package to reduce dependency
def poller(args: Optional[Tuple],
interval: int,
kwargs: Optional[Dict],
max_retry: int,
target: Callable[..., Union[Dict, str]]):
""""""
collected_values: list = []
expected_value = None
tries = 0
while True:
if tries >= max_retry:
raise MaxRetryReached(collected_values, expected_value)
try:
if args:
value = target(*args)
elif kwargs:
value = target(**kwargs)
else:
value = target()
expected_value = value
except () as error:
expected_value = error
else:
if bool(value) or value == {}:
logg.debug(f'Resource: {expected_value} now available.')
break
collected_values.append(expected_value)
logg.debug(f'Collected values are: {collected_values}')
tries += 1
time.sleep(interval)
def wait_for_cache(identifier: Union[list, bytes],
resource_name: str,
salt: MetadataPointer,
interval: int = 1,
max_retry: int = 5):
"""
:param identifier:
:type identifier:
:param interval:
:type interval:
:param resource_name:
:type resource_name:
:param salt:
:type salt:
:param max_retry:
:type max_retry:
:return:
:rtype:
"""
key: str = cache_data_key(identifier=identifier, salt=salt)
logg.debug(f'Polling for resource: {resource_name} at: {key} every: {interval} second(s) for {max_retry} seconds.')
poller(args=(key,), interval=interval, kwargs=None, max_retry=max_retry, target=get_cached_data)
def wait_for_session_data(resource_name: str,
session_data_key: str,
ussd_session: dict,
interval: int = 1,
max_retry: int = 5):
"""
:param interval:
:type interval:
:param resource_name:
:type resource_name:
:param session_data_key:
:type session_data_key:
:param ussd_session:
:type ussd_session:
:param max_retry:
:type max_retry:
:return:
:rtype:
"""
# poll for data element first
logg.debug(f'Data poller with max retry at: {max_retry}. Checking for every: {interval} seconds.')
poller(args=('data',), interval=interval, kwargs=None, max_retry=max_retry, target=ussd_session.get)
# poll for session data element
get_session_data = ussd_session.get('data').get
logg.debug(f'Session data poller for: {resource_name} with max retry at: {max_retry}. Checking for every: {interval} seconds.')
poller(args=(session_data_key,), interval=interval, kwargs=None, max_retry=max_retry, target=get_session_data)

View File

@ -102,77 +102,3 @@ def ussd_menu_list(fallback: str, menu_list: list, split: int = 3) -> List[str]:
except IndexError: except IndexError:
menu_list_reprs.append(fallback) menu_list_reprs.append(fallback)
return menu_list_reprs return menu_list_reprs
def wait_for_cache(identifier: Union[list, bytes], resource_name: str, salt: MetadataPointer, interval: int = 1, max_retry: int = 5):
"""
:param identifier:
:type identifier:
:param interval:
:type interval:
:param resource_name:
:type resource_name:
:param salt:
:type salt:
:param max_retry:
:type max_retry:
:return:
:rtype:
"""
key = cache_data_key(identifier=identifier, salt=salt)
resource = get_cached_data(key)
counter = 0
while resource is None:
logg.debug(f'Waiting for: {resource_name} at: {key}. Checking after: {interval} ...')
time.sleep(interval)
counter += 1
resource = get_cached_data(key)
if resource is not None:
logg.debug(f'{resource_name} now available.')
break
else:
if counter == max_retry:
logg.debug(f'Could not find: {resource_name} within: {max_retry}')
break
def wait_for_session_data(resource_name: str, session_data_key: str, ussd_session: dict, interval: int = 1, max_retry: int = 5):
"""
:param interval:
:type interval:
:param resource_name:
:type resource_name:
:param session_data_key:
:type session_data_key:
:param ussd_session:
:type ussd_session:
:param max_retry:
:type max_retry:
:return:
:rtype:
"""
data = ussd_session.get('data')
data_poller = 0
while not data:
logg.debug(f'Waiting for data object on ussd session: {ussd_session.get("external_session_id")}')
logg.debug(f'Data poller at: {data_poller}. Checking again after: {interval} secs...')
time.sleep(interval)
data_poller += 1
if data:
logg.debug(f'Data object found, proceeding to poll for: {session_data_key}')
break
if data:
session_data_poller = 0
session_data = data.get(session_data_key)
while not session_data_key:
logg.debug(
f'Session data poller at: {data_poller} with max retry at: {max_retry}. Checking again after: {interval} secs...')
time.sleep(interval)
session_data_poller += 1
if session_data:
logg.debug(f'{resource_name} now available.')
break
elif session_data_poller >= max_retry:
logg.debug(f'Could not find data object within: {max_retry}')

View File

@ -11,7 +11,7 @@ from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.cache import cache_data_key, get_cached_data from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.processor.util import wait_for_cache, wait_for_session_data from cic_ussd.processor.poller import wait_for_cache, wait_for_session_data
from cic_ussd.session.ussd_session import save_session_data from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.translation import Languages from cic_ussd.translation import Languages

View File

@ -15,7 +15,7 @@ from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.enum import AccountStatus from cic_ussd.db.enum import AccountStatus
from cic_ussd.encoder import create_password_hash, check_password_hash from cic_ussd.encoder import create_password_hash, check_password_hash
from cic_ussd.processor.util import wait_for_session_data from cic_ussd.processor.poller import wait_for_session_data
from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session

View File

@ -87,7 +87,7 @@ def is_valid_guardian_addition(state_machine_data: Tuple[str, dict, Account, Ses
guardianship = Guardianship() guardianship = Guardianship()
is_system_guardian = guardianship.is_system_guardian(phone_number) is_system_guardian = guardianship.is_system_guardian(phone_number)
is_initiator = phone_number == account.phone_number is_initiator = phone_number == account.phone_number
is_existent_guardian = phone_number in account.get_guardians() is_existent_guardian = phone_number in account.get_guardians() or is_system_guardian
failure_reason = '' failure_reason = ''
if not is_valid_account: if not is_valid_account:

View File

@ -7,7 +7,7 @@ from sqlalchemy.orm.session import Session
# local imports # local imports
from cic_ussd.account.tokens import set_active_token from cic_ussd.account.tokens import set_active_token
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.processor.util import wait_for_session_data from cic_ussd.processor.poller import wait_for_session_data
from cic_ussd.session.ussd_session import save_session_data from cic_ussd.session.ussd_session import save_session_data

View File

@ -15,7 +15,7 @@ from cic_ussd.cache import Cache, cache_data, cache_data_key, get_cached_data
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.db.models.base import SessionBase from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.processor.util import wait_for_cache from cic_ussd.processor.poller import wait_for_cache
from cic_ussd.account.statement import filter_statement_transactions from cic_ussd.account.statement import filter_statement_transactions
from cic_ussd.account.transaction import transaction_actors from cic_ussd.account.transaction import transaction_actors
from cic_ussd.account.tokens import (collate_token_metadata, from cic_ussd.account.tokens import (collate_token_metadata,

View File

@ -13,4 +13,4 @@ port =
ssl = ssl =
[system] [system]
guardians_file = var/lib/sys/guardians.txt guardians_file = cic_ussd/data/sys/guardians.txt

View File

@ -6,3 +6,6 @@ password_pepper=QYbzKff6NhiQzY3ygl2BkiKOpER8RE/Upqs/5aZWW+I=
[machine] [machine]
states=states/ states=states/
transitions=transitions/ transitions=transitions/
[system]
guardians_file = cic_ussd/data/sys/guardians.txt

View File

@ -1,2 +1,2 @@
[chain] [chain]
spec = 'evm:foo:1:bar' spec = evm:foo:1:bar

View File

@ -1,3 +1,10 @@
[locale] [locale]
fallback=sw fallback=sw
path=var/lib/locale/ path=cic_ussd/data/locale/
file_builders=cic_ussd/data/sys/
[schema]
file_path = data/schema
[languages]
file = cic_ussd/data/sys/languages.json

View File

@ -1,5 +1,5 @@
[ussd] [ussd]
menu_file=cic_ussd/db/ussd_menu.json menu_file=cic_ussd/data/sys/ussd_menu.json
service_code=*483*46#,*483*061#,*384*96# service_code=*483*46#,*483*061#,*384*96#
user = user =
pass = pass =

View File

@ -1,10 +1,10 @@
[locale] [locale]
fallback=sw fallback=sw
path=var/lib/locale/ path=cic_ussd/data/locale/
file_builders=var/lib/sys/ file_builders=cic_ussd/data/sys/
[schema] [schema]
file_path = /usr/local/lib/python3.8/site-packages/cic_translations/data/schema file_path = /usr/local/lib/python3.8/site-packages/cic_translations/data/schema
[languages] [languages]
file = var/lib/sys/languages.json file = cic_ussd/data/sys/languages.json

View File

@ -1,5 +1,5 @@
[ussd] [ussd]
menu_file=data/ussd_menu.json menu_file=cic_ussd/data/sys/ussd_menu.json
service_code=*483*46#,*483*061#,*384*96# service_code=*483*46#,*483*061#,*384*96#
user = user =
pass = pass =

View File

@ -3,12 +3,10 @@ ARG DOCKER_REGISTRY="registry.gitlab.com/grassrootseconomics"
FROM $DOCKER_REGISTRY/cic-base-images:python-3.8.6-dev-e8eb2ee2 FROM $DOCKER_REGISTRY/cic-base-images:python-3.8.6-dev-e8eb2ee2
RUN apt-get install -y redis-server RUN apt-get install -y redis-server
# create secrets directory # create secrets directory
RUN mkdir -vp pgp/keys RUN mkdir -vp pgp/keys
# create application directory
RUN mkdir -vp cic-ussd
RUN mkdir -vp data
ARG EXTRA_PIP_INDEX_URL=https://pip.grassrootseconomics.net ARG EXTRA_PIP_INDEX_URL=https://pip.grassrootseconomics.net
ARG EXTRA_PIP_ARGS="" ARG EXTRA_PIP_ARGS=""
@ -25,7 +23,8 @@ RUN --mount=type=cache,mode=0755,target=/root/.cache/pip \
COPY . . COPY . .
RUN python setup.py install RUN python setup.py install
COPY cic_ussd/db/ussd_menu.json data/ # create local files directory
RUN mkdir -vp cic_ussd/data/locale
COPY docker/*.sh ./ COPY docker/*.sh ./
RUN chmod +x /root/*.sh RUN chmod +x /root/*.sh

View File

@ -1,12 +1,12 @@
cic-eth[services]~=0.12.4a13 cic-eth[services]~=0.12.7
Faker==8.1.2 Faker==11.1.0
faker-e164==0.1.0 faker-e164==0.1.0
pytest==6.2.4 pytest==6.2.5
pytest-alembic==0.2.5 pytest-alembic==0.7.0
pytest-celery==0.0.0a1 pytest-celery==0.0.0a1
pytest-cov==2.10.1 pytest-cov==3.0.0
pytest-mock==3.3.1 pytest-mock==3.6.1
pytest-ordering==0.6 pytest-ordering==0.6
pytest-redis==2.0.0 pytest-redis==2.3.0
requests-mock==1.8.0 requests-mock==1.9.3
tavern==1.14.2 tavern==1.18.0

View File

@ -2,10 +2,16 @@
# external imports # external imports
import pytest import pytest
from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.balance import calculate_available_balance, get_balances, get_cached_available_balance from cic_ussd.account.balance import (calculate_available_balance,
get_balances,
get_cached_adjusted_balance,
get_cached_available_balance)
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.tokens import get_cached_token_data_list
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError from cic_ussd.error import CachedDataNotFoundError
# test imports # test imports
@ -57,19 +63,45 @@ def test_calculate_available_balance(activated_account,
'balance_outgoing': balance_outgoing, 'balance_outgoing': balance_outgoing,
'balance_incoming': balance_incoming 'balance_incoming': balance_incoming
} }
assert calculate_available_balance(balances) == available_balance assert calculate_available_balance(balances, 6) == available_balance
def test_get_cached_available_balance(activated_account, def test_get_cached_available_balance(activated_account,
balances, balances,
cache_balances, cache_balances,
cache_default_token_data, cache_default_token_data,
load_chain_spec): load_chain_spec,
cached_available_balance = get_cached_available_balance(activated_account.blockchain_address) token_symbol):
available_balance = calculate_available_balance(balances[0]) identifier = [bytes.fromhex(activated_account.blockchain_address), token_symbol.encode('utf-8')]
cached_available_balance = get_cached_available_balance(6, identifier)
available_balance = calculate_available_balance(balances[0], 6)
assert cached_available_balance == available_balance assert cached_available_balance == available_balance
address = blockchain_address() address = blockchain_address()
with pytest.raises(CachedDataNotFoundError) as error: with pytest.raises(CachedDataNotFoundError) as error:
cached_available_balance = get_cached_available_balance(address) identifier = [bytes.fromhex(address), token_symbol.encode('utf-8')]
key = cache_data_key(identifier=identifier, salt=MetadataPointer.BALANCES)
cached_available_balance = get_cached_available_balance(6, identifier)
assert cached_available_balance is None assert cached_available_balance is None
assert str(error.value) == f'No cached available balance for address: {address}' assert str(error.value) == f'No cached available balance at {key}'
def test_get_cached_adjusted_balance(activated_account, cache_adjusted_balances, token_symbol):
identifier = bytes.fromhex(activated_account.blockchain_address)
balances_identifier = [identifier, token_symbol.encode('utf-8')]
key = cache_data_key(balances_identifier, MetadataPointer.BALANCES_ADJUSTED)
adjusted_balances = get_cached_data(key)
assert get_cached_adjusted_balance(balances_identifier) == adjusted_balances
def test_get_account_tokens_balance(activated_account,
cache_token_data_list,
celery_session_worker,
load_chain_spec,
load_config,
mock_async_balance_api_query,
token_symbol):
blockchain_address = activated_account.blockchain_address
chain_str = Chain.spec.__str__()
get_balances(blockchain_address, chain_str, token_symbol, asynchronous=True)
assert mock_async_balance_api_query.get('address') == blockchain_address
assert mock_async_balance_api_query.get('token_symbol') == token_symbol

View File

@ -0,0 +1,21 @@
# standard imports
import os
# external imports
# local imports
from cic_ussd.account.guardianship import Guardianship
# test imports
from tests.fixtures.config import root_directory
def test_guardianship(load_config, setup_guardianship):
guardians_file = os.path.join(root_directory, load_config.get('SYSTEM_GUARDIANS_FILE'))
with open(guardians_file, 'r') as system_guardians:
guardians = [line.strip() for line in system_guardians]
assert Guardianship.guardians == guardians
guardianship = Guardianship()
assert guardianship.is_system_guardian(Guardianship.guardians[0]) is True
assert guardianship.is_system_guardian('+254712345678') is False

View File

@ -11,8 +11,7 @@ from cic_ussd.account.statement import (filter_statement_transactions,
generate, generate,
get_cached_statement, get_cached_statement,
parse_statement_transactions, parse_statement_transactions,
query_statement, query_statement)
statement_transaction_set)
from cic_ussd.account.transaction import transaction_actors from cic_ussd.account.transaction import transaction_actors
from cic_ussd.cache import cache_data_key, get_cached_data from cic_ussd.cache import cache_data_key, get_cached_data
@ -74,12 +73,3 @@ def test_query_statement(blockchain_address, limit, load_chain_spec, activated_a
query_statement(blockchain_address, limit) query_statement(blockchain_address, limit)
assert mock_transaction_list_query.get('address') == blockchain_address assert mock_transaction_list_query.get('address') == blockchain_address
assert mock_transaction_list_query.get('limit') == limit assert mock_transaction_list_query.get('limit') == limit
def test_statement_transaction_set(cache_default_token_data, load_chain_spec, preferences, set_locale_files, statement):
parsed_transactions = parse_statement_transactions(statement)
preferred_language = preferences.get('preferred_language')
transaction_set = statement_transaction_set(preferred_language, parsed_transactions)
transaction_set.startswith('Sent')
transaction_set = statement_transaction_set(preferred_language, [])
transaction_set.startswith('No')

View File

@ -1,17 +1,80 @@
# standard imports # standard imports
import hashlib
import json import json
# external imports # external imports
import pytest import pytest
from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.tokens import get_cached_default_token, get_default_token_symbol, query_default_token from cic_ussd.account.tokens import (collate_token_metadata,
create_account_tokens_list,
get_active_token_symbol,
get_default_token_symbol,
get_cached_default_token,
get_cached_token_data,
get_cached_token_data_list,
get_cached_token_symbol_list,
hashed_token_proof,
handle_token_symbol_list,
order_account_tokens_list,
parse_token_list,
process_token_data,
query_default_token,
query_token_data,
remove_from_account_tokens_list,
set_active_token)
from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.error import CachedDataNotFoundError
# test imports # test imports
def test_collate_token_metadata(token_meta_symbol, token_proof_symbol):
description = token_proof_symbol.get('description')
issuer = token_proof_symbol.get('issuer')
location = token_meta_symbol.get('location')
contact = token_meta_symbol.get('contact')
data = {
'description': description,
'issuer': issuer,
'location': location,
'contact': contact
}
assert collate_token_metadata(token_proof_symbol, token_meta_symbol) == data
def test_create_account_tokens_list(activated_account,
cache_balances,
cache_token_data,
cache_token_symbol_list,
init_cache):
create_account_tokens_list(activated_account.blockchain_address)
key = cache_data_key(bytes.fromhex(activated_account.blockchain_address), MetadataPointer.TOKEN_DATA_LIST)
cached_data_list = json.loads(get_cached_data(key))
data = get_cached_token_data_list(activated_account.blockchain_address)
assert cached_data_list == data
def test_get_active_token_symbol(activated_account, set_active_token, valid_recipient):
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_ACTIVE)
active_token_symbol = get_cached_data(key)
assert active_token_symbol == get_active_token_symbol(activated_account.blockchain_address)
with pytest.raises(CachedDataNotFoundError) as error:
get_active_token_symbol(valid_recipient.blockchain_address)
assert str(error.value) == 'No active token set.'
def test_get_cached_token_data(activated_account, cache_token_data, token_symbol):
identifier = [bytes.fromhex(activated_account.blockchain_address), token_symbol.encode('utf-8')]
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
token_data = json.loads(get_cached_data(key))
assert token_data == get_cached_token_data(activated_account.blockchain_address, token_symbol)
def test_get_cached_default_token(cache_default_token_data, default_token_data, load_chain_spec): def test_get_cached_default_token(cache_default_token_data, default_token_data, load_chain_spec):
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
cached_default_token = get_cached_default_token(chain_str) cached_default_token = get_cached_default_token(chain_str)
@ -27,6 +90,84 @@ def test_get_default_token_symbol_from_api(default_token_data, load_chain_spec,
assert default_token_symbol == default_token_data['symbol'] assert default_token_symbol == default_token_data['symbol']
def test_get_cached_token_data_list(activated_account, cache_token_data_list):
blockchain_address = activated_account.blockchain_address
key = cache_data_key(identifier=bytes.fromhex(blockchain_address), salt=MetadataPointer.TOKEN_DATA_LIST)
token_symbols_list = json.loads(get_cached_data(key))
assert token_symbols_list == get_cached_token_data_list(blockchain_address)
def test_get_cached_token_symbol_list(activated_account, cache_token_symbol_list):
blockchain_address = activated_account.blockchain_address
key = cache_data_key(identifier=bytes.fromhex(blockchain_address), salt=MetadataPointer.TOKEN_SYMBOLS_LIST)
token_symbols_list = json.loads(get_cached_data(key))
assert token_symbols_list == get_cached_token_symbol_list(blockchain_address)
def test_hashed_token_proof(token_proof_symbol):
hash_object = hashlib.new("sha256")
token_proof = json.dumps(token_proof_symbol)
hash_object.update(token_proof.encode('utf-8'))
assert hash_object.digest().hex() == hashed_token_proof(token_proof_symbol)
def test_handle_token_symbol_list(activated_account, init_cache):
handle_token_symbol_list(activated_account.blockchain_address, 'GFT')
cached_token_symbol_list = get_cached_token_symbol_list(activated_account.blockchain_address)
assert len(cached_token_symbol_list) == 1
handle_token_symbol_list(activated_account.blockchain_address, 'DET')
cached_token_symbol_list = get_cached_token_symbol_list(activated_account.blockchain_address)
assert len(cached_token_symbol_list) == 2
def test_order_account_tokens_list(activated_account, token_list_entries):
identifier = bytes.fromhex(activated_account.blockchain_address)
last_sent_token_key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_SENT)
cache_data(last_sent_token_key, 'FII')
last_received_token_key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_LAST_RECEIVED)
cache_data(last_received_token_key, 'DET')
ordered_list = order_account_tokens_list(token_list_entries, identifier)
assert ordered_list == [
{
'name': 'Fee',
'symbol': 'FII',
'issuer': 'Foo',
'contact': {
'phone': '+254712345678'
},
'location': 'Fum',
'balance': 50.0
},
{
'name': 'Demurrage Token',
'symbol': 'DET',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'},
'location': 'Fum',
'balance': 49.99
},
{
'name': 'Giftable Token',
'symbol': 'GFT',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'},
'location': 'Fum',
'balance': 60.0
}
]
def test_parse_token_list(token_list_entries):
parsed_token_list = ['1. FII 50.0', '2. GFT 60.0', '3. DET 49.99']
assert parsed_token_list == parse_token_list(token_list_entries)
def test_query_default_token(default_token_data, load_chain_spec, mock_sync_default_token_api_query): def test_query_default_token(default_token_data, load_chain_spec, mock_sync_default_token_api_query):
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
queried_default_token_data = query_default_token(chain_str) queried_default_token_data = query_default_token(chain_str)
@ -40,3 +181,38 @@ def test_get_default_token_symbol_from_cache(cache_default_token_data, default_t
default_token_symbol = get_default_token_symbol() default_token_symbol = get_default_token_symbol()
assert default_token_symbol is not None assert default_token_symbol is not None
assert default_token_symbol == default_token_data.get('symbol') assert default_token_symbol == default_token_data.get('symbol')
def test_remove_from_account_tokens_list(token_list_entries):
assert remove_from_account_tokens_list(token_list_entries, 'GFT') == ([{
'name': 'Giftable Token',
'symbol': 'GFT',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'
},
'location': 'Fum',
'balance': 60.0
}],
[
{
'name': 'Fee',
'symbol': 'FII',
'issuer': 'Foo',
'contact': {'phone': '+254712345678'},
'location': 'Fum',
'balance': 50.0
},
{
'name': 'Demurrage Token',
'symbol': 'DET',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'
},
'location': 'Fum',
'balance': 49.99
}
])

View File

@ -1,5 +1,4 @@
# standard imports # standard imports
from decimal import Decimal
# external imports # external imports
import pytest import pytest
@ -37,11 +36,11 @@ def test_aux_transaction_data(preferences, set_locale_files, transactions_list):
@pytest.mark.parametrize("value, expected_result", [ @pytest.mark.parametrize("value, expected_result", [
(50000000, Decimal('50.00')), (50000000, 50.0),
(100000, Decimal('0.10')) (100000, 0.1)
]) ])
def test_from_wei(cache_default_token_data, expected_result, value): def test_from_wei(cache_default_token_data, expected_result, value):
assert from_wei(value) == expected_result assert from_wei(6, value) == expected_result
@pytest.mark.parametrize("value, expected_result", [ @pytest.mark.parametrize("value, expected_result", [
@ -49,7 +48,7 @@ def test_from_wei(cache_default_token_data, expected_result, value):
(0.10, 100000) (0.10, 100000)
]) ])
def test_to_wei(cache_default_token_data, expected_result, value): def test_to_wei(cache_default_token_data, expected_result, value):
assert to_wei(value) == expected_result assert to_wei(6, value) == expected_result
@pytest.mark.parametrize("decimals, value, expected_result", [ @pytest.mark.parametrize("decimals, value, expected_result", [
@ -108,8 +107,8 @@ def test_outgoing_transaction_processor(activated_account,
activated_account.blockchain_address, activated_account.blockchain_address,
valid_recipient.blockchain_address) valid_recipient.blockchain_address)
outgoing_tx_processor.transfer(amount, token_symbol) outgoing_tx_processor.transfer(amount, 6, token_symbol)
assert mock_transfer_api.get('from_address') == activated_account.blockchain_address assert mock_transfer_api.get('from_address') == activated_account.blockchain_address
assert mock_transfer_api.get('to_address') == valid_recipient.blockchain_address assert mock_transfer_api.get('to_address') == valid_recipient.blockchain_address
assert mock_transfer_api.get('value') == to_wei(amount) assert mock_transfer_api.get('value') == to_wei(6, amount)
assert mock_transfer_api.get('token_symbol') == token_symbol assert mock_transfer_api.get('token_symbol') == token_symbol

View File

@ -90,7 +90,7 @@ def test_standard_metadata_id(activated_account, cache_person_metadata, pending_
def test_account_create(init_cache, init_database, load_chain_spec, mock_account_creation_task_result, task_uuid): def test_account_create(init_cache, init_database, load_chain_spec, mock_account_creation_task_result, task_uuid):
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
create(chain_str, phone_number(), init_database) create(chain_str, phone_number(), init_database, 'en')
assert len(init_database.query(TaskTracker).all()) == 1 assert len(init_database.query(TaskTracker).all()) == 1
account_creation_data = get_cached_data(task_uuid) account_creation_data = get_cached_data(task_uuid)
assert json.loads(account_creation_data).get('status') == AccountStatus.PENDING.name assert json.loads(account_creation_data).get('status') == AccountStatus.PENDING.name

View File

@ -23,7 +23,7 @@ def test_ussd_metadata_handler(activated_account,
setup_metadata_signer): setup_metadata_signer):
identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address)) identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address))
cic_type = MetadataPointer.PERSON cic_type = MetadataPointer.PERSON
metadata_client = UssdMetadataHandler(cic_type, identifier) metadata_client = UssdMetadataHandler(cic_type=cic_type, identifier=identifier)
assert metadata_client.cic_type == cic_type assert metadata_client.cic_type == cic_type
assert metadata_client.engine == 'pgp' assert metadata_client.engine == 'pgp'
assert metadata_client.identifier == identifier assert metadata_client.identifier == identifier

View File

@ -0,0 +1,72 @@
# standard imports
import json
# external imports
import pytest
import requests_mock
from cic_types.condiments import MetadataPointer
from requests.exceptions import HTTPError
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.metadata import TokenMetadata
from cic_ussd.metadata.tokens import token_metadata_handler, query_token_metadata, query_token_info
# test imports
def test_token_metadata_handler(activated_account,
init_cache,
setup_metadata_request_handler,
setup_metadata_signer,
token_meta_symbol,
token_symbol):
with requests_mock.Mocker(real_http=False) as request_mocker:
with pytest.raises(HTTPError) as error:
metadata_client = TokenMetadata(identifier=b'foo', cic_type=MetadataPointer.TOKEN_META_SYMBOL)
reason = 'Not Found'
status_code = 401
request_mocker.register_uri('GET', metadata_client.url, status_code=status_code, reason=reason)
token_metadata_handler(metadata_client)
assert str(error.value) == f'Client Error: {status_code}, reason: {reason}'
identifier = token_symbol.encode('utf-8')
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_META_SYMBOL)
request_mocker.register_uri('GET', metadata_client.url, json=token_meta_symbol, status_code=200, reason='OK')
token_metadata_handler(metadata_client)
key = cache_data_key(identifier, MetadataPointer.TOKEN_META_SYMBOL)
cached_token_meta_symbol = get_cached_data(key)
assert json.loads(cached_token_meta_symbol) == token_meta_symbol
def test_query_token_metadata(init_cache,
setup_metadata_request_handler,
setup_metadata_signer,
token_meta_symbol,
token_proof_symbol,
token_symbol):
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = token_symbol.encode('utf-8')
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_META_SYMBOL)
request_mocker.register_uri('GET', metadata_client.url, json=token_meta_symbol, status_code=200, reason='OK')
query_token_metadata(identifier)
key = cache_data_key(identifier, MetadataPointer.TOKEN_META_SYMBOL)
cached_token_meta_symbol = get_cached_data(key)
assert json.loads(cached_token_meta_symbol) == token_meta_symbol
def test_query_token_info(init_cache,
setup_metadata_request_handler,
setup_metadata_signer,
token_meta_symbol,
token_proof_symbol,
token_symbol):
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = token_symbol.encode('utf-8')
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_PROOF_SYMBOL)
request_mocker.register_uri('GET', metadata_client.url, json=token_proof_symbol, status_code=200, reason='OK')
query_token_info(identifier)
key = cache_data_key(identifier, MetadataPointer.TOKEN_PROOF_SYMBOL)
cached_token_proof_symbol = get_cached_data(key)
assert json.loads(cached_token_proof_symbol) == token_proof_symbol

View File

@ -1,6 +1,6 @@
# standard imports # standard imports
import json import json
import datetime import os
# external imports # external imports
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
@ -10,195 +10,464 @@ from cic_ussd.account.balance import get_cached_available_balance
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.statement import ( from cic_ussd.account.statement import (
get_cached_statement, get_cached_statement,
parse_statement_transactions, parse_statement_transactions
statement_transaction_set
) )
from cic_ussd.account.tokens import get_default_token_symbol from cic_ussd.account.tokens import (get_active_token_symbol,
get_cached_token_data)
from cic_ussd.account.transaction import from_wei, to_wei from cic_ussd.account.transaction import from_wei, to_wei
from cic_ussd.cache import cache_data, cache_data_key from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.menu.ussd_menu import UssdMenu
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.phone_number import Support from cic_ussd.phone_number import Support
from cic_ussd.processor.menu import response from cic_ussd.processor.menu import response, MenuProcessor
from cic_ussd.processor.util import parse_person_metadata from cic_ussd.processor.util import parse_person_metadata, ussd_menu_list
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
# test imports # test imports
def test_account_balance(activated_account, cache_balances, cache_preferences, cache_token_data,
def test_menu_processor(activated_account, generic_ussd_session, init_database, set_active_token):
balances, """blockchain_address = activated_account.blockchain_address
cache_balances, token_symbol = get_active_token_symbol(blockchain_address)
cache_default_token_data, token_data = get_cached_token_data(blockchain_address, token_symbol)
cache_preferences, preferred_language = get_cached_preferred_language(blockchain_address)
cache_person_metadata, decimals = token_data.get("decimals")
cache_statement, identifier = bytes.fromhex(blockchain_address)
celery_session_worker, balances_identifier = [identifier, token_symbol.encode('utf-8')]
generic_ussd_session, available_balance = get_cached_available_balance(decimals, balances_identifier)
init_database,
load_chain_spec,
load_support_phone,
load_ussd_menu,
mock_get_adjusted_balance,
mock_sync_balance_api_query,
mock_transaction_list_query,
valid_recipient):
preferred_language = get_cached_preferred_language(activated_account.blockchain_address)
available_balance = get_cached_available_balance(activated_account.blockchain_address)
token_symbol = get_default_token_symbol()
with_available_balance = 'ussd.account_balances.available_balance' with_available_balance = 'ussd.account_balances.available_balance'
with_fees = 'ussd.account_balances.with_fees' resp = response(activated_account, with_available_balance, with_available_balance[5:], init_database,
ussd_menu = UssdMenu.find_by_name('account_balances') generic_ussd_session)
name = ussd_menu.get('name')
resp = response(activated_account, 'ussd.account_balances', name, init_database, generic_ussd_session)
assert resp == translation_for(with_available_balance, assert resp == translation_for(with_available_balance,
preferred_language, preferred_language,
available_balance=available_balance, available_balance=available_balance,
token_symbol=token_symbol) token_symbol=token_symbol)
identifier = bytes.fromhex(activated_account.blockchain_address) with_fees = 'ussd.account_balances.with_fees'
key = cache_data_key(identifier, MetadataPointer.BALANCES_ADJUSTED) key = cache_data_key(balances_identifier, MetadataPointer.BALANCES_ADJUSTED)
adjusted_balance = 45931650.64654012 adjusted_balance = 45931650.64654012
cache_data(key, json.dumps(adjusted_balance)) cache_data(key, json.dumps(adjusted_balance))
resp = response(activated_account, 'ussd.account_balances', name, init_database, generic_ussd_session) resp = response(activated_account, with_fees, with_fees[5:], init_database, generic_ussd_session)
tax_wei = to_wei(int(available_balance)) - int(adjusted_balance) tax_wei = to_wei(decimals, int(available_balance)) - int(adjusted_balance)
tax = from_wei(int(tax_wei)) tax = from_wei(decimals, int(tax_wei))
assert resp == translation_for(key=with_fees, assert resp == translation_for(key=with_fees,
preferred_language=preferred_language, preferred_language=preferred_language,
available_balance=available_balance, available_balance=available_balance,
tax=tax, tax=tax,
token_symbol=token_symbol) token_symbol=token_symbol)"""
pass
cached_statement = get_cached_statement(activated_account.blockchain_address)
statement = json.loads(cached_statement)
statement_transactions = parse_statement_transactions(statement)
transaction_sets = [statement_transactions[tx:tx + 3] for tx in range(0, len(statement_transactions), 3)]
first_transaction_set = []
middle_transaction_set = []
last_transaction_set = []
if transaction_sets:
first_transaction_set = statement_transaction_set(preferred_language, transaction_sets[0])
if len(transaction_sets) >= 2:
middle_transaction_set = statement_transaction_set(preferred_language, transaction_sets[1])
if len(transaction_sets) >= 3:
last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2])
display_key = 'ussd.first_transaction_set' def test_account_statement(activated_account,
ussd_menu = UssdMenu.find_by_name('first_transaction_set') cache_preferences,
name = ussd_menu.get('name') cache_statement,
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) generic_ussd_session,
init_database,
set_active_token,
set_locale_files):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
cached_statement = get_cached_statement(blockchain_address)
statement_list = parse_statement_transactions(statement=json.loads(cached_statement))
first_transaction_set = 'ussd.first_transaction_set'
middle_transaction_set = 'ussd.middle_transaction_set'
last_transaction_set = 'ussd.last_transaction_set'
fallback = translation_for('helpers.no_transaction_history', preferred_language)
transaction_sets = ussd_menu_list(fallback=fallback, menu_list=statement_list, split=3)
resp = response(activated_account, first_transaction_set, first_transaction_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(first_transaction_set, preferred_language, first_transaction_set=transaction_sets[0])
resp = response(activated_account, middle_transaction_set, middle_transaction_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(middle_transaction_set, preferred_language,
middle_transaction_set=transaction_sets[1])
resp = response(activated_account, last_transaction_set, last_transaction_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(last_transaction_set, preferred_language, last_transaction_set=transaction_sets[2])
assert resp == translation_for(display_key, preferred_language, first_transaction_set=first_transaction_set)
display_key = 'ussd.middle_transaction_set' def test_add_guardian_pin_authorization(activated_account,
ussd_menu = UssdMenu.find_by_name('middle_transaction_set') cache_preferences,
name = ussd_menu.get('name') guardian_account,
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) generic_ussd_session,
init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
add_guardian_pin_authorization = 'ussd.add_guardian_pin_authorization'
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
generic_ussd_session['external_session_id'] = os.urandom(20).hex()
generic_ussd_session['msisdn'] = guardian_account.phone_number
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
generic_ussd_session['state'] = 'add_guardian_pin_authorization'
resp = response(activated_account,
add_guardian_pin_authorization,
add_guardian_pin_authorization[5:],
init_database,
generic_ussd_session)
assert resp == translation_for(f'{add_guardian_pin_authorization}.first', preferred_language,
guardian_information=guardian_account.standard_metadata_id())
assert resp == translation_for(display_key, preferred_language, middle_transaction_set=middle_transaction_set)
display_key = 'ussd.last_transaction_set' def test_guardian_list(activated_account,
ussd_menu = UssdMenu.find_by_name('last_transaction_set') cache_preferences,
name = ussd_menu.get('name') generic_ussd_session,
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) guardian_account,
init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
guardians_list = 'ussd.guardian_list'
guardians_list_header = translation_for('helpers.guardians_list_header', preferred_language)
guardian_information = guardian_account.standard_metadata_id()
guardians = guardians_list_header + '\n' + f'{guardian_information}\n'
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
resp = response(activated_account, guardians_list, guardians_list[5:], init_database, generic_ussd_session)
assert resp == translation_for(guardians_list, preferred_language, guardians_list=guardians)
guardians = translation_for('helpers.no_guardians_list', preferred_language)
identifier = bytes.fromhex(guardian_account.blockchain_address)
key = cache_data_key(identifier, MetadataPointer.PREFERENCES)
cache_data(key, json.dumps({'preferred_language': preferred_language}))
resp = response(guardian_account, guardians_list, guardians_list[5:], init_database, generic_ussd_session)
assert resp == translation_for(guardians_list, preferred_language, guardians_list=guardians)
assert resp == translation_for(display_key, preferred_language, last_transaction_set=last_transaction_set)
display_key = 'ussd.display_user_metadata' def test_account_tokens(activated_account, cache_token_data_list, celery_session_worker, generic_ussd_session,
ussd_menu = UssdMenu.find_by_name('display_user_metadata') init_cache, init_database):
name = ussd_menu.get('name') """blockchain_address = activated_account.blockchain_address
identifier = bytes.fromhex(activated_account.blockchain_address) preferred_language = get_cached_preferred_language(blockchain_address)
cached_token_data_list = get_cached_token_data_list(blockchain_address)
token_data_list = ['1. GFT 50.0']
fallback = translation_for('helpers.no_tokens_list', preferred_language)
token_list_sets = ussd_menu_list(fallback=fallback, menu_list=token_data_list, split=3)
first_account_tokens_set = 'ussd.first_account_tokens_set'
middle_account_tokens_set = 'ussd.middle_account_tokens_set'
last_account_tokens_set = 'ussd.last_account_tokens_set'
resp = response(activated_account, first_account_tokens_set, first_account_tokens_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(first_account_tokens_set, preferred_language,
first_account_tokens_set=token_list_sets[0])
assert generic_ussd_session.get('data').get('account_tokens_list') == cached_token_data_list
resp = response(activated_account, middle_account_tokens_set, middle_account_tokens_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(middle_account_tokens_set, preferred_language,
middle_account_tokens_set=token_list_sets[1])
resp = response(activated_account, last_account_tokens_set, last_account_tokens_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(last_account_tokens_set, preferred_language,
last_account_tokens_set=token_list_sets[2])"""
pass
def test_help(activated_account, cache_preferences, generic_ussd_session, init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
help = 'ussd.help'
resp = response(activated_account, help, help[5:], init_database, generic_ussd_session)
assert resp == translation_for(help, preferred_language, support_phone=Support.phone_number)
def test_person_data(activated_account, cache_person_metadata, cache_preferences, cached_ussd_session,
generic_ussd_session, init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
identifier = bytes.fromhex(blockchain_address)
display_user_metadata = 'ussd.display_user_metadata'
person_metadata = PersonMetadata(identifier) person_metadata = PersonMetadata(identifier)
cached_person_metadata = person_metadata.get_cached_metadata() cached_person_metadata = person_metadata.get_cached_metadata()
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) resp = response(activated_account, display_user_metadata, display_user_metadata[5:], init_database,
assert resp == parse_person_metadata(cached_person_metadata, display_key, preferred_language) generic_ussd_session)
assert resp == parse_person_metadata(cached_person_metadata, display_user_metadata, preferred_language)
display_key = 'ussd.account_balances_pin_authorization'
ussd_menu = UssdMenu.find_by_name('account_balances_pin_authorization')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == translation_for(f'{display_key}.first', preferred_language)
activated_account.failed_pin_attempts = 1 def test_guarded_account_metadata(activated_account, generic_ussd_session, init_database):
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) reset_guarded_pin_authorization = 'ussd.reset_guarded_pin_authorization'
retry_pin_entry = translation_for('ussd.retry_pin_entry', preferred_language, remaining_attempts=2) generic_ussd_session['data'] = {'guarded_account_phone_number': activated_account.phone_number}
assert resp == translation_for(f'{display_key}.retry', preferred_language, retry_pin_entry=retry_pin_entry) menu_processor = MenuProcessor(activated_account, reset_guarded_pin_authorization,
activated_account.failed_pin_attempts = 0 reset_guarded_pin_authorization[5:], init_database, generic_ussd_session)
assert menu_processor.guarded_account_metadata() == activated_account.standard_metadata_id()
display_key = 'ussd.start'
ussd_menu = UssdMenu.find_by_name('start') def test_guardian_metadata(activated_account, generic_ussd_session, guardian_account, init_database):
name = ussd_menu.get('name') add_guardian_pin_authorization = 'ussd.add_guardian_pin_authorization'
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
assert resp == translation_for(display_key, menu_processor = MenuProcessor(activated_account, add_guardian_pin_authorization,
add_guardian_pin_authorization[5:], init_database, generic_ussd_session)
assert menu_processor.guardian_metadata() == guardian_account.standard_metadata_id()
def test_language(activated_account, cache_preferences, generic_ussd_session, init_database, load_languages):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
initial_language_selection = 'ussd.initial_language_selection'
select_preferred_language = 'ussd.select_preferred_language'
initial_middle_language_set = 'ussd.initial_middle_language_set'
middle_language_set = 'ussd.middle_language_set'
initial_last_language_set = 'ussd.initial_last_language_set'
last_language_set = 'ussd.last_language_set'
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list: list = json.loads(cached_system_languages)
fallback = translation_for('helpers.no_language_list', preferred_language)
language_list_sets = ussd_menu_list(fallback=fallback, menu_list=language_list, split=3)
resp = response(activated_account, initial_language_selection, initial_language_selection[5:], init_database,
generic_ussd_session)
assert resp == translation_for(initial_language_selection, preferred_language,
first_language_set=language_list_sets[0])
resp = response(activated_account, select_preferred_language, select_preferred_language[5:], init_database,
generic_ussd_session)
assert resp == translation_for(select_preferred_language, preferred_language,
first_language_set=language_list_sets[0])
resp = response(activated_account, initial_middle_language_set, initial_middle_language_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(initial_middle_language_set, preferred_language,
middle_language_set=language_list_sets[1])
resp = response(activated_account, initial_last_language_set, initial_last_language_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(initial_last_language_set, preferred_language,
last_language_set=language_list_sets[2])
resp = response(activated_account, middle_language_set, middle_language_set[5:], init_database,
generic_ussd_session)
assert resp == translation_for(middle_language_set, preferred_language, middle_language_set=language_list_sets[1])
resp = response(activated_account, last_language_set, last_language_set[5:], init_database, generic_ussd_session)
assert resp == translation_for(last_language_set, preferred_language, last_language_set=language_list_sets[2])
def test_account_creation_prompt(activated_account, cache_preferences, generic_ussd_session, init_database,
load_languages):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
user_input = ''
if preferred_language == 'en':
user_input = '1'
elif preferred_language == 'sw':
user_input = '2'
account_creation_prompt = 'ussd.account_creation_prompt'
generic_ussd_session['user_input'] = user_input
resp = response(activated_account, account_creation_prompt, account_creation_prompt[5:], init_database,
generic_ussd_session)
assert resp == translation_for(account_creation_prompt, preferred_language)
def test_reset_guarded_pin_authorization(activated_account, cache_preferences, generic_ussd_session, guardian_account,
init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
reset_guarded_pin_authorization = 'ussd.reset_guarded_pin_authorization'
generic_ussd_session['external_session_id'] = os.urandom(20).hex()
generic_ussd_session['msisdn'] = guardian_account.phone_number
generic_ussd_session['data'] = {'guarded_account_phone_number': activated_account.phone_number}
resp = response(activated_account,
reset_guarded_pin_authorization,
reset_guarded_pin_authorization[5:],
init_database,
generic_ussd_session)
assert resp == translation_for(f'{reset_guarded_pin_authorization}.first', preferred_language,
guarded_account_information=activated_account.phone_number)
def test_start(activated_account, cache_balances, cache_preferences, cache_token_data, cache_token_data_list,
cache_token_symbol_list, celery_session_worker, generic_ussd_session, init_database, load_chain_spec,
mock_sync_balance_api_query, set_active_token):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
decimals = token_data.get("decimals")
identifier = bytes.fromhex(blockchain_address)
balances_identifier = [identifier, token_symbol.encode('utf-8')]
available_balance = get_cached_available_balance(decimals, balances_identifier)
start = 'ussd.start'
resp = response(activated_account, start, start[5:], init_database, generic_ussd_session)
assert resp == translation_for(start,
preferred_language, preferred_language,
account_balance=available_balance, account_balance=available_balance,
account_token_name=token_symbol) account_token_name=token_symbol)
display_key = 'ussd.start'
ussd_menu = UssdMenu.find_by_name('start')
name = ussd_menu.get('name')
older_timestamp = (activated_account.created - datetime.timedelta(days=35))
activated_account.created = older_timestamp
init_database.flush()
response(activated_account, display_key, name, init_database, generic_ussd_session)
assert mock_get_adjusted_balance['timestamp'] == int((datetime.datetime.now() - datetime.timedelta(days=30)).timestamp())
display_key = 'ussd.transaction_pin_authorization' def test_token_selection_pin_authorization(activated_account, cache_preferences, cache_token_data, generic_ussd_session,
ussd_menu = UssdMenu.find_by_name('transaction_pin_authorization') init_database, set_active_token):
name = ussd_menu.get('name') blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
token_selection_pin_authorization = 'ussd.token_selection_pin_authorization'
generic_ussd_session['data'] = {'selected_token': token_data}
resp = response(activated_account,
token_selection_pin_authorization,
token_selection_pin_authorization[5:],
init_database,
generic_ussd_session)
token_name = token_data.get('name')
token_symbol = token_data.get('symbol')
token_issuer = token_data.get('issuer')
token_contact = token_data.get('contact')
token_location = token_data.get('location')
data = f'{token_name} ({token_symbol})\n{token_issuer}\n{token_contact}\n{token_location}\n'
assert resp == translation_for(f'{token_selection_pin_authorization}.first', preferred_language,
token_data=data)
def test_transaction_pin_authorization(activated_account, cache_preferences, cache_token_data, generic_ussd_session,
init_database, set_active_token, valid_recipient):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
decimals = token_data.get("decimals")
transaction_pin_authorization = 'ussd.transaction_pin_authorization'
generic_ussd_session['data'] = { generic_ussd_session['data'] = {
'recipient_phone_number': valid_recipient.phone_number, 'recipient_phone_number': valid_recipient.phone_number,
'transaction_amount': '15' 'transaction_amount': '15'
} }
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) resp = response(activated_account, transaction_pin_authorization, transaction_pin_authorization[5:], init_database,
generic_ussd_session)
user_input = generic_ussd_session.get('data').get('transaction_amount') user_input = generic_ussd_session.get('data').get('transaction_amount')
transaction_amount = to_wei(value=int(user_input)) transaction_amount = to_wei(decimals, int(user_input))
tx_recipient_information = valid_recipient.standard_metadata_id() tx_recipient_information = valid_recipient.standard_metadata_id()
tx_sender_information = activated_account.standard_metadata_id() tx_sender_information = activated_account.standard_metadata_id()
assert resp == translation_for(f'{display_key}.first', assert resp == translation_for(f'{transaction_pin_authorization}.first',
preferred_language, preferred_language,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
transaction_amount=from_wei(transaction_amount), transaction_amount=from_wei(decimals, transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
sender_information=tx_sender_information) sender_information=tx_sender_information)
display_key = 'ussd.exit_insufficient_balance'
ussd_menu = UssdMenu.find_by_name('exit_insufficient_balance') def test_guardian_exits(activated_account, cache_preferences, cache_token_data, generic_ussd_session, guardian_account,
name = ussd_menu.get('name') init_database, set_active_token):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
# testing exit guardian addition success
exit_guardian_addition_success = 'ussd.exit_guardian_addition_success'
resp = response(activated_account, exit_guardian_addition_success, exit_guardian_addition_success[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_guardian_addition_success, preferred_language,
guardian_information=guardian_account.standard_metadata_id())
# testing exit guardian removal success
exit_guardian_removal_success = 'ussd.exit_guardian_removal_success'
resp = response(activated_account, exit_guardian_removal_success, exit_guardian_removal_success[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_guardian_removal_success, preferred_language,
guardian_information=guardian_account.standard_metadata_id())
generic_ussd_session['data'] = {'failure_reason': 'foo'}
# testing exit invalid guardian addition
exit_invalid_guardian_addition = 'ussd.exit_invalid_guardian_addition'
resp = response(activated_account, exit_invalid_guardian_addition, exit_invalid_guardian_addition[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_invalid_guardian_addition, preferred_language, error_exit='foo')
# testing exit invalid guardian removal
exit_invalid_guardian_removal = 'ussd.exit_invalid_guardian_removal'
resp = response(activated_account, exit_invalid_guardian_removal, exit_invalid_guardian_removal[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_invalid_guardian_removal, preferred_language, error_exit='foo')
def test_exit_pin_reset_initiated_success(activated_account, cache_preferences, generic_ussd_session, init_database):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
exit_pin_reset_initiated_success = 'ussd.exit_pin_reset_initiated_success'
generic_ussd_session['data'] = {'guarded_account_phone_number': activated_account.phone_number}
resp = response(activated_account, exit_pin_reset_initiated_success, exit_pin_reset_initiated_success[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_pin_reset_initiated_success,
preferred_language,
guarded_account_information=activated_account.standard_metadata_id())
def test_exit_insufficient_balance(activated_account, cache_balances, cache_preferences, cache_token_data,
generic_ussd_session, init_database, set_active_token, valid_recipient):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
decimals = token_data.get("decimals")
identifier = bytes.fromhex(blockchain_address)
balances_identifier = [identifier, token_symbol.encode('utf-8')]
available_balance = get_cached_available_balance(decimals, balances_identifier)
tx_recipient_information = valid_recipient.standard_metadata_id()
exit_insufficient_balance = 'ussd.exit_insufficient_balance'
generic_ussd_session['data'] = { generic_ussd_session['data'] = {
'recipient_phone_number': valid_recipient.phone_number, 'recipient_phone_number': valid_recipient.phone_number,
'transaction_amount': '85' 'transaction_amount': '85'
} }
transaction_amount = generic_ussd_session.get('data').get('transaction_amount') transaction_amount = generic_ussd_session.get('data').get('transaction_amount')
transaction_amount = to_wei(value=int(transaction_amount)) transaction_amount = to_wei(decimals, int(transaction_amount))
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) resp = response(activated_account, exit_insufficient_balance, exit_insufficient_balance[5:], init_database,
assert resp == translation_for(display_key, generic_ussd_session)
assert resp == translation_for(exit_insufficient_balance,
preferred_language, preferred_language,
amount=from_wei(transaction_amount), amount=from_wei(decimals, transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
token_balance=available_balance) token_balance=available_balance)
display_key = 'ussd.exit_invalid_menu_option'
ussd_menu = UssdMenu.find_by_name('exit_invalid_menu_option')
name = ussd_menu.get('name')
resp = response(activated_account, display_key, name, init_database, generic_ussd_session)
assert resp == translation_for(display_key, preferred_language, support_phone=Support.phone_number)
display_key = 'ussd.exit_successful_transaction' def test_exit_invalid_menu_option(activated_account, cache_preferences, generic_ussd_session, init_database,
ussd_menu = UssdMenu.find_by_name('exit_successful_transaction') load_support_phone):
name = ussd_menu.get('name') blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
exit_invalid_menu_option = 'ussd.exit_invalid_menu_option'
resp = response(activated_account, exit_invalid_menu_option, exit_invalid_menu_option[5:], init_database,
generic_ussd_session)
assert resp == translation_for(exit_invalid_menu_option, preferred_language, support_phone=Support.phone_number)
def test_exit_pin_blocked(activated_account, cache_preferences, generic_ussd_session, init_database,
load_support_phone):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
exit_pin_blocked = 'ussd.exit_pin_blocked'
resp = response(activated_account, exit_pin_blocked, exit_pin_blocked[5:], init_database, generic_ussd_session)
assert resp == translation_for(exit_pin_blocked, preferred_language, support_phone=Support.phone_number)
def test_exit_successful_token_selection(activated_account, cache_preferences, cache_token_data, generic_ussd_session,
init_database, set_active_token):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
exit_successful_token_selection = 'ussd.exit_successful_token_selection'
generic_ussd_session['data'] = {'selected_token': token_data}
resp = response(activated_account, exit_successful_token_selection, exit_successful_token_selection[5:],
init_database, generic_ussd_session)
assert resp == translation_for(exit_successful_token_selection, preferred_language, token_symbol=token_symbol)
def test_exit_successful_transaction(activated_account, cache_preferences, cache_token_data, generic_ussd_session,
init_database, set_active_token, valid_recipient):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
preferred_language = get_cached_preferred_language(blockchain_address)
decimals = token_data.get("decimals")
tx_recipient_information = valid_recipient.standard_metadata_id()
tx_sender_information = activated_account.standard_metadata_id()
exit_successful_transaction = 'ussd.exit_successful_transaction'
generic_ussd_session['data'] = { generic_ussd_session['data'] = {
'recipient_phone_number': valid_recipient.phone_number, 'recipient_phone_number': valid_recipient.phone_number,
'transaction_amount': '15' 'transaction_amount': '15'
} }
transaction_amount = generic_ussd_session.get('data').get('transaction_amount') transaction_amount = generic_ussd_session.get('data').get('transaction_amount')
transaction_amount = to_wei(value=int(transaction_amount)) transaction_amount = to_wei(decimals, int(transaction_amount))
resp = response(activated_account, display_key, name, init_database, generic_ussd_session) resp = response(activated_account, exit_successful_transaction, exit_successful_transaction[5:], init_database,
assert resp == translation_for(display_key, generic_ussd_session)
assert resp == translation_for(exit_successful_transaction,
preferred_language, preferred_language,
transaction_amount=from_wei(transaction_amount), transaction_amount=from_wei(decimals, transaction_amount),
token_symbol=token_symbol, token_symbol=token_symbol,
recipient_information=tx_recipient_information, recipient_information=tx_recipient_information,
sender_information=tx_sender_information) sender_information=tx_sender_information)

View File

@ -0,0 +1,69 @@
# standard imports
import logging
import time
from queue import Queue
# external imports
import pytest
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
from cic_ussd.error import MaxRetryReached
from cic_ussd.processor.poller import poller, wait_for_cache, wait_for_session_data
# test imports
def test_poller(activated_account, caplog, init_cache, token_symbol):
caplog.set_level(logging.DEBUG)
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier, MetadataPointer.TOKEN_ACTIVE)
with pytest.raises(MaxRetryReached) as error:
interval = 1
max_retry = 3
collected_values = [None, None, None]
poller(args=(key,), interval=interval, kwargs=None, max_retry=max_retry, target=get_cached_data)
assert str(error.value) == str(MaxRetryReached(collected_values, None))
cache_data(key, token_symbol)
poller(args=(key,), interval=interval, kwargs=None, max_retry=max_retry, target=get_cached_data)
assert f'Resource: {token_symbol} now available.' in caplog.text
def test_wait_for_cache(activated_account, caplog, init_cache, token_symbol):
caplog.set_level(logging.DEBUG)
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier, MetadataPointer.TOKEN_ACTIVE)
cache_data(key, token_symbol)
interval = 1
max_retry = 3
resource_name = 'Active Token'
wait_for_cache(identifier, resource_name, MetadataPointer.TOKEN_ACTIVE, interval, max_retry)
assert f'Polling for resource: {resource_name} at: {key} every: {interval} second(s) for {max_retry} seconds.' in caplog.text
def test_wait_for_session_data(activated_account, caplog, generic_ussd_session):
caplog.set_level(logging.DEBUG)
generic_ussd_session.__delitem__('data')
interval = 1
max_retry = 3
collected_values = [None, None, None]
resource_name = 'Foo Data'
session_data_key = 'foo'
with pytest.raises(MaxRetryReached) as error:
wait_for_session_data(resource_name, session_data_key, generic_ussd_session, interval, max_retry)
assert str(error.value) == str(MaxRetryReached(collected_values, None))
assert f'Data poller with max retry at: {max_retry}. Checking for every: {interval} seconds.' in caplog.text
generic_ussd_session['data'] = {}
with pytest.raises(MaxRetryReached) as error:
collected_values = [None, None, None]
wait_for_session_data(resource_name, session_data_key, generic_ussd_session, interval, max_retry)
assert f'Data poller with max retry at: {max_retry}. Checking for every: {interval} seconds.' in caplog.text
assert f'Session data poller for: {resource_name} with max retry at: {max_retry}. Checking for every: {interval} seconds.' in caplog.text
assert str(error.value) == str(MaxRetryReached(collected_values, None))
expected_value = 'bar'
generic_ussd_session['data'] = {'foo': expected_value}
wait_for_session_data(resource_name, session_data_key, generic_ussd_session, interval, max_retry)
assert f'Data poller with max retry at: {max_retry}. Checking for every: {interval} seconds.' in caplog.text
assert f'Session data poller for: {resource_name} with max retry at: {max_retry}. Checking for every: {interval} seconds.' in caplog.text
assert f'Resource: {expected_value} now available.' in caplog.text

View File

@ -10,13 +10,16 @@ from chainlib.hash import strip_0x
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.chain import Chain
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.cache import cache_data, cache_data_key, get_cached_data from cic_ussd.cache import cache_data, cache_data_key, get_cached_data
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.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.processor.ussd import get_menu, handle_menu, handle_menu_operations from cic_ussd.processor.ussd import (get_menu,
handle_menu,
handle_menu_operations)
from cic_ussd.processor.util import ussd_menu_list
from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
# test imports # test imports
@ -43,7 +46,7 @@ def test_handle_menu(activated_account,
ussd_menu = UssdMenu.find_by_name('exit_pin_blocked') ussd_menu = UssdMenu.find_by_name('exit_pin_blocked')
assert menu_resp.get('name') == ussd_menu.get('name') assert menu_resp.get('name') == ussd_menu.get('name')
menu_resp = handle_menu(pending_account, init_database) menu_resp = handle_menu(pending_account, init_database)
ussd_menu = UssdMenu.find_by_name('initial_language_selection') ussd_menu = UssdMenu.find_by_name('initial_pin_entry')
assert menu_resp.get('name') == ussd_menu.get('name') assert menu_resp.get('name') == ussd_menu.get('name')
identifier = bytes.fromhex(strip_0x(pending_account.blockchain_address)) identifier = bytes.fromhex(strip_0x(pending_account.blockchain_address))
key = cache_data_key(identifier, MetadataPointer.PREFERENCES) key = cache_data_key(identifier, MetadataPointer.PREFERENCES)
@ -75,38 +78,62 @@ def test_get_menu(activated_account,
assert menu_resp.get('name') == ussd_menu.get('name') assert menu_resp.get('name') == ussd_menu.get('name')
def test_handle_menu_operations(activated_account, def test_handle_no_account_menu_operations(celery_session_worker,
cache_preferences,
celery_session_worker,
generic_ussd_session,
init_database,
init_cache, init_cache,
init_database,
load_chain_spec, load_chain_spec,
load_config, load_config,
load_languages,
load_ussd_menu,
mock_account_creation_task_result, mock_account_creation_task_result,
pending_account,
persisted_ussd_session, persisted_ussd_session,
person_metadata,
set_locale_files, set_locale_files,
setup_metadata_request_handler,
setup_metadata_signer,
task_uuid): task_uuid):
# sourcery skip: extract-duplicate-method initial_language_selection = 'ussd.initial_language_selection'
chain_str = Chain.spec.__str__()
phone = phone_number() phone = phone_number()
external_session_id = os.urandom(20).hex() external_session_id = os.urandom(20).hex()
valid_service_codes = load_config.get('USSD_SERVICE_CODE').split(",") valid_service_codes = load_config.get('USSD_SERVICE_CODE').split(",")
preferred_language = i18n.config.get('fallback') preferred_language = i18n.config.get('fallback')
resp = handle_menu_operations(chain_str, external_session_id, phone, None, valid_service_codes[0], init_database, '4444') key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
assert resp == translation_for('ussd.account_creation_prompt', preferred_language) cached_system_languages = get_cached_data(key)
language_list: list = json.loads(cached_system_languages)
fallback = translation_for('helpers.no_language_list', preferred_language)
language_list_sets = ussd_menu_list(fallback=fallback, menu_list=language_list, split=3)
resp = handle_menu_operations(external_session_id, phone, None, valid_service_codes[0], init_database, '')
assert resp == translation_for(initial_language_selection, preferred_language,
first_language_set=language_list_sets[0])
cached_ussd_session = get_cached_data(external_session_id) cached_ussd_session = get_cached_data(external_session_id)
ussd_session = json.loads(cached_ussd_session) ussd_session = json.loads(cached_ussd_session)
assert ussd_session['msisdn'] == phone assert ussd_session['msisdn'] == phone
persisted_ussd_session.external_session_id = external_session_id
persisted_ussd_session.msisdn = phone
persisted_ussd_session.state = initial_language_selection[5:]
init_database.add(persisted_ussd_session)
init_database.commit()
account_creation_prompt = 'ussd.account_creation_prompt'
user_input = '2'
resp = handle_menu_operations(external_session_id, phone, None, valid_service_codes[0], init_database, user_input)
preferred_language = preferred_langauge_from_selection(user_input)
assert resp == translation_for(account_creation_prompt, preferred_language)
task_tracker = init_database.query(TaskTracker).filter_by(task_uuid=task_uuid).first() task_tracker = init_database.query(TaskTracker).filter_by(task_uuid=task_uuid).first()
assert task_tracker.task_uuid == task_uuid assert task_tracker.task_uuid == task_uuid
cached_creation_task_uuid = get_cached_data(task_uuid) cached_creation_task_uuid = get_cached_data(task_uuid)
creation_task_uuid_data = json.loads(cached_creation_task_uuid) creation_task_uuid_data = json.loads(cached_creation_task_uuid)
assert creation_task_uuid_data['status'] == 'PENDING' assert creation_task_uuid_data['status'] == 'PENDING'
def test_handle_account_menu_operations(activated_account,
cache_preferences,
celery_session_worker,
init_database,
load_config,
persisted_ussd_session,
person_metadata,
set_locale_files,
setup_metadata_request_handler,
setup_metadata_signer, ):
valid_service_codes = load_config.get('USSD_SERVICE_CODE').split(",")
identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address)) identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address))
person_metadata_client = PersonMetadata(identifier) person_metadata_client = PersonMetadata(identifier)
with requests_mock.Mocker(real_http=False) as request_mocker: with requests_mock.Mocker(real_http=False) as request_mocker:
@ -117,6 +144,5 @@ def test_handle_menu_operations(activated_account,
phone = activated_account.phone_number phone = activated_account.phone_number
preferred_language = get_cached_preferred_language(activated_account.blockchain_address) preferred_language = get_cached_preferred_language(activated_account.blockchain_address)
persisted_ussd_session.state = 'enter_transaction_recipient' persisted_ussd_session.state = 'enter_transaction_recipient'
resp = handle_menu_operations(chain_str, external_session_id, phone, None, valid_service_codes[0], init_database, '1') resp = handle_menu_operations(external_session_id, phone, None, valid_service_codes[0], init_database, '1')
assert resp == translation_for('ussd.enter_transaction_recipient', preferred_language) assert resp == translation_for('ussd.enter_transaction_recipient', preferred_language)

View File

@ -10,7 +10,10 @@ from cic_types.models.person import get_contact_data_from_vcard
# local imports # local imports
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.metadata import PersonMetadata from cic_ussd.metadata import PersonMetadata
from cic_ussd.processor.util import latest_input, parse_person_metadata, resume_last_ussd_session from cic_ussd.processor.util import (latest_input,
parse_person_metadata,
resume_last_ussd_session,
ussd_menu_list)
from cic_ussd.translation import translation_for from cic_ussd.translation import translation_for
@ -60,3 +63,20 @@ def test_parse_person_metadata(activated_account, cache_person_metadata, cache_p
]) ])
def test_resume_last_ussd_session(expected_menu_name, last_state, load_ussd_menu): def test_resume_last_ussd_session(expected_menu_name, last_state, load_ussd_menu):
assert resume_last_ussd_session(last_state).get('name') == expected_menu_name assert resume_last_ussd_session(last_state).get('name') == expected_menu_name
def test_ussd_menu_list(activated_account, cache_preferences, load_ussd_menu, set_locale_files):
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
fallback = translation_for('helpers.no_transaction_history', preferred_language)
menu_list_sets = ['1. FII 50.0', '2. GFT 60.0', '3. DET 49.99']
split = 3
menu_list = ussd_menu_list(fallback=fallback, menu_list=menu_list_sets, split=split)
menu_list_sets = [menu_list_sets[item:item + split] for item in range(0, len(menu_list), split)]
menu_list_reprs = []
for i in range(split):
try:
menu_list_reprs.append(''.join(f'{list_set_item}\n' for list_set_item in menu_list_sets[i]).rstrip('\n'))
except IndexError:
menu_list_reprs.append(fallback)
assert menu_list == menu_list_reprs

View File

@ -3,8 +3,7 @@ import json
# external imports # external imports
import pytest import pytest
import requests_mock
from chainlib.hash import strip_0x
from cic_types.models.person import Person, get_contact_data_from_vcard from cic_types.models.person import Person, get_contact_data_from_vcard
# local imports # local imports
@ -12,9 +11,7 @@ from cic_ussd.cache import get_cached_data
from cic_ussd.account.maps import gender from cic_ussd.account.maps import gender
from cic_ussd.account.metadata import get_cached_preferred_language from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.db.enum import AccountStatus from cic_ussd.db.enum import AccountStatus
from cic_ussd.metadata import PreferencesMetadata from cic_ussd.state_machine.logic.account import (edit_user_metadata_attribute,
from cic_ussd.state_machine.logic.account import (change_preferred_language,
edit_user_metadata_attribute,
parse_gender, parse_gender,
parse_person_metadata, parse_person_metadata,
save_complete_person_metadata, save_complete_person_metadata,
@ -26,32 +23,6 @@ from cic_ussd.translation import translation_for
# test imports # test imports
@pytest.mark.parametrize('user_input, expected_preferred_language', [
('1', 'en'),
('2', 'sw')
])
def test_change_preferred_language(activated_account,
celery_session_worker,
expected_preferred_language,
init_database,
generic_ussd_session,
mock_response,
preferences,
setup_metadata_request_handler,
user_input):
identifier = bytes.fromhex(strip_0x(activated_account.blockchain_address))
preferences_metadata_client = PreferencesMetadata(identifier)
with requests_mock.Mocker(real_http=False) as requests_mocker:
requests_mocker.register_uri(
'POST', preferences_metadata_client.url, status_code=200, reason='OK', json=mock_response
)
state_machine_data = (user_input, generic_ussd_session, activated_account, init_database)
res = change_preferred_language(state_machine_data)
init_database.commit()
assert res.id is not None
assert activated_account.preferred_language == expected_preferred_language
@pytest.mark.parametrize('user_input', [ @pytest.mark.parametrize('user_input', [
'1', '1',
'2', '2',

View File

@ -0,0 +1,52 @@
# standard imports
import json
# external imports
import requests_mock
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.metadata import PreferencesMetadata
from cic_ussd.state_machine.logic.language import (change_preferred_language,
is_valid_language_selection,
preferred_langauge_from_selection,
process_language_selection)
# test imports
def test_change_preferred_language(activated_account,
cached_ussd_session,
celery_session_worker,
init_database,
load_languages,
mocker,
setup_metadata_signer,
setup_metadata_request_handler):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
preferences = {
'preferred_language': 'en'
}
ussd_session['data'] = preferences
mock_add_preferences_metadata = mocker.patch('cic_ussd.tasks.metadata.add_preferences_metadata.apply_async')
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = bytes.fromhex(activated_account.blockchain_address)
metadata_client = PreferencesMetadata(identifier=identifier)
request_mocker.register_uri('POST', metadata_client.url, status_code=201, reason='CREATED', json=preferences)
state_machine_data = ('1', ussd_session, activated_account, init_database)
change_preferred_language(state_machine_data)
mock_add_preferences_metadata.assert_called_with(
(activated_account.blockchain_address, preferences), {}, queue='cic-ussd')
def test_is_valid_language_selection(activated_account,
generic_ussd_session,
init_cache,
init_database,
load_languages):
state_machine_data = ('1', generic_ussd_session, activated_account, init_database)
assert is_valid_language_selection(state_machine_data) is True
state_machine_data = ('12', generic_ussd_session, activated_account, init_database)
assert is_valid_language_selection(state_machine_data) is False

View File

@ -9,7 +9,10 @@ from cic_ussd.state_machine.logic.menu import (menu_one_selected,
menu_four_selected, menu_four_selected,
menu_five_selected, menu_five_selected,
menu_six_selected, menu_six_selected,
menu_nine_selected,
menu_zero_zero_selected, menu_zero_zero_selected,
menu_eleven_selected,
menu_twenty_two_selected,
menu_ninety_nine_selected) menu_ninety_nine_selected)
# test imports # test imports
@ -29,8 +32,14 @@ def test_menu_selection(init_database, pending_account, persisted_ussd_session):
assert menu_five_selected(('e', ussd_session, pending_account, init_database)) is False assert menu_five_selected(('e', ussd_session, pending_account, init_database)) is False
assert menu_six_selected(('6', ussd_session, pending_account, init_database)) is True assert menu_six_selected(('6', ussd_session, pending_account, init_database)) is True
assert menu_six_selected(('8', ussd_session, pending_account, init_database)) is False assert menu_six_selected(('8', ussd_session, pending_account, init_database)) is False
assert menu_nine_selected(('9', ussd_session, pending_account, init_database)) is True
assert menu_nine_selected(('-', ussd_session, pending_account, init_database)) is False
assert menu_zero_zero_selected(('00', ussd_session, pending_account, init_database)) is True assert menu_zero_zero_selected(('00', ussd_session, pending_account, init_database)) is True
assert menu_zero_zero_selected(('/', ussd_session, pending_account, init_database)) is False assert menu_zero_zero_selected(('/', ussd_session, pending_account, init_database)) is False
assert menu_eleven_selected(('11', ussd_session, pending_account, init_database)) is True
assert menu_eleven_selected(('*', ussd_session, pending_account, init_database)) is False
assert menu_twenty_two_selected(('22', ussd_session, pending_account, init_database)) is True
assert menu_twenty_two_selected(('5', ussd_session, pending_account, init_database)) is False
assert menu_ninety_nine_selected(('99', ussd_session, pending_account, init_database)) is True assert menu_ninety_nine_selected(('99', ussd_session, pending_account, init_database)) is True
assert menu_ninety_nine_selected(('d', ussd_session, pending_account, init_database)) is False assert menu_ninety_nine_selected(('d', ussd_session, pending_account, init_database)) is False

View File

@ -0,0 +1,221 @@
# standard imports
import json
# external imports
import requests_mock
# local imports
from cic_ussd.account.guardianship import Guardianship
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.db.models.account import Account
from cic_ussd.metadata import PersonMetadata
from cic_ussd.state_machine.logic.pin_guard import (add_pin_guardian,
is_dialers_pin_guardian,
is_others_pin_guardian,
is_set_pin_guardian,
remove_pin_guardian,
initiate_pin_reset,
save_guardian_to_session_data,
save_guarded_account_session_data,
retrieve_person_metadata,
is_valid_guardian_addition)
from cic_ussd.translation import translation_for
def test_save_guardian_to_session_data(activated_account,
cached_ussd_session,
celery_session_worker,
guardian_account,
init_cache,
init_database):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['msisdn'] = activated_account.phone_number
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
save_guardian_to_session_data(state_machine_data)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
assert ussd_session.get('data').get('guardian_phone_number') == guardian_account.phone_number
def test_save_guarded_account_session_data(activated_account,
cached_ussd_session,
celery_session_worker,
guardian_account,
init_cache,
init_database):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['msisdn'] = guardian_account.phone_number
state_machine_data = (activated_account.phone_number, ussd_session, guardian_account, init_database)
save_guarded_account_session_data(state_machine_data)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
assert ussd_session.get('data').get('guarded_account_phone_number') == activated_account.phone_number
def test_retrieve_person_metadata(activated_account,
cached_ussd_session,
celery_session_worker,
guardian_account,
init_cache,
init_database,
mocker,
person_metadata,
setup_metadata_request_handler,
setup_metadata_signer):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['msisdn'] = activated_account.phone_number
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
mocker_query_person_metadata = mocker.patch('cic_ussd.tasks.metadata.query_person_metadata.apply_async')
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = bytes.fromhex(activated_account.blockchain_address)
metadata_client = PersonMetadata(identifier)
request_mocker.register_uri('GET', metadata_client.url, json=person_metadata, reason='OK', status_code=200)
retrieve_person_metadata(state_machine_data)
mocker_query_person_metadata.assert_called_with((guardian_account.blockchain_address,), {}, queue='cic-ussd')
def test_is_valid_guardian_addition(activated_account,
cache_preferences,
cached_ussd_session,
celery_session_worker,
init_cache,
init_database,
guardian_account,
load_languages,
load_ussd_menu,
set_locale_files,
setup_guardianship):
blockchain_address = activated_account.blockchain_address
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
assert is_valid_guardian_addition(state_machine_data) is True
state_machine_data = (activated_account.phone_number, ussd_session, activated_account, init_database)
assert is_valid_guardian_addition(state_machine_data) is False
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
preferred_language = get_cached_preferred_language(blockchain_address)
failure_reason = translation_for('helpers.error.is_initiator', preferred_language)
assert ussd_session.get('data').get('failure_reason') == failure_reason
state_machine_data = (Guardianship.guardians[0], ussd_session, activated_account, init_database)
assert is_valid_guardian_addition(state_machine_data) is False
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
preferred_language = get_cached_preferred_language(blockchain_address)
failure_reason = translation_for('helpers.error.is_existent_guardian', preferred_language)
assert ussd_session.get('data').get('failure_reason') == failure_reason
def test_add_pin_guardian(activated_account, generic_ussd_session, guardian_account, init_database):
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
state_machine_data = ('', generic_ussd_session, activated_account, init_database)
add_pin_guardian(state_machine_data)
account = Account.get_by_phone_number(activated_account.phone_number, init_database)
assert account.get_guardians()[0] == guardian_account.phone_number
def test_is_set_pin_guardian(activated_account,
cache_preferences,
cached_ussd_session,
celery_session_worker,
init_cache,
init_database,
guardian_account,
load_languages,
load_ussd_menu,
set_locale_files,
setup_guardianship):
blockchain_address = activated_account.blockchain_address
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
preferred_language = get_cached_preferred_language(blockchain_address)
assert is_set_pin_guardian(activated_account, guardian_account.phone_number, preferred_language, init_database,
ussd_session) is False
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
failure_reason = translation_for('helpers.error.is_not_existent_guardian', preferred_language)
assert ussd_session.get('data').get('failure_reason') == failure_reason
assert is_set_pin_guardian(activated_account, Guardianship.guardians[0], preferred_language, init_database,
ussd_session) is True
assert is_set_pin_guardian(activated_account, activated_account.phone_number, preferred_language, init_database,
ussd_session) is False
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
failure_reason = translation_for('helpers.error.is_initiator', preferred_language)
assert ussd_session.get('data').get('failure_reason') == failure_reason
def test_is_dialers_pin_guardian(activated_account,
cache_preferences,
cached_ussd_session,
celery_session_worker,
init_database,
guardian_account):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
assert is_dialers_pin_guardian(state_machine_data) is False
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
state_machine_data = (guardian_account.phone_number, ussd_session, activated_account, init_database)
assert is_dialers_pin_guardian(state_machine_data) is True
def test_is_others_pin_guardian(activated_account,
cache_preferences,
cached_ussd_session,
celery_session_worker,
init_database,
guardian_account):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
state_machine_data = (activated_account.phone_number, ussd_session, guardian_account, init_database)
assert is_others_pin_guardian(state_machine_data) is False
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
state_machine_data = (activated_account.phone_number, ussd_session, guardian_account, init_database)
assert is_others_pin_guardian(state_machine_data) is True
def test_remove_pin_guardian(activated_account, generic_ussd_session, guardian_account, init_database):
generic_ussd_session['data'] = {'guardian_phone_number': guardian_account.phone_number}
activated_account.add_guardian(guardian_account.phone_number)
init_database.flush()
assert activated_account.get_guardians()[0] == guardian_account.phone_number
state_machine_data = ('', generic_ussd_session, activated_account, init_database)
remove_pin_guardian(state_machine_data)
assert len(activated_account.get_guardians()) == 0
def test_initiate_pin_reset(activated_account,
cache_preferences,
celery_session_worker,
cached_ussd_session,
guardian_account,
init_cache,
init_database,
load_ussd_menu,
mock_notifier_api,
set_locale_files):
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['data'] = {'guarded_account_phone_number': activated_account.phone_number}
state_machine_data = ('', ussd_session, guardian_account, init_database)
initiate_pin_reset(state_machine_data)
blockchain_address = activated_account.blockchain_address
preferred_language = get_cached_preferred_language(blockchain_address)
message = translation_for('sms.pin_reset_initiated', preferred_language, pin_initiator=guardian_account.standard_metadata_id())
assert mock_notifier_api.get('message') == message
assert mock_notifier_api.get('recipient') == activated_account.phone_number

View File

@ -23,6 +23,7 @@ def test_upsell_unregistered_recipient(activated_account,
load_support_phone, load_support_phone,
mock_notifier_api, mock_notifier_api,
set_locale_files, set_locale_files,
set_active_token,
valid_recipient): valid_recipient):
cached_ussd_session.set_data('recipient_phone_number', valid_recipient.phone_number) cached_ussd_session.set_data('recipient_phone_number', valid_recipient.phone_number)
state_machine_data = ('', cached_ussd_session.to_json(), activated_account, init_database) state_machine_data = ('', cached_ussd_session.to_json(), activated_account, init_database)

View File

@ -0,0 +1,69 @@
# standard imports
import json
# external imports
from cic_types.condiments import MetadataPointer
# local imports
from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.state_machine.logic.tokens import (is_valid_token_selection,
process_token_selection,
set_selected_active_token)
from cic_ussd.account.tokens import get_cached_token_data_list
# test imports
def test_is_valid_token_selection(activated_account,
cache_token_data_list,
cache_token_symbol_list,
cached_ussd_session,
init_cache,
init_database):
cached_token_data_list = get_cached_token_data_list(activated_account.blockchain_address)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['data'] = {'account_tokens_list': cached_token_data_list}
state_machine_data = ('GFT', ussd_session, activated_account, init_database)
assert is_valid_token_selection(state_machine_data) is True
state_machine_data = ('1', ussd_session, activated_account, init_database)
assert is_valid_token_selection(state_machine_data) is True
state_machine_data = ('3', ussd_session, activated_account, init_database)
assert is_valid_token_selection(state_machine_data) is False
def test_process_token_selection(activated_account,
cache_token_data_list,
cache_token_symbol_list,
cached_ussd_session,
celery_session_worker,
init_cache,
init_database):
cached_token_data_list = get_cached_token_data_list(activated_account.blockchain_address)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['data'] = {'account_tokens_list': cached_token_data_list}
state_machine_data = ('GFT', ussd_session, activated_account, init_database)
process_token_selection(state_machine_data)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
assert ussd_session.get('data').get('selected_token').get('symbol') == 'GFT'
def test_set_selected_active_token(activated_account,
cache_token_data_list,
cache_token_symbol_list,
cached_ussd_session,
init_cache,
init_database):
cached_token_data_list = get_cached_token_data_list(activated_account.blockchain_address)
ussd_session = get_cached_data(cached_ussd_session.external_session_id)
ussd_session = json.loads(ussd_session)
ussd_session['data'] = {'selected_token': cached_token_data_list[0]}
state_machine_data = ('GFT', ussd_session, activated_account, init_database)
set_selected_active_token(state_machine_data)
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_ACTIVE)
active_token = get_cached_data(key)
assert active_token == 'GFT'

View File

@ -3,13 +3,12 @@ import json
# external imports # external imports
import pytest import pytest
import requests_mock
from chainlib.hash import strip_0x
# local imports # local imports
from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.tokens import get_active_token_symbol, get_cached_token_data
from cic_ussd.account.transaction import to_wei from cic_ussd.account.transaction import to_wei
from cic_ussd.cache import get_cached_data from cic_ussd.cache import get_cached_data
from cic_ussd.metadata import PersonMetadata
from cic_ussd.state_machine.logic.transaction import (is_valid_recipient, from cic_ussd.state_machine.logic.transaction import (is_valid_recipient,
is_valid_transaction_amount, is_valid_transaction_amount,
has_sufficient_balance, has_sufficient_balance,
@ -18,7 +17,6 @@ from cic_ussd.state_machine.logic.transaction import (is_valid_recipient,
save_recipient_phone_to_session_data, save_recipient_phone_to_session_data,
save_transaction_amount_to_session_data) save_transaction_amount_to_session_data)
# test imports # test imports
@ -49,17 +47,18 @@ def test_is_valid_transaction_amount(activated_account, amount, expected_result,
]) ])
def test_has_sufficient_balance(activated_account, def test_has_sufficient_balance(activated_account,
cache_balances, cache_balances,
cache_default_token_data, cache_token_data,
expected_result, expected_result,
generic_ussd_session, generic_ussd_session,
init_database, init_database,
set_active_token,
value): value):
state_machine_data = (value, generic_ussd_session, activated_account, init_database) state_machine_data = (value, generic_ussd_session, activated_account, init_database)
assert has_sufficient_balance(state_machine_data=state_machine_data) == expected_result assert has_sufficient_balance(state_machine_data=state_machine_data) == expected_result
def test_process_transaction_request(activated_account, def test_process_transaction_request(activated_account,
cache_default_token_data, cache_token_data,
cached_ussd_session, cached_ussd_session,
celery_session_worker, celery_session_worker,
init_cache, init_cache,
@ -67,7 +66,12 @@ def test_process_transaction_request(activated_account,
load_chain_spec, load_chain_spec,
load_config, load_config,
mock_transfer_api, mock_transfer_api,
set_active_token,
valid_recipient): valid_recipient):
blockchain_address = activated_account.blockchain_address
token_symbol = get_active_token_symbol(blockchain_address)
token_data = get_cached_token_data(blockchain_address, token_symbol)
decimals = token_data.get("decimals")
cached_ussd_session.set_data('recipient_phone_number', valid_recipient.phone_number) cached_ussd_session.set_data('recipient_phone_number', valid_recipient.phone_number)
cached_ussd_session.set_data('transaction_amount', '50') cached_ussd_session.set_data('transaction_amount', '50')
ussd_session = get_cached_data(cached_ussd_session.external_session_id) ussd_session = get_cached_data(cached_ussd_session.external_session_id)
@ -76,7 +80,7 @@ def test_process_transaction_request(activated_account,
process_transaction_request(state_machine_data) process_transaction_request(state_machine_data)
assert mock_transfer_api['from_address'] == activated_account.blockchain_address assert mock_transfer_api['from_address'] == activated_account.blockchain_address
assert mock_transfer_api['to_address'] == valid_recipient.blockchain_address assert mock_transfer_api['to_address'] == valid_recipient.blockchain_address
assert mock_transfer_api['value'] == to_wei(50) assert mock_transfer_api['value'] == to_wei(decimals, 50)
assert mock_transfer_api['token_symbol'] == load_config.get('TEST_TOKEN_SYMBOL') assert mock_transfer_api['token_symbol'] == load_config.get('TEST_TOKEN_SYMBOL')

View File

@ -6,8 +6,10 @@ def test_state_machine(activated_account_ussd_session,
celery_session_worker, celery_session_worker,
init_database, init_database,
init_state_machine, init_state_machine,
pending_account): load_languages,
pending_account,
set_locale_files):
state_machine = UssdStateMachine(activated_account_ussd_session) state_machine = UssdStateMachine(activated_account_ussd_session)
state_machine.scan_data(('1', activated_account_ussd_session, pending_account, init_database)) state_machine.scan_data(('1', activated_account_ussd_session, pending_account, init_database))
assert state_machine.__repr__() == f'<KenyaUssdStateMachine: {state_machine.state}>' assert state_machine.__repr__() == f'<KenyaUssdStateMachine: {state_machine.state}>'
assert state_machine.state == 'initial_pin_entry' assert state_machine.state == 'account_creation_prompt'

View File

@ -4,15 +4,18 @@ import json
# external imports # external imports
import celery import celery
import pytest import pytest
import requests_mock
from chainlib.hash import strip_0x from chainlib.hash import strip_0x
from cic_types.condiments import MetadataPointer from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.statement import filter_statement_transactions from cic_ussd.account.statement import filter_statement_transactions
from cic_ussd.account.tokens import collate_token_metadata
from cic_ussd.account.transaction import transaction_actors from cic_ussd.account.transaction import transaction_actors
from cic_ussd.cache import cache_data_key, get_cached_data from cic_ussd.cache import cache_data_key, get_cached_data
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
from cic_ussd.error import AccountCreationDataNotFound from cic_ussd.error import AccountCreationDataNotFound
from cic_ussd.metadata import TokenMetadata
# test imports # test imports
@ -22,11 +25,13 @@ from tests.helpers.accounts import blockchain_address
def test_account_creation_callback(account_creation_data, def test_account_creation_callback(account_creation_data,
cache_account_creation_data, cache_account_creation_data,
celery_session_worker, celery_session_worker,
cache_default_token_data,
custom_metadata, custom_metadata,
init_cache, init_cache,
init_database, init_database,
load_chain_spec, load_chain_spec,
mocker, mocker,
preferences,
setup_metadata_request_handler, setup_metadata_request_handler,
setup_metadata_signer): setup_metadata_signer):
phone_number = account_creation_data.get('phone_number') phone_number = account_creation_data.get('phone_number')
@ -48,10 +53,12 @@ def test_account_creation_callback(account_creation_data,
cached_account_creation_data = get_cached_data(task_uuid) cached_account_creation_data = get_cached_data(task_uuid)
cached_account_creation_data = json.loads(cached_account_creation_data) cached_account_creation_data = json.loads(cached_account_creation_data)
assert cached_account_creation_data.get('status') == account_creation_data.get('status') assert cached_account_creation_data.get('status') == account_creation_data.get('status')
mock_add_preferences_metadata = mocker.patch('cic_ussd.tasks.metadata.add_preferences_metadata.apply_async')
mock_add_phone_pointer = mocker.patch('cic_ussd.tasks.metadata.add_phone_pointer.apply_async') mock_add_phone_pointer = mocker.patch('cic_ussd.tasks.metadata.add_phone_pointer.apply_async')
mock_add_custom_metadata = mocker.patch('cic_ussd.tasks.metadata.add_custom_metadata.apply_async') mock_add_custom_metadata = mocker.patch('cic_ussd.tasks.metadata.add_custom_metadata.apply_async')
preferred_language = preferences.get('preferred_language')
s_account_creation_callback = celery.signature( s_account_creation_callback = celery.signature(
'cic_ussd.tasks.callback_handler.account_creation_callback', [result, '', 0] 'cic_ussd.tasks.callback_handler.account_creation_callback', [result, preferred_language, 0]
) )
s_account_creation_callback.apply_async().get() s_account_creation_callback.apply_async().get()
account = init_database.query(Account).filter_by(phone_number=phone_number).first() account = init_database.query(Account).filter_by(phone_number=phone_number).first()
@ -59,6 +66,7 @@ def test_account_creation_callback(account_creation_data,
cached_account_creation_data = get_cached_data(task_uuid) cached_account_creation_data = get_cached_data(task_uuid)
cached_account_creation_data = json.loads(cached_account_creation_data) cached_account_creation_data = json.loads(cached_account_creation_data)
assert cached_account_creation_data.get('status') == 'CREATED' assert cached_account_creation_data.get('status') == 'CREATED'
mock_add_preferences_metadata.assert_called_with((result, preferences), {}, queue='cic-ussd')
mock_add_phone_pointer.assert_called_with((result, phone_number), {}, queue='cic-ussd') mock_add_phone_pointer.assert_called_with((result, phone_number), {}, queue='cic-ussd')
mock_add_custom_metadata.assert_called_with((result, custom_metadata), {}, queue='cic-ussd') mock_add_custom_metadata.assert_called_with((result, custom_metadata), {}, queue='cic-ussd')
@ -117,12 +125,46 @@ def test_statement_callback(activated_account, mocker, transactions_list):
(activated_account.blockchain_address, sender_transaction), {}, queue='cic-ussd') (activated_account.blockchain_address, sender_transaction), {}, queue='cic-ussd')
def test_token_data_callback(activated_account,
cache_token_data,
cache_token_meta_symbol,
cache_token_proof_symbol,
celery_session_worker,
default_token_data,
init_cache,
token_meta_symbol,
token_symbol):
blockchain_address = activated_account.blockchain_address
identifier = token_symbol.encode('utf-8')
status_code = 1
with pytest.raises(ValueError) as error:
s_token_data_callback = celery.signature(
'cic_ussd.tasks.callback_handler.token_data_callback',
[[default_token_data], blockchain_address, status_code])
s_token_data_callback.apply_async().get()
assert str(error.value) == f'Unexpected status code: {status_code}.'
token_data_key = cache_data_key([bytes.fromhex(blockchain_address), identifier], MetadataPointer.TOKEN_DATA)
token_meta_key = cache_data_key(identifier, MetadataPointer.TOKEN_META_SYMBOL)
token_info_key = cache_data_key(identifier, MetadataPointer.TOKEN_PROOF_SYMBOL)
token_meta = get_cached_data(token_meta_key)
token_meta = json.loads(token_meta)
token_info = get_cached_data(token_info_key)
token_info = json.loads(token_info)
token_data = collate_token_metadata(token_info=token_info, token_metadata=token_meta)
token_data = {**token_data, **default_token_data}
cached_token_data = json.loads(get_cached_data(token_data_key))
for key, value in token_data.items():
assert token_data[key] == cached_token_data[key]
def test_transaction_balances_callback(activated_account, def test_transaction_balances_callback(activated_account,
balances, balances,
cache_balances, cache_balances,
cache_default_token_data, cache_token_data,
cache_person_metadata, cache_person_metadata,
cache_preferences, cache_preferences,
celery_session_worker,
load_chain_spec, load_chain_spec,
mocker, mocker,
preferences, preferences,
@ -157,7 +199,16 @@ def test_transaction_balances_callback(activated_account,
mocked_chain.assert_called() mocked_chain.assert_called()
def test_transaction_callback(load_chain_spec, mock_async_balance_api_query, transaction_result): def test_transaction_callback(cache_token_data,
celery_session_worker,
default_token_data,
init_cache,
load_chain_spec,
mock_async_balance_api_query,
token_symbol,
token_meta_symbol,
token_proof_symbol,
transaction_result):
status_code = 1 status_code = 1
with pytest.raises(ValueError) as error: with pytest.raises(ValueError) as error:
s_transaction_callback = celery.signature( s_transaction_callback = celery.signature(
@ -166,6 +217,12 @@ def test_transaction_callback(load_chain_spec, mock_async_balance_api_query, tra
s_transaction_callback.apply_async().get() s_transaction_callback.apply_async().get()
assert str(error.value) == f'Unexpected status code: {status_code}.' assert str(error.value) == f'Unexpected status code: {status_code}.'
with requests_mock.Mocker(real_http=False) as request_mocker:
identifier = token_symbol.encode('utf-8')
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_META_SYMBOL)
request_mocker.register_uri('GET', metadata_client.url, json=token_meta_symbol, status_code=200, reason='OK')
metadata_client = TokenMetadata(identifier, cic_type=MetadataPointer.TOKEN_PROOF_SYMBOL)
request_mocker.register_uri('GET', metadata_client.url, json=token_proof_symbol, status_code=200, reason='OK')
status_code = 0 status_code = 0
s_transaction_callback = celery.signature( s_transaction_callback = celery.signature(
'cic_ussd.tasks.callback_handler.transaction_callback', 'cic_ussd.tasks.callback_handler.transaction_callback',

View File

@ -14,13 +14,14 @@ from cic_ussd.translation import translation_for
def test_transaction(cache_default_token_data, def test_transaction(cache_default_token_data,
cache_token_data,
celery_session_worker, celery_session_worker,
load_support_phone, load_support_phone,
mock_notifier_api, mock_notifier_api,
notification_data, notification_data,
set_locale_files): set_locale_files):
notification_data['transaction_type'] = 'transfer' notification_data['transaction_type'] = 'transfer'
amount = from_wei(notification_data.get('token_value')) amount = from_wei(6, notification_data.get('token_value'))
balance = notification_data.get('available_balance') balance = notification_data.get('available_balance')
phone_number = notification_data.get('phone_number') phone_number = notification_data.get('phone_number')
preferred_language = notification_data.get('preferred_language') preferred_language = notification_data.get('preferred_language')

View File

@ -52,6 +52,11 @@ def test_cache_statement(activated_account,
cached_statement = get_cached_data(key) cached_statement = get_cached_data(key)
cached_statement = json.loads(cached_statement) cached_statement = json.loads(cached_statement)
assert len(cached_statement) == 1 assert len(cached_statement) == 1
sender_transaction['token_value'] = 60.0
s_parse_transaction = celery.signature(
'cic_ussd.tasks.processor.parse_transaction', [sender_transaction])
result = s_parse_transaction.apply_async().get()
s_cache_statement = celery.signature( s_cache_statement = celery.signature(
'cic_ussd.tasks.processor.cache_statement', [result, activated_account.blockchain_address] 'cic_ussd.tasks.processor.cache_statement', [result, activated_account.blockchain_address]
) )

View File

@ -9,7 +9,7 @@ from cic_ussd.notifications import Notifier
@pytest.mark.parametrize("key, preferred_language, recipient, expected_message", [ @pytest.mark.parametrize("key, preferred_language, recipient, expected_message", [
("ussd.exit", "en", "+254712345678", "END Thank you for using the service."), ("ussd.exit", "en", "+254712345678", "END Thank you for using the service."),
("ussd.exit", "sw", "+254712345678", "END Asante kwa kutumia huduma.") ("ussd.exit", "sw", "+254712345678", "END Asante kwa kutumia huduma")
]) ])
def test_send_sms_notification(celery_session_worker, def test_send_sms_notification(celery_session_worker,
expected_message, expected_message,

View File

@ -17,5 +17,5 @@ def test_translation_for(set_locale_files):
key='ussd.exit_invalid_request', key='ussd.exit_invalid_request',
preferred_language='sw' preferred_language='sw'
) )
assert swahili_translation == 'END Chaguo si sahihi.' assert swahili_translation == 'END Chaguo si sahihi'
assert english_translation == 'END Invalid request.' assert english_translation == 'END Invalid request.'

View File

@ -8,6 +8,7 @@ from cic_types.condiments import MetadataPointer
# local imports # local imports
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.tokens import set_active_token
from cic_ussd.cache import cache_data, cache_data_key from cic_ussd.cache import cache_data, cache_data_key
from cic_ussd.db.enum import AccountStatus from cic_ussd.db.enum import AccountStatus
from cic_ussd.db.models.account import Account from cic_ussd.db.models.account import Account
@ -36,6 +37,16 @@ def activated_account(init_database, set_fernet_key):
return account return account
@pytest.fixture(scope='function')
def guardian_account(init_database, set_fernet_key):
account = Account(blockchain_address(), phone_number())
account.create_password('0000')
account.activate_account()
init_database.add(account)
init_database.commit()
return account
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def balances(): def balances():
return [{ return [{
@ -53,13 +64,22 @@ def cache_account_creation_data(init_cache, account_creation_data):
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def cache_balances(activated_account, balances, init_cache): def cache_balances(activated_account, balances, init_cache, token_symbol):
identifier = bytes.fromhex(activated_account.blockchain_address) identifier = [bytes.fromhex(activated_account.blockchain_address), token_symbol.encode('utf-8')]
balances = json.dumps(balances[0]) balances = json.dumps(balances[0])
key = cache_data_key(identifier, MetadataPointer.BALANCES) key = cache_data_key(identifier, MetadataPointer.BALANCES)
cache_data(key, balances) cache_data(key, balances)
@pytest.fixture(scope='function')
def cache_adjusted_balances(activated_account, balances, init_cache, token_symbol):
identifier = bytes.fromhex(activated_account.blockchain_address)
balances_identifier = [identifier, token_symbol.encode('utf-8')]
key = cache_data_key(balances_identifier, MetadataPointer.BALANCES_ADJUSTED)
adjusted_balance = 45931650.64654012
cache_data(key, adjusted_balance)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def cache_default_token_data(default_token_data, init_cache, load_chain_spec): def cache_default_token_data(default_token_data, init_cache, load_chain_spec):
chain_str = Chain.spec.__str__() chain_str = Chain.spec.__str__()
@ -68,6 +88,113 @@ def cache_default_token_data(default_token_data, init_cache, load_chain_spec):
cache_data(key, data) cache_data(key, data)
@pytest.fixture(scope='function')
def set_active_token(activated_account, init_cache, token_symbol):
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier, MetadataPointer.TOKEN_ACTIVE)
cache_data(key=key, data=token_symbol)
@pytest.fixture(scope='function')
def cache_token_data(activated_account, init_cache, token_data):
identifier = [bytes.fromhex(activated_account.blockchain_address), token_data.get('symbol').encode('utf-8')]
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
cache_data(key=key, data=json.dumps(token_data))
@pytest.fixture(scope='function')
def cache_token_symbol_list(activated_account, init_cache, token_symbol):
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier=identifier, salt=MetadataPointer.TOKEN_SYMBOLS_LIST)
token_symbols_list = [token_symbol]
cache_data(key, json.dumps(token_symbols_list))
@pytest.fixture(scope='function')
def cache_token_data_list(activated_account, init_cache, token_data):
identifier = bytes.fromhex(activated_account.blockchain_address)
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA_LIST)
token_data_list = [token_data]
cache_data(key, json.dumps(token_data_list))
@pytest.fixture(scope='function')
def token_meta_symbol():
return {
"contact": {
"phone": "+254700000000",
"email": "info@grassrootseconomics.org"
},
"country_code": "KE",
"location": "Kilifi",
"name": "GRASSROOTS ECONOMICS"
}
@pytest.fixture(scope='function')
def token_proof_symbol():
return {
"description": "Community support",
"issuer": "Grassroots Economics",
"namespace": "ge",
"proofs": [
"0x4746540000000000000000000000000000000000000000000000000000000000",
"1f0f0e3e9db80eeaba22a9d4598e454be885855d6048545546fd488bb709dc2f"
],
"version": 0
}
@pytest.fixture(scope='function')
def token_list_entries():
return [
{
'name': 'Fee',
'symbol': 'FII',
'issuer': 'Foo',
'contact': {'phone': '+254712345678'},
'location': 'Fum',
'balance': 50.0
},
{
'name': 'Giftable Token',
'symbol': 'GFT',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'
},
'location': 'Fum',
'balance': 60.0
},
{
'name': 'Demurrage Token',
'symbol': 'DET',
'issuer': 'Grassroots Economics',
'contact': {
'phone': '+254700000000',
'email': 'info@grassrootseconomics.org'
},
'location': 'Fum',
'balance': 49.99
}
]
@pytest.fixture(scope='function')
def cache_token_meta_symbol(token_meta_symbol, token_symbol):
identifier = token_symbol.encode('utf-8')
key = cache_data_key(identifier, MetadataPointer.TOKEN_META_SYMBOL)
cache_data(key, json.dumps(token_meta_symbol))
@pytest.fixture(scope='function')
def cache_token_proof_symbol(token_proof_symbol, token_symbol):
identifier = token_symbol.encode('utf-8')
key = cache_data_key(identifier, MetadataPointer.TOKEN_PROOF_SYMBOL)
cache_data(key, json.dumps(token_proof_symbol))
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def cache_person_metadata(activated_account, init_cache, person_metadata): def cache_person_metadata(activated_account, init_cache, person_metadata):
identifier = bytes.fromhex(activated_account.blockchain_address) identifier = bytes.fromhex(activated_account.blockchain_address)
@ -101,9 +228,32 @@ def custom_metadata():
def default_token_data(token_symbol): def default_token_data(token_symbol):
return { return {
'symbol': token_symbol, 'symbol': token_symbol,
'address': blockchain_address(), 'address': '32e860c2a0645d1b7b005273696905f5d6dc5d05',
'name': 'Giftable', 'name': 'Giftable Token',
'decimals': 6 'decimals': 6,
"converters": []
}
@pytest.fixture(scope='function')
def token_data():
return {
"description": "Community support",
"issuer": "Grassroots Economics",
"location": "Kilifi",
"contact": {
"phone": "+254700000000",
"email": "info@grassrootseconomics.org"
},
"decimals": 6,
"name": "Giftable Token",
"symbol": "GFT",
"address": "32e860c2a0645d1b7b005273696905f5d6dc5d05",
"proofs": [
"0x4746540000000000000000000000000000000000000000000000000000000000",
"1f0f0e3e9db80eeaba22a9d4598e454be885855d6048545546fd488bb709dc2f"
],
"converters": []
} }

View File

@ -2,14 +2,18 @@
# external imports # external imports
import pytest import pytest
from pytest_redis import factories
# local imports # local imports
from cic_ussd.cache import Cache from cic_ussd.cache import Cache
from cic_ussd.session.ussd_session import UssdSession from cic_ussd.session.ussd_session import UssdSession
redis_test_proc = factories.redis_proc()
redis_db = factories.redisdb('redis_test_proc', decode=True)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def init_cache(redisdb): def init_cache(redis_db):
Cache.store = redisdb Cache.store = redis_db
UssdSession.store = redisdb UssdSession.store = redis_db
return redisdb return redis_db

View File

@ -10,11 +10,13 @@ from confini import Config
# local imports # local imports
from cic_ussd.account.chain import Chain from cic_ussd.account.chain import Chain
from cic_ussd.account.guardianship import Guardianship
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.phone_number import E164Format, Support from cic_ussd.phone_number import E164Format, Support
from cic_ussd.state_machine import UssdStateMachine from cic_ussd.state_machine import UssdStateMachine
from cic_ussd.translation import generate_locale_files, Languages
from cic_ussd.validator import validate_presence from cic_ussd.validator import validate_presence
logg = logging.getLogger(__name__) logg = logging.getLogger(__name__)
@ -39,6 +41,14 @@ def init_state_machine(load_config):
UssdStateMachine.transitions = json_file_parser(filepath=load_config.get('MACHINE_TRANSITIONS')) UssdStateMachine.transitions = json_file_parser(filepath=load_config.get('MACHINE_TRANSITIONS'))
@pytest.fixture(scope='function')
def load_languages(init_cache, load_config):
validate_presence(load_config.get('LANGUAGES_FILE'))
Languages.load_languages_dict(load_config.get('LANGUAGES_FILE'))
languages = Languages()
languages.cache_system_languages()
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def load_chain_spec(load_config): def load_chain_spec(load_config):
chain_spec = ChainSpec.from_chain_str(load_config.get('CHAIN_SPEC')) chain_spec = ChainSpec.from_chain_str(load_config.get('CHAIN_SPEC'))
@ -75,8 +85,23 @@ def set_fernet_key(load_config):
PasswordEncoder.set_key(load_config.get('APP_PASSWORD_PEPPER')) PasswordEncoder.set_key(load_config.get('APP_PASSWORD_PEPPER'))
@pytest.fixture @pytest.fixture(scope='function')
def set_locale_files(load_config): def setup_guardianship(load_config):
validate_presence(load_config.get('LOCALE_PATH')) guardians_file = os.path.join(root_directory, load_config.get('SYSTEM_GUARDIANS_FILE'))
i18n.load_path.append(load_config.get('LOCALE_PATH')) validate_presence(guardians_file)
Guardianship.load_system_guardians(guardians_file)
@pytest.fixture(scope="session")
def set_locale_files(load_config, tmpdir_factory):
tmpdir = tmpdir_factory.mktemp("var")
tmpdir_path = str(tmpdir)
validate_presence(tmpdir_path)
import cic_translations
package_path = cic_translations.__path__
schema_files = os.path.join(package_path[0], load_config.get("SCHEMA_FILE_PATH"))
generate_locale_files(locale_dir=tmpdir_path,
schema_file_path=schema_files,
translation_builder_path=load_config.get('LOCALE_FILE_BUILDERS'))
i18n.load_path.append(tmpdir_path)
i18n.set('fallback', load_config.get('LOCALE_FALLBACK')) i18n.set('fallback', load_config.get('LOCALE_FALLBACK'))

View File

@ -40,6 +40,7 @@ def statement(activated_account):
'blockchain_address': activated_account.blockchain_address, 'blockchain_address': activated_account.blockchain_address,
'token_symbol': 'GFT', 'token_symbol': 'GFT',
'token_value': 25000000, 'token_value': 25000000,
'token_decimals': 6,
'role': 'sender', 'role': 'sender',
'action_tag': 'Sent', 'action_tag': 'Sent',
'direction_tag': 'To', 'direction_tag': 'To',
@ -63,7 +64,7 @@ def transaction_result(activated_account, load_config, valid_recipient):
'destination_token_symbol': load_config.get('TEST_TOKEN_SYMBOL'), 'destination_token_symbol': load_config.get('TEST_TOKEN_SYMBOL'),
'source_token_decimals': 6, 'source_token_decimals': 6,
'destination_token_decimals': 6, 'destination_token_decimals': 6,
'chain': 'evm:bloxberg:8996' 'chain': load_config.get('CHAIN_SPEC')
} }

View File

@ -1,19 +0,0 @@
keys,en,sw
female,Female,Mwanamke
from,From,Kutoka kwa
male,Male,Mwanaume
not_provided,Not provided,Haijawekwa
no_language_list,No language list,Hamna lugha ya kuchagua
no_transaction_history,No transaction history,Hamna ripoti ya matumizi
no_tokens_list,No tokens to list,Hamna sarafu nyingine
other,Other,Nyingine
received,Received,Ulipokea
sent,Sent,Ulituma
to,To,Kwa
guardians_list_header,Your set guardians are:,Walinzi uliowaongeza ni:
no_guardians_list,No guardians set,Hamna walinzi walioongezwa
error.no_phone_number_provided,No phone number was provided.,Namabari ya simu haijawekwa.
error.no_matching_account,The number provided is not registered.,Nambari uliyoweka haijasajiliwa.
error.is_initiator,Phone number cannot be your own.,Nambari yafaa kuwa tofauti na yako.
error.is_existent_guardian,This phone number is is already added as a guardian.,Namabari hii tayari imeongezwa kama mlinzi wa nambari ya siri.
error.is_not_existent_guardian,Phone number not set as PIN reset guardian.,Nambari hii haijaongezwa kama mlinzi wa nambari ya siri.
1 keys en sw
2 female Female Mwanamke
3 from From Kutoka kwa
4 male Male Mwanaume
5 not_provided Not provided Haijawekwa
6 no_language_list No language list Hamna lugha ya kuchagua
7 no_transaction_history No transaction history Hamna ripoti ya matumizi
8 no_tokens_list No tokens to list Hamna sarafu nyingine
9 other Other Nyingine
10 received Received Ulipokea
11 sent Sent Ulituma
12 to To Kwa
13 guardians_list_header Your set guardians are: Walinzi uliowaongeza ni:
14 no_guardians_list No guardians set Hamna walinzi walioongezwa
15 error.no_phone_number_provided No phone number was provided. Namabari ya simu haijawekwa.
16 error.no_matching_account The number provided is not registered. Nambari uliyoweka haijasajiliwa.
17 error.is_initiator Phone number cannot be your own. Nambari yafaa kuwa tofauti na yako.
18 error.is_existent_guardian This phone number is is already added as a guardian. Namabari hii tayari imeongezwa kama mlinzi wa nambari ya siri.
19 error.is_not_existent_guardian Phone number not set as PIN reset guardian. Nambari hii haijaongezwa kama mlinzi wa nambari ya siri.

View File

@ -1,7 +0,0 @@
keys,en,sw
account_successfully_created,You have been registered on Sarafu Network! To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}.,Umesajiliwa kwa huduma ya Sarafu! Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
received_tokens,Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp} to %{tx_recipient_information}. New balance is %{balance} %{token_symbol}.,Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp} ikapokewa na %{tx_recipient_information}. Salio lako ni %{balance} %{token_symbol}.
sent_tokens,Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp} from %{tx_sender_information}. New balance is %{balance} %{token_symbol}.,Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kutoka kwa %{tx_sender_information}. Salio lako ni %{balance} %{token_symbol}.
terms,"By using the service, you agree to the terms and conditions at http://grassecon.org/tos","Kwa kutumia hii huduma, umekubali sheria na masharti yafuatayo http://grassecon.org/tos"
upsell_unregistered_recipient,%{tx_sender_information} tried to send you %{token_symbol} but you are not registered. To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}.,%{tx_sender_information} amejaribu kukutumia %{token_symbol} lakini hujasajili. Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
pin_reset_initiated,%{pin_initiator} has sent a request to initiate your PIN reset.,%{pin_initiator} ametuma ombi la kubadilisha PIN yako.
1 keys en sw
2 account_successfully_created You have been registered on Sarafu Network! To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}. Umesajiliwa kwa huduma ya Sarafu! Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
3 received_tokens Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp} to %{tx_recipient_information}. New balance is %{balance} %{token_symbol}. Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp} ikapokewa na %{tx_recipient_information}. Salio lako ni %{balance} %{token_symbol}.
4 sent_tokens Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp} from %{tx_sender_information}. New balance is %{balance} %{token_symbol}. Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kutoka kwa %{tx_sender_information}. Salio lako ni %{balance} %{token_symbol}.
5 terms By using the service, you agree to the terms and conditions at http://grassecon.org/tos Kwa kutumia hii huduma, umekubali sheria na masharti yafuatayo http://grassecon.org/tos
6 upsell_unregistered_recipient %{tx_sender_information} tried to send you %{token_symbol} but you are not registered. To use dial *384*96# on Safaricom and *483*96# on other networks. For help %{support_phone}. %{tx_sender_information} amejaribu kukutumia %{token_symbol} lakini hujasajili. Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}.
7 pin_reset_initiated %{pin_initiator} has sent a request to initiate your PIN reset. %{pin_initiator} ametuma ombi la kubadilisha PIN yako.

View File

@ -1,862 +0,0 @@
keys,en,sw,kam,kik,miji,luo,bor
initial_language_selection,"CON Welcome to Sarafu Network
%{first_language_set}
11. Next
00. Exit","CON Karibu Sarafu Network
%{first_language_set}
11. Mbele
00. Ondoka","CON Kalivu Network ya Sarafu
1. English
2. Kiswahili
3. Kikamba
3. Help","CON Karibu Sarafu Network
1. Githungu
2. Githweri
3. Uteithio","CON Karibu Sarafu Network
1. Chizungu
2. Chiswahili
3. Avizwa","CON Machiegni e network mar Sarafu
1. Dho Ngere
2. Dho oswayo
3. Kony","CON Karibu Sarafu Network
1. Afaan ferenji
2. Afaan kiswahili
3. Qarqars"
initial_pin_entry,CON Please enter a new four number PIN for your account.,CON Tafadhali weka pin mpya yenye nambari nne kwa akaunti yako,CON Tafadhali ikia pin yumbya ila ina namba inya kinanduni chaku,CON Ekera namba yaku ya thiri njeru ena numba enna.,CON Ika piniyo ya namba Ne kwa akaunti Yakwako.,CON Kiyie to ket namba ni mopondo e akaont ni.,CON Tafadhal pin hareti kekhae ka namba afuri fulaa akaunti kake
initial_pin_confirmation,CON Enter your four number PIN again,CON Weka PIN yako tena,CON Ikia PIN yaku ingi,CON Ekera namba yaku ya thiri ringi,CON Uyira Kwika pin kaheri.,CON Ket namba ni mopondo kendo,CON Mar dibii pin kekhae
enter_given_name,"CON Enter first name
0. Back",CON Weka jina lako la kwanza,CON Ikia isyitwa yaku ya mbee,CON Ekera retwa rwaku ria mbere,CON Ika dzinaro rakwanza.,CON Ket nyingi mokwongo,CON Makhaa kake ka karaa kor
enter_family_name,"CON Enter family name
0. Back","CON Weka jina lako la mwisho
0. Rudi","CON Ikia isyitwa yaku ya muthya
0. Syoka itina","CON Ekera ritwa rwaku ria mwisho
0. Coka thutha","CON Ika dzinaro ra mwisho
0. Uya Nyuma","CON Ket nyingi mogik.
0. Dog chien","CON Makhaa kake ka egee
0. Dhebii"
enter_date_of_birth,"CON Enter year of birth
0. Back","CON Weka mwaka wa kuzaliwa
0. Rudi","CON Ikia mwaka wa kusyawa
0. Syoka itina","CON Ekera mwaka waku wa guciarwo
0. Coka thutha","CON Ika mwaka wakuvyalwa
0. Uya Nyuma","CON Ket iki mar nyuol
0. Dog chien","CON Gan kake ka athdalat kor
0. Dheebi"
enter_gender,"CON Enter gender
1. Male
2. Female
3. Other
0. Back","CON Weka jinsia yako
1. Mwanaume
2. Mwanamke
3. Nyngine
0. Rudi","CON Ikia gender yaku
1. Mundume
2. Mundumuka
3. Ingi
0. Syoka itina","CON We mudurume kana mutumia
1.Mudurume
2. Mutumia
3. Ingi
0. Coka thutha","CON Ika kala Umulume ama Umuche au vingine.
1. Mulume
2. Muche
3. Vinjine
0. Uya Nyuma","CON Ket kit chwech mari
1. Dichuo
2. Dhako
3. Moko
0. Dog chien","CON Athin Dir mo Dubr
1. Dir
2. Dubr
3. Ka dibii
0. Dheebi"
enter_location,"CON Enter your location,
0. Back","CON Weka eneo lako
0. Rudi","CON Ikia utui waku kana location
0. Syoka itina","CON Ekera kuria uumete
0. Coka thutha","CON Ika enero wombolako.
0. Uya nyuma","CON Ket kumaidake
0. Dog chien","CON Fulaa athin kubat kor
0. Dhebii"
enter_products,"CON Please enter a product or service you offer
0. Back","CON Weka bidhaa ama huduma unauza
0. Rudi","CON Ikia syindu kana huduma ila unenganae
0. Syoka itina","CON Ekera indo kana wira uria urendia
0. Coka thutha","CON Ika Viya ama utu uhendao
0. Uya Nyuma","CON Ket gima iuso kata tich mitimo
0. Dog chien","CON Waan gurgurt okan namaa kenit khes khae
0. Dheebi"
start,"CON Balance %{account_balance} %{account_token_name}
1. Send
2. My Sarafu
3. My Account
4. Help","CON Salio %{account_balance} %{account_token_name}
1. Tuma
2. Sarafu yangu
3. Akaunti yangu
4. Usaidizi","CON Mbalansi kana utyalyo %{account_balance} %{account_token_name}
1. Tuma
2. Kinandu chakwa
3. Utethyo","CON Matigari %{account_balance} %{account_token_name}
1. Tuma
2. Akaunti yaku
3. Uteithio","CON Sazo %{account_balance} %{account_token_name}
1. Huma
2. Akaunti yangu
3. Avizwa","CON Ma Odong' %{account_balance} %{account_token_name}
1. Or
2. Akaont na
3. Kony","CON Salio %{account_balance} %{account_token_name}
1. Erg
2. Akaunti khiy
3. Qarqars"
enter_transaction_recipient,"CON Enter phone number
0. Back","CON Weka nambari ya simu
0. Rudi","CON Ikia namba ya simu
0. Syoka itina","CON Ikira namba ya thimu
0. Coka thutha","CON Ika namba yasimu.
0. Uya Nyuma","CON Ket nambani mar simu
0. Dog chien","CON Namba ta simuu kekhai
0. Dheebi"
enter_transaction_amount,"CON Enter amount
0. Back","CON Weka kiwango
0. Rudi","CON Ikia kiwango
0. Syoka itina","CON Ikira muigana
0. Coka thutha","CON Ika chaasi.
0. Uya nyuma","CON Ket giko mari
0. Dog chien","CON kiwango kekhai
0. Dheebi"
first_account_tokens_set,"CON Choose a number or symbol from your balances:
%{first_account_tokens_set}
0. Back
11. Next
00. Exit","CON Chagua nambari au ishara kutoka kwa salio zako:
%{first_account_tokens_set}
0. Rudi
11. Mbele
00. Ondoka","Sakua Sarafu:
%{token_list}
99. Thi mbee
00. Syoka itina","Shaghura Sarafu:
%{token_list}
99. Mbere
00. Coka thutha","Tsagula Sarafu:
%{token_list}
99. Enderera
00. Uya Nyuma","Yier Sarafu:
%{token_list}
99. Nyime
00. Dog chien","Chaqui Sarafu:
%{token_list}
99. Dhuur
00. Dheebi"
middle_account_tokens_set,"CON Choose a number or symbol from your balances:
%{middle_account_tokens_set}
11. Next
22. Previous
00. Exit","CON Chagua nambari au ishara kutoka kwa salio zako:
%{middle_account_tokens_set}
11. Mbele
22. Rudi
00. Ondoka","Sakura Sarafu:
%{token_list}
99. Thi mbee
00. Syoka itina","Shaghura Sarafu:
%{token_list}
99. Mbere
00. Cooka thutha","Tsagula Sarafu:
%{token_list}
99. Enderera
00. Uya Nyuma","Yier Sarafu:
%{token_list}
99. Nyime
00. Dog chien","Chaqui Sarafu:
%{token_list}
99. Dhuur
00. Dheebi"
last_account_tokens_set,"CON Choose a number or symbol from your balances:
%{last_account_tokens_set}
22. Previous
00. Exit","CON Chagua nambari au ishara kutoka kwa salio zako:
%{last_account_tokens_set}
22. Rudi
00. Ondoka","Sakura sarafu:
%{token_list}
00. Syoka itina","Sarafu:
%{token_list}
00. Cooka thutha","Tsagula Sarafu:
%{token_list}
00. Uya Nyuma","Yier Sarafu:
%{token_list}
00. Dog chien","Chagua Sarafu:
%{token_list}
00. Dheebi"
token_selection_pin_authorization.first,"CON %{token_data}
Enter pin to select:","CON %{token_data}
Weka nambari ya siri kuchagua:
0. Back","%{token_info}
Sakua kwa kwikia pin yaku:
0. Syoka itina","%{token_info}
Ekera pin yaku gushaghura:
0. Cooka thutha","%{token_info}
Ika piniyo kutsagula Sarafu:
0. Uya Nyuma","%{token_info}
Ket pin ni iyier:
0. Dog chien","%{token_info}
Pin kake khai akh dibii chaguat
0. Dheebi"
token_selection_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
account_management,"CON My account
1. My profile
2. Change language
3. Check balance
4. Check statement
5. PIN options
0. Back","CON Akaunti yangu
1. Wasifu wangu
2. Chagua lugha utakayotumia
3. Angalia salio
4. Angalia taarifa ya matumizi
5. Mipangilio ya nambari ya siri
0. Rudi","CON Kinandu Chakwa
1. Profile/wasifu wakwa
2. Sakua luka ila ukatumiaa
3. Syisya Mbalansi yaku
4. Syisya welesyo wa utumii
5. Chenja namba yaku ya siri
0. Syoka itina","CON Akaunti yakwa
1. Maritwa makwa
2. Shaghura rothiumu ukuhuthira
3. Rora matigari
4. Rora rugano rwa mahuthira
5. Chengia namba ya thiri
0. Coka","CON Akaunti yangu
1. Malagizo Gangu
2. Tsagula Luga Undohumira
3. Lola Sazo
4. Lola tarifa Za Mahumizi
5. Galuza Namba Fitse
0. Uya Nyuma","CON Akaont na
1. Nyanonro mara
2. Yier dhok midwatiyogo
3. Ngi midong go
4. N'gi chal mar akaont
5. Lok nambani mopondo
0. Dog chien","CON Akaunti khiy
1. Wasifu wangu
2. Afaan dubaad chaqui
3. laali balansi
4. Angalia taarifa ya matumizi
5. Gargarch namba
0. Dheebi"
metadata_management,"CON My profile
1. Edit name
2. Edit gender
3. Edit age
4. Edit location
5. Edit products
6. View my profile
0. Back","CON Wasifu wangu
1. Weka jina
2. Weka jinsia
3. Weka umri
4. Weka eneo
5. Weka bidhaa
6. Angalia wasifu wako
0. Rudi","CON Profile/Wasifu wakwa
1. Ikia isyitwa
2. Ikia jinsia/gender yaku
3. Ikia miaka yaku
4. Ikia utui waku
5. Ikia syindu ila utesaa
6. Sisya profile/wasifu waku
0. Syoka itina","CON Maondu maku
1. Ekera ritwa
2. Ekera kana we mundurume kana mutumia
3. Ekera miaka yaku
4. Ekera kuria uikaraga
5. Ikira kiria uendagia
6. Rora maundu maku
0. Coka thutha","CON Malagizo Gangu
1. Ika dzinaro
2. Ika kala umulume ama Umuche
3. Ika umuri
4. Ika eneo
5. Ika Miyo ama viya uguzavyo
6. Lola malagizo Gangu
0. Uya nyuma","CON Wasifu wangu
1. Ket nyingi
2. Ket kit chuech mari
3. Ket iki
4. Ket kumaidake
5. Ket gikmaiuso
6. Ng'i nyanonro mara
0. Dog chien","CON Wasifu wangu
1. Maqa kekhai
2. Naam dira mo dubr
3. Gan kekhai
4. Fulaa itgalt kai
5. Mih kai
6. Angalia wasifu wako
0. Dheebi"
display_user_metadata,"CON Your details are:
Name: %{full_name}
Gender: %{gender}
Age: %{age}
Location: %{location}
You sell: %{products}
0. Back","CON Wasifu wako una maelezo yafuatayo:
Jina: %{full_name}
Jinsia: %{gender}
Umri: %{age}
Eneo: %{location}
Unauza: %{products}
0. Rudi","CON Profile/Wasifu waku wina maelesyo aa:
Isyitwa: %{full_name}
Jinsia yaku/gender: %{gender}
Miaka yaku: %{age}
Utui/location yaku: %{location}
Syindu ila uta: %{products}
0. Syoka itina","CON Maundu maku mena rugano ruru:
Maretwa: %{full_name}
Mutumia kana muthuri: %{gender}
Miaka : %{age}
Kuria uikaraga : %{location}
Kiria uendagia : %{products}
0. Coka thutha","CON Malagizo gako gana moro uthuwizirao:
Dzina: %{full_name}
Umuche ama Mulume: %{gender}
Umuri: %{age}
Umbolako: %{location}
Miyo uguzayo: %{products}
0. Uya nyuma","CON Nyanonro mari en:
Nying: %{full_name}
Kit chuech: %{gender}
Iga: %{age}
Kumidake: %{location}
Gima iuso: %{products}
0. Dog chien","CON Wasifu wako una maelezo yafuatayo:
JinaMakha: %{full_name}
Jinsia: %{gender}
Gan: %{age}
Fulaa : %{location}
Maan gurgurt: %{products}
0. Dheebi"
select_preferred_language,"CON Choose language:
%{first_language_set}
0. Back
11. Next
00. Exit","CON Chagua lugha:
%{first_language_set}
0. Rudi
11. Mbele
00. Ondoka","CON Sakua luka
1. Kisungu
2. Kiswahili
3. Kikamba
0. Syoka itina","CON Caghura ruthiomi
1. Githungu
2. Githweri
0. Coka","CON Tsagula Luga
1. Kizungu
2. Kiswahili
0. Uya nyuma","CON Yier dhok
1. Dho Ngere
2. Dho Oswayo
0. Dog chien","CON Chagua lugha
1. Afaan ferenji
2. Afaan kiswahili
0. Dheebi"
retry_pin_entry,"CON Incorrect PIN entered,please try again. You have %{remaining_attempts} attempts remaining.
0. Back","CON Nambari uliyoweka si sahihi, jaribu tena. Una majaribio %{remaining_attempts} yaliyobaki.
0. Rudi","CON Namba ila wekiya iyaile, tata kwikia ingi. Tata mala %{remaining_attempts} nimo matyele.
0. Itina","CON Namba uikirite ti njega, geria ringi.Ni maita %{remaining_attempts} matigarete.
0. Gucoka thutha","CON Nambari fitse urioika seyo, jeza kaheri. Usere Majezo %{remaining_attempts} Gaserego.
0. Uya nyuma","CON Namba miketo oknikare, tem kendo. Idong gi temo di %{remaining_attempts} modong.
0. Chien","CON Namba at keket suninit,laal amalle.Nafaas kaitdheebit %tanaataf
0. Dheebi"
pin_management,"CON Pin options
1. Change PIN
2. Reset PIN
3. Guard PIN
0. Back","CON Pin options
1. Badilisha nambari yangu ya siri
2. Tuma ombili la kubadilisha nambari ya siri
3. Linda nambari ya siri
0. Rudi",,,,,
enter_current_pin.first,"CON Enter current PIN.
0. Back","CON Weka nambari ya siri.
0. Rudi","CON Ikia namba yaku ya siri.
0. Syoka itina","CON Ekera namba ya thiri
0. Coka thutha","CON Ika namba fitse.
0. Uya Nyuma","CON Ket nambani mopondo.
0. Dog chien","CON Namba ka namii imben kekhai
0. Dheebi"
enter_current_pin.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
enter_new_pin,"CON Enter your new four number PIN
0. Back","CON Weka nambari ya siri mpya
0. Rudi","CON Ikia namba yaku ya siri ila yumbya
0. Syoka itina","CON Ekera namba njeru ya thiri
0. coka thutha","CON Ika namba fitse mbisha
0. Uya nyuma","CON Ket namba mopondo maanyien.
0. Dog chien","CON Namba hareti ka namii imben kekhai
0. Dheebi"
new_pin_confirmation,"CON Enter your new four number PIN again
0. Back","CON Weka nambari yako ya siri tena
0. Rudi","CON Ikia namba yaku ya siri ingi
0. Syoka itina","CON Ekera namba yaku ya thiri renge
0. Coka thutha","CON Uyira kuika lwaphiri
0. Uya Nyuma","CON Ket nambani mopondo kendo
0. Dog chien","CON Namba hareti ka namii imben kekhai amalle
0. Dheebi"
reset_guarded_pin,"CON Enter phone number you are the guardian to reset their pin
0. Back","CON Weka nambari ya simu ili kutuma ombi la kubalisha nambari ya siri.
0. Rudi",,,,,
reset_guarded_pin_authorization.first,"CON Enter YOUR pin to confirm %{guarded_account_information}'s reset
0. Back","CON Weka nambari YAKO ya siri ili kudhibitisha ombi la kubadilisha nambari ya siri ya %{guarded_account_information}.
0. Rudi",,,,,
reset_guarded_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},,,,,
exit_pin_reset_initiated_success,"CON Success: You have initiated a PIN reset for %{guarded_account_information}
0. Back
9. Exit","CON Ombi lako la kubadili nambari ya siri ya %{guarded_account_information} limetumwa.
0. Rudi
9. Ondoka",,,,,
exit_not_authorized_for_pin_reset,"CON Failure: You are not authorized to reset that PIN. You must be a guardian!
0. Back
9. Exit","CON Huruhusiwi kutuma ombi la kubadilisha nambari ya siri.
0. Rudi
9. Ondoka",,,,,
guard_pin,"CON Pin guard
1. View guardians
2. Add guardian
3. Remove guardian
0. Back","CON Linda nambari ya siri
1. Walinzi wa namabari ya siri
2. Ongeza mlinzi
3. Ondoa mlinzi
0. Rudi",,,,,
guardian_list_pin_authorization.first,"CON Enter your pin to view set guardians
0. Back","CON Weka nambari yako ya siri ili kuona walinzi uliowaongeza
0. Rudi",,,,,
guardian_list_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},,,,,
guardian_list,"CON %{guardians_list}
0. Back
9. Exit","CON %{guardians_list}
0. Rudi
9. Ondoka",,,,,
add_guardian,"CON Enter phone number to add as pin reset guardian
0. Back","CON Weka nambari ya simu ili kuongeza mlinzi
0. Rudi",,,,,
add_guardian_pin_authorization.first,"CON Enter your pin to add %{guardian_information} as your PIN reset guardian
0. Back","CON Weka nambari YAKO ya siri ili kumwongeza %{guardian_information} kama mlinzi
0. Rudi",,,,,
add_guardian_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},,,,,
exit_guardian_addition_success,"CON Success: %{guardian_information} can now reset your PIN
0. Back
9. Exit","CON Ombi lako la kumwongeza: %{guardian_information} kama mlinzi limefanikiwa
0. Rudi
9. Ondoka",,,,,
exit_invalid_guardian_addition,"CON %{error_exit}
0. Back
9. Exit","CON %{error_exit}
0. Rudi
9. Ondoka",,,,,
remove_guardian,"CON Enter phone number to revoke guardianship:
0. Back","CON Weka nambari ya simu ili kuondoa mlinzi
0. Rudi",,,,,
remove_guardian_pin_authorization.first,"CON Enter your pin to remove %{guardian_information} as your PIN reset guardian
0. Back","CON Weka nambari YAKO ya siri ili kumwondoa %{guardian_information} kama mlinzi
0. Rudi",,,,,
remove_guardian_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},,,,,
exit_guardian_removal_success,"CON Success: %{guardian_information} PIN reset guardianship is revoked
0. Back
9. Exit","CON Ombi lako la kumwondoa: %{guardian_information} kama mlinzi limefanikiwa
0. Rudi
9. Ondoka",,,,,
exit_invalid_guardian_removal,"CON %{error_exit}
0. Back
9. Exit","CON %{error_exit}
0. Rudi
9. Ondoka",,,,,
transaction_pin_authorization.first,"CON %{recipient_information} will receive %{transaction_amount} %{token_symbol} from %{sender_information}.
Please enter your PIN to confirm.
0. Back","CON %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}.
Tafadhali weka nambari yako ya siri kudhibitisha.
0. Rudi","CON %{recipient_information} nukwata %{transaction_amount} %{token_symbol} kuma kwa %{sender_information}.
Tafadhali ikia namba yaku ya siri kuvitukithya.
0. Syoka itina","CON %{recipient_information} akuamukira %{transaction_amount} %{token_symbol} kuuma kwa %{sender_information}.
Ekera namba yaku ya thiri kuetekeria.
0. Coka thutha","CON %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}.
Unavoywa kuika nambayo fitse kugeluza.
0. Uya nyuma.","CON %{recipient_information} dhiyudo %{transaction_amount} %{token_symbol} kowuok kuom %{sender_information}.
Kiyie to ket nambani mopondo mondo iyie:
0. Dog chien","CON %{recipient_information} in argad%{transaction_amount} %{token_symbol} ir %{sender_information}.
Namba ka namii imben kekhai
0. Dheebi"
transaction_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
display_metadata_pin_authorization.first,"CON Please enter your PIN
0. Back","CON Tafadhali weka PIN yako
0. Rudi","CON Tafadhali ikia PIN yaku
0. Syoka itina","CON Ekera pin yaku
0. coka thutha","CON Unavoywa kuika namayo fitswe
0. Uya Nyuma","CON Kiyie to ket nambani mopondo
0. Dog chien","CON Namba ka namii imben kekhai
0. Dheebi"
display_metadata_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
account_balances_pin_authorization.first,"CON Please enter your PIN to view balances
0. Back","CON Tafadhali weka PIN yako kuona salio.
0. Rudi","CON Tafadhali ikia PIN yaku kwona utyalo.
0. Syoka itina","CON Ekera pin yaku kuona matigari maku
0. Coka ","CON Unavoywa namba fitswe kulola Sazo.
0. Uya nyuma","CON Kiyie to ket nambani mopondo mondo ine modong'
0. Dog chien","CON Namba ka namii imbeen kekhai ak balansi kake lalt
0. Dheebi"
account_balances_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
account_statement_pin_authorization.first,"CON Please enter your PIN to view statement
0. Back","CON Tafadhali weka PIN yako kuona taarifa ya matumizi.
0. Rudi","CON Tafadhali ikia PIN yaku kwona welesyo wa utumii.
0. Syoka itina","CON Ekera pin yaku kuona rugano rwa mahuthira maku
0. coka thutha","CON Unavoywa namba fitswe kupata maerezo ga mahumizi Gako.
0. Uya Nyuma","CON Kiyie to ket nambani mar siri mondo ine chenro mar tiyo.
0. Dog chien","CON Tafadhali weka PIN yako kuona taarifa ya matumizi.
0. Dheebi"
account_statement_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
name_edit_pin_authorization.first,"CON Please enter your PIN
0. Back","CON Tafadhali weka PIN yako
0. Rudi","CON Tafadhali ikia PIN yaku
0. Syoka itina","CON Ekera pin yaku
0. coka thutha","CON Unavoywa namba fitse
0. Uya Nyuma","CON Ket nambani mopondo
0. Dog chien","CON Namba ka namii imben kekhai
0. Dheebi"
name_edit_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
dob_edit_pin_authorization.first,"CON Please enter your PIN
0. Back","CON Tafadhali weka PIN yako
0. Rudi","CON Tafadhali ikia PIN yaku
0. Syoka itina","CON Ekera namba yaku ya thiri
0. Rudi","CON Unavoywa namba fitswe
0. Uya nyuma","CON Kiyie to ket nambani mopondo
0. Dog chien","CON Namba kake ka namii imbeen kekhai
0. Dheebi"
dob_edit_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
gender_edit_pin_authorization.first,"CON Please enter your PIN
0. Back","CON Tafadhali weka PIN yako
0. Rudi","CON Tafadhali ikia PIN yaku
0. Syoka itina","CON Ekera namba yaku ya thiri
0. coka thutha","CON Unavoywa namba fitswe
0. Uya nyuma","CON Kiyie to ket nambani mopondo
0. Dog chien","CON Namba kake ka namii imbeen kekhai
0. Dheebi"
gender_edit_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
location_edit_pin_authorization.first,"CON Please enter your PIN
0. Back","CON Tafadhali weka PIN yako
0. Rudi","CON Tafadhali ikia PIN yaku
0. Syoka itina","CON Ekera namba yaku ya thiri
0. Coka thutha","CON Unavoywa namba fitswe
0. Uya nyuma","CON Kiyie to ket nambani mopondo
0. Dog chien","CON Namba kake ka namii imbeen kekhai
0. Dheebi"
location_edit_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
products_edit_pin_authorization.first,"CON Please enter your PIN
0. Back","CON Tafadhali weka PIN yako
0. Rudi","CON Tafadhali ikia PIN yaku
0. Syoka itina","CON Ekera namba yaku ya thiri
0. Coka thutha","CON Unavoywa namba fitswe
0. Uya nyuma","CON Kiyie to ket nambani mopondo
0. Dog chien","CON Namba kake ka namii imbeen kekhai
0. Dheebi"
products_edit_pin_authorization.retry,%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry},%{retry_pin_entry}
account_balances.available_balance,"CON Your balances are as follows:
balance: %{available_balance} %{token_symbol}
0. Back","CON Salio zako ni zifuatazo:
salio: %{available_balance} %{token_symbol}
0. Rudi","CON Utyalo waku ni uu:
utyalo: %{available_balance} %{token_symbol}
0. Syoka itina","CON Matigari maku ni maya:
matigari: %{available_balance} %{token_symbol}
0. coka","CON Masazogo nidzavivyo gatuwiravyo:
Sazo: %{available_balance} %{token_symbol}
0. Uya Nyuma","CON Dong mari en:
Dong: %{available_balance} %{token_symbol}
0. Dog chien","CON Balansi kake akan
salio: %{available_balance} %{token_symbol}
0. Dheebi"
account_balances.with_fees,"CON Your balances are as follows:
balances: %{available_balance} %{token_symbol}
fees: %{tax} %{token_symbol}
0. Back","CON Salio zako ni zifuatazo:
salio: %{available_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
0. Rudi","CON Utyalo waku ni uu:
utyalo: %{available_balance} %{token_symbol}
tax/ushuru: %{tax} %{token_symbol}
0. Syoka itina","CON Matigari maku ni maya:
matigari: %{available_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
0. coka thutha","CON Masazogo ni gatuwirago:
Masazo: %{available_balance} %{token_symbol}
Ushuuru: %{tax} %{token_symbol}
0. Uya nyuma","CON Dong mari en:
Dong: %{available_balance} %{token_symbol}
osuru: %{tax} %{token_symbol}
0. Dog chien","CON Balansi kake akan
salio: %{available_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
0. Dheebi"
account_balances.with_rewards,"CON Your balances are as follows:
balance: %{available_balance} %{token_symbol}
fees: %{tax} %{token_symbol}
rewards: %{bonus} %{token_symbol}
0. Back","CON Salio zako ni zifuatazo:
salio: %{available_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
tuzo: %{bonus} %{token_symbol}
0. Rudi","CON Utyalo waku ni uu:
Utyalo: %{available_balance} %{token_symbol}
Tax/ushuru: %{tax} %{token_symbol}
muthinzio: %{bonus} %{token_symbol}
0. Syoka itina","CON Salio zako ni zifuatazo:
salio: %{available_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
tuzo: %{bonus} %{token_symbol}
0. coka thutha","CON Sazoro ni rituwiraro:
Sazo: %{available_balance} %{token_symbol}
Ushuuru: %{tax} %{token_symbol}
Zawadi: %{bonus} %{token_symbol}
0. Uya Nyuma","CON Dong mari en:
Dong: %{available_balance} %{token_symbol}
osuru: %{tax} %{token_symbol}
mich: %{bonus} %{token_symbol}
0. Dog chien","CON Balansi kake akan
salio: %{available_balance} %{token_symbol}
ushuru: %{tax} %{token_symbol}
tuzo: %{bonus} %{token_symbol}
0. Dheebi"
first_transaction_set,"CON %{first_transaction_set}
0. Back
11. Next
00. Exit","CON %{first_transaction_set}
0. Rudi
11. Mbele
00. Ondoka","CON %{first_transaction_set}
1. Mbee
00. Ondoka","CON %{first_transaction_set}
1. Mbere
00. uma","CON %{first_transaction_set}
1. Mbere
00. Uka","CON %{first_transaction_set}
1. Nyime
00. Wuogi","CON %{first_transaction_set}
1. Dhuur
00. Bai"
middle_transaction_set,"CON %{middle_transaction_set}
11. Next
22. Previous
00. Exit","CON %{middle_transaction_set}
11. Mbele
22. Rudi
00. Ondoka","CON %{middle_transaction_set}
1. Mbee
2. Itina
00. Ondoka","CON %{middle_transaction_set}
1. Mbere
2. coka thutha
00. Uma","CON %{middle_transaction_set}
1. Mbere
2. Uya nyuma
00. Uka","CON %{middle_transaction_set}
1. Nyime
2. Dog chien
00. Wuogi","CON %{middle_transaction_set}
1. Dhuur
2. Dheebi
00. Bai"
last_transaction_set,"CON %{last_transaction_set}
22. Previous
00. Exit","CON %{last_transaction_set}
22. Rudi
00. Ondoka","CON %{last_transaction_set}
2. Itina
00. Ondoka","CON %{last_transaction_set}
2. Coka thutha
00. Uma","CON %{last_transaction_set}
2. Uya Nyuma
00. Uka","CON %{last_transaction_set}
2. Dog chien
00. Wuogi","CON %{last_transaction_set}
2. Dhuur
00. Bai"
exit,END Thank you for using the service.,END Asante kwa kutumia huduma.,END Ni muvea kwa kutumia huduma ii.,END Thegio ni kuhuthira mutabo,END. Namvera kwa mahumizi ga ii huduma.,END Erokamano kuom tiyo kodwa.,END Asante kwa kutumia huduma.
exit_invalid_request,END Invalid request.,END Chaguo si sahihi.,END Usakuo waku uyaile.,END shaguro riaku ti riega,END. Tsagulo karisawa sawa,END Yiero okni kare,END Ka at chaquad suninit
exit_invalid_menu_option,"CON Invalid menu option. For help,call %{support_phone}.
00. Back
99. Exit","CON Chaguo lako sio sahihi. Kwa usaidizi piga simu %{support_phone}
00. Rudi
99. Ondoka","CON Usakuo waku uyaile. Kwa utethyo kuna simu %{support_phone}
00. Itina
99. Ondoka","CON Shaguro riaku ti riega.Kwa uteithio hura %{support_phone}
00. Coka thutha
99. Uma","CON Tsaguloro karisawa sawa. Kuavizwa piga %{support_phone}
00. Uya Nyuma
99. Uka","CON Yiero ni oknikare. Kuom kony go simu e %{support_phone}
00. Dog chien
99. Wuogi","CON Ka at chaqui suninit.qarqarsa simu dai%{support_phone}
00. Dheebi
99. Bai"
exit_invalid_input,"CON Invalid input. Nothing selected
00. Back
99. Exit","CON Chaguo lako halipatikani. Hakuna kilichochaguliwa.
00. Rudi
99. Ondoka","CON Usakuo waku wikwonekana.Vaii kindu kisakue.
00. Itina
99. Ondoka","CON Shaguro riaku ritironekana. Hatiri kindu washaghura.
00. Coka thutha
99. Uma","CON Tsaguloro karipatikana. Kakuna Kutsagurire chochosi.
00. Uya nyuma
99. Uka","CON Yiero ni okyudre. Onge gima iyiero.
00. Dog chien
99. Wuogi","CON Ka at chaguad injirt. oo
00. dheebi
99. Bai"
exit_pin_blocked,"END Your PIN has been blocked. For help, please call %{support_phone}.",END PIN yako imefungwa. Kwa usaidizi tafadhali piga simu %{support_phone}.,END PIN yaku niyavingwa. Kutethwa kuna simu ino %{support_phone}.,END PIN yaku niyahingwo. Kwa uteithio hura thimu %{support_phone}.,END. Namba fitse yakwako ifungwa. Kwa kuavizwa unaangwa upige simu %{support_phone}.,END Nambani mopondo olor. Kuom kony go simu e %{support_phone}.,END Pin kake yahidat. Qarqarsa simu dai %{support_phone}.
exit_invalid_pin,"END The PIN you have entered is invalid. PIN must consist of 4 digits. For help, call %{support_phone}.",END PIN uliyobonyeza sio sahihi. PIN lazima iwe na nambari nne. Kwa usaidizi piga simu %{support_phone}.,END PIN ila wekia iyaile. Ni lasima PIN ithiwe na namba inya. Kutethwa kuna namba ii %{support_phone}.,END PIN iria wekera tii njega. PIN nomoka ikorwo na namba inya. Kwa uteithio hura thimu %{support_phone}.,"END Namba fitse urohofya seyo, kaisawa. Namba fitswe inamalwa ikale na namba nee. Kwa kuvizwa, piga simu%{support_phone}.",END. Namba mopondo miketo oknikare. Nyaka obed gi nembni ang'wen. Kuom kony go simu e %{support_phone}.,END PIN ka at keket suninit. PIN Pin namba afuuri tatatu. Qarqarsa simu dai %{support_phone}.
exit_invalid_new_pin,"END The PIN you have entered is invalid. PIN must be different from your current PIN. For help, call %{support_phone}.",END PIN uliyobonyeza sio sahihi. PIN lazima iwe tofauti na pin yako ya sasa. Kwa usaidizi piga simu %{support_phone}.,END PIN ila wekia iyaile. PIN ni lasima ithiwe tofauti na pin yaku ya oyu. Kutethwa kuna namba ii %{support_phone}.,END PIN uria wekera ti njega. PIN nomohaka ikorwo na namba ndiganu na ya riu . Kwa uteithio hora thimu %{support_phone}.,END Namba fitswe uriohopya siyo ya karakara. Namba fitswe inahenzekana ikale itofauti na uhumirayo vivi. Kwa maavizo piga simu %{support_phone}.,END Namba mopondo miketo oknikare. Nyaka obed mopogore gi nambani mopondo masani. Kuom kony gochi e %{support_phone}.,END PIN ka at keket suninit.Pin kake walinfakaatin.Qarqars simu dai %{support_phone}.
exit_pin_mismatch,"END The new PIN does not match the one you entered. Please try again. For help, call %{support_phone}.",END PIN mpya na udhibitisho wa pin mpya hazilingani. Tafadhali jaribu tena. Kwa usaidizi piga simu %{support_phone}.,END PIN yumbya na uhakikisho wa pin yumbya syivwanene. Tafadhali tata ingi. Kutethwa kuna simu %{support_phone}.,END PIN njeru na pin ya guetekeria shitira hianana . Geria ringi. Kwa uteithio hora thimu %{support_phone}.,END Namba fitse uzdoinjiza kaikara kara na uriyohopya laphiri. Unavoywa ujeze kaheri. Kwa kuavizwa piga simu %{support_phone}.,GIKO. Namba mopondo miketo opogore gi manikuongo keto. Kiyie to ket kendo. Kuom kony gochi e %{support_phone}.,END PIN mpya na udhibitisho wa pin mpya hazilingani. Pin hareti ka at kekeet walinfakan. it dheebi amaale.Qarqars simu dai%{support_phone}.
exit_invalid_recipient,"CON Recipient's phone number is not registered or is invalid:
00. Retry
99. Exit","CON Mpokeaji wa nambari hapatikani au sio sahihi.
00. Jaribu tena
99. Ondoka","CON Mukwati wa namba ndokwatikana kana ii namba iyaile kana ti sahihi.
00. Tata ingi
99. Ondoka","CON Mpokeaji wa nambari hapatikani au sio sahihi.Namba ya mutumirwo ndiranyitikana kana ti njega
00. Geria ringi
99. Uma","CON Muphokezi wa namba kapatikana ama namba kai karakara.
00. Jeza Kaheri
99. Uka","CON Jayuto mar nambani okyudre kata oknikare.
00. Tem kendo
99. Wuogi","CON Mpokeaji wa nambari hapatikani au sio sahihi.
00.
99. Ondoka"
exit_successful_transaction,"CON Your request has been sent. %{recipient_information} will receive %{transaction_amount} %{token_symbol} from %{sender_information}.
00. Back
99. Exit","CON Ombi lako limetumwa. %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}.
00. Rudi
99. Ondoka","CON Woni waku niwatumwa. %{recipient_information} nukupokea %{transaction_amount} %{token_symbol} kuma kwa %{sender_information}.
00. Itina
99. Ondoka","CON Mahoya maku nimatomwo. %{recipient_information} akuamukira%{transaction_amount} %{token_symbol} kuma kwa %{sender_information}.
00. Coka
99. Uma","CON Mavoyogo gahumwa. %{recipient_information} undaphokera %{transaction_amount} %{token_symbol} kuombola kwa %{sender_information}.
00. Uya Nyuma
99. Uka","CON Kwayo ni oseor. %{recipient_information} oboyudo %{transaction_amount} %{token_symbol} kowuok kuom %{sender_information}.
00. Dog chien
99. Wuogi","CON Qarqar kake yaergad. %{recipient_information} inargat %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}.
00. Dheebi
99. Bai"
exit_insufficient_balance,"CON Payment of %{amount} %{token_symbol} to %{recipient_information} has failed due to insufficient balance.
Your Sarafu-Network balances is: %{token_balance}
00. Back
99. Exit","CON Malipo ya %{amount} %{token_symbol} kwa %{recipient_information} halijakamilika kwa sababu salio lako haitoshi.
Akaunti yako ya Sarafu ina salio ifuatayo: %{token_balance}
00. Rudi
99. Ondoka","CON Ndivi ya %{amount} %{token_symbol} kwa %{recipient_information} inavitukithwa nundu utyalyo waku ni munini.
Kinandu chaku cha Sarafu kina utyalo uu: %{token_balance}
00. Itina
99. Ondoka","CON Marehi ma %{amount} %{token_symbol} kwa %{recipient_information} matinarekereka tondu matigari maku matiraigana.
Akaunti yako ya Sarafu ina salio ifuatayo: %{token_balance}
00. Coka
99. Uma","CON Maripho ga %{amount} %{token_symbol} kwa %{recipient_information} Karidzangwe kukamirika Kwaukala sazoro Karitosha.
Akauntiyo vivi ina sazo dza rituwiranavyo: %{token_balance}
00. Uya nyuma
99. Uka","CON Chudo mar %{amount} %{token_symbol} kuom %{recipient_information} okotieki nikech dong ni okrom.
Akaont ni mar Sarafu ni gi dong mar: %{token_balance}
00. Dog chien
99. Wuogi","CON Malipo ka%{amount} %{token_symbol} kwa %{recipient_information} Inkamilikee balansi kake ingau
Akaunti kake balansi akan kabd: %{token_balance}
00. Dheebi
99. Bai"
exit_successful_token_selection,"CON Success! %{token_symbol} is your active Sarafu.
00. Back
99. Exit","CON Chaguo lako limekamilika, %{token_symbol} ni sarafu itakayotumika.
00. Rudi
99. Ondoka",,,,,
invalid_service_code,Please dial %{valid_service_code} to access Sarafu Network,Bonyeza %{valid_service_code} kutumia mtandao wa Sarafu,Vinyia %{valid_service_code} kutumia mutandao wa Sarafu,Hihinya%{valid_service_code} kuhudhira mutabo wa Sarafu,Hofya %{valid_service_code} Kuhumira Mutandao wa sarafu,Dii %{valid_service_code} mondo iti gi Sarafu,Bonyeza %{valid_service_code} kutumia mtandao wa Sarafu
help,"CON For assistance call %{support_phone}
00. Back
99. Exit","CON Kwa usaidizi piga simu %{support_phone}
0. Rudi
9. Ondoka","CON Kwa utethyo kuna simu %{support_phone}
0. Itina
9. Ondoka","CON Kwa uteithio hora thimu %{support_phone}
0. Coka
9. Uma","CON Kwa Kuavizwa piga simu %{support_phone}
0. Uya nyuma
9. Uka","CON Kuom kony go simu e %{support_phone}
0. Dog chien
9. Wuogi","CON Qarqars simu dai%{support_phone}
0. Dheebi
9. Bai"
complete,"CON Your request has been sent. You will receive an SMS shortly.
00. Back
99. Exit","CON Ombi lako limetumwa. Utapokea uthibitishaji wa SMS kwa muda mfupi.
00. Rudi
99. Ondoka","CON Woni waku niwatumwa. Nukwata SMS ya kwonya ivinda ite yasa.
00. Itina
99. Ondoka","CON Mahoya maku nimatomwo. Niukuamukira SMS ya guitikirika ihinda ikuhi .
00. Coka
99. Uma","CON Vyoyoro rihumwa. Undaphokera Uthibitishaji wa SMS kwa muda mufuhi.
00. Uya nyuma
99. Uka","CON Kwayo ni oseor. Iboyudo mesej mar ote ni bang' saa matin.
00. Dog chien
99. Wuogi","CON Qarqars kake yaergad. Utapokea uthibitishaji wa SMS kwa muda mfupi.
00. Dheebi
99. Bai"
account_creation_prompt,END Your account is being created. You will receive an SMS when your account is ready.,END Akaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.,END Akaunti yako ya Sarafu yendeye usovwa. Nukwata SMS akaunti yaku yasovwa.,END Akaunti yaku ya Sarafu niiraharirio.Niugutumirwo SMS akauti yaku ya rikio kuharirio,END Akauntiyo ya sarafu idzikoni. Undaphokera ujumbe wa SMS ichikala tayari.,END Akaont ni mar Sarafu iloso. Iboyudo mesej ka akaont ni otieki.,END Akaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.
initial_middle_language_set,"CON Choose language:
%{middle_language_set}
11. Next
22. Previous
00. Exit","CON Chagua lugha:
%{middle_language_set}
11. Mbele
22. Rudi
00. Ondoka",,,,,
initial_last_language_set,"CON Choose language:
%{last_language_set}
22. Previous
00. Exit","CON Choose language:
%{last_language_set}
22. Rudi
00. Ondoka",,,,,
middle_language_set,"CON Choose language:
%{middle_language_set}
11. Next
22. Previous
00. Exit","CON Chagua lugha:
%{middle_language_set}
11. Mbele
22. Rudi
00. Ondoka",,,,,
last_language_set,"CON Choose language:
%{last_language_set}
22. Previous
00. Exit","CON Choose language:
%{last_language_set}
22. Rudi
00. Ondoka",,,,,
1 keys en sw kam kik miji luo bor
2 initial_language_selection CON Welcome to Sarafu Network %{first_language_set} 11. Next 00. Exit CON Karibu Sarafu Network %{first_language_set} 11. Mbele 00. Ondoka CON Kalivu Network ya Sarafu 1. English 2. Kiswahili 3. Kikamba 3. Help CON Karibu Sarafu Network 1. Githungu 2. Githweri 3. Uteithio CON Karibu Sarafu Network 1. Chizungu 2. Chiswahili 3. Avizwa CON Machiegni e network mar Sarafu 1. Dho Ngere 2. Dho oswayo 3. Kony CON Karibu Sarafu Network 1. Afaan ferenji 2. Afaan kiswahili 3. Qarqars
3 initial_pin_entry CON Please enter a new four number PIN for your account. CON Tafadhali weka pin mpya yenye nambari nne kwa akaunti yako CON Tafadhali ikia pin yumbya ila ina namba inya kinanduni chaku CON Ekera namba yaku ya thiri njeru ena numba enna. CON Ika piniyo ya namba Ne kwa akaunti Yakwako. CON Kiyie to ket namba ni mopondo e akaont ni. CON Tafadhal pin hareti kekhae ka namba afuri fulaa akaunti kake
4 initial_pin_confirmation CON Enter your four number PIN again CON Weka PIN yako tena CON Ikia PIN yaku ingi CON Ekera namba yaku ya thiri ringi CON Uyira Kwika pin kaheri. CON Ket namba ni mopondo kendo CON Mar dibii pin kekhae
5 enter_given_name CON Enter first name 0. Back CON Weka jina lako la kwanza CON Ikia isyitwa yaku ya mbee CON Ekera retwa rwaku ria mbere CON Ika dzinaro rakwanza. CON Ket nyingi mokwongo CON Makhaa kake ka karaa kor
6 enter_family_name CON Enter family name 0. Back CON Weka jina lako la mwisho 0. Rudi CON Ikia isyitwa yaku ya muthya 0. Syoka itina CON Ekera ritwa rwaku ria mwisho 0. Coka thutha CON Ika dzinaro ra mwisho 0. Uya Nyuma CON Ket nyingi mogik. 0. Dog chien CON Makhaa kake ka egee 0. Dhebii
7 enter_date_of_birth CON Enter year of birth 0. Back CON Weka mwaka wa kuzaliwa 0. Rudi CON Ikia mwaka wa kusyawa 0. Syoka itina CON Ekera mwaka waku wa guciarwo 0. Coka thutha CON Ika mwaka wakuvyalwa 0. Uya Nyuma CON Ket iki mar nyuol 0. Dog chien CON Gan kake ka athdalat kor 0. Dheebi
8 enter_gender CON Enter gender 1. Male 2. Female 3. Other 0. Back CON Weka jinsia yako 1. Mwanaume 2. Mwanamke 3. Nyngine 0. Rudi CON Ikia gender yaku 1. Mundume 2. Mundumuka 3. Ingi 0. Syoka itina CON We mudurume kana mutumia 1.Mudurume 2. Mutumia 3. Ingi 0. Coka thutha CON Ika kala Umulume ama Umuche au vingine. 1. Mulume 2. Muche 3. Vinjine 0. Uya Nyuma CON Ket kit chwech mari 1. Dichuo 2. Dhako 3. Moko 0. Dog chien CON Athin Dir mo Dubr 1. Dir 2. Dubr 3. Ka dibii 0. Dheebi
9 enter_location CON Enter your location, 0. Back CON Weka eneo lako 0. Rudi CON Ikia utui waku kana location 0. Syoka itina CON Ekera kuria uumete 0. Coka thutha CON Ika enero wombolako. 0. Uya nyuma CON Ket kumaidake 0. Dog chien CON Fulaa athin kubat kor 0. Dhebii
10 enter_products CON Please enter a product or service you offer 0. Back CON Weka bidhaa ama huduma unauza 0. Rudi CON Ikia syindu kana huduma ila unenganae 0. Syoka itina CON Ekera indo kana wira uria urendia 0. Coka thutha CON Ika Viya ama utu uhendao 0. Uya Nyuma CON Ket gima iuso kata tich mitimo 0. Dog chien CON Waan gurgurt okan namaa kenit khes khae 0. Dheebi
11 start CON Balance %{account_balance} %{account_token_name} 1. Send 2. My Sarafu 3. My Account 4. Help CON Salio %{account_balance} %{account_token_name} 1. Tuma 2. Sarafu yangu 3. Akaunti yangu 4. Usaidizi CON Mbalansi kana utyalyo %{account_balance} %{account_token_name} 1. Tuma 2. Kinandu chakwa 3. Utethyo CON Matigari %{account_balance} %{account_token_name} 1. Tuma 2. Akaunti yaku 3. Uteithio CON Sazo %{account_balance} %{account_token_name} 1. Huma 2. Akaunti yangu 3. Avizwa CON Ma Odong' %{account_balance} %{account_token_name} 1. Or 2. Akaont na 3. Kony CON Salio %{account_balance} %{account_token_name} 1. Erg 2. Akaunti khiy 3. Qarqars
12 enter_transaction_recipient CON Enter phone number 0. Back CON Weka nambari ya simu 0. Rudi CON Ikia namba ya simu 0. Syoka itina CON Ikira namba ya thimu 0. Coka thutha CON Ika namba yasimu. 0. Uya Nyuma CON Ket nambani mar simu 0. Dog chien CON Namba ta simuu kekhai 0. Dheebi
13 enter_transaction_amount CON Enter amount 0. Back CON Weka kiwango 0. Rudi CON Ikia kiwango 0. Syoka itina CON Ikira muigana 0. Coka thutha CON Ika chaasi. 0. Uya nyuma CON Ket giko mari 0. Dog chien CON kiwango kekhai 0. Dheebi
14 first_account_tokens_set CON Choose a number or symbol from your balances: %{first_account_tokens_set} 0. Back 11. Next 00. Exit CON Chagua nambari au ishara kutoka kwa salio zako: %{first_account_tokens_set} 0. Rudi 11. Mbele 00. Ondoka Sakua Sarafu: %{token_list} 99. Thi mbee 00. Syoka itina Shaghura Sarafu: %{token_list} 99. Mbere 00. Coka thutha Tsagula Sarafu: %{token_list} 99. Enderera 00. Uya Nyuma Yier Sarafu: %{token_list} 99. Nyime 00. Dog chien Chaqui Sarafu: %{token_list} 99. Dhuur 00. Dheebi
15 middle_account_tokens_set CON Choose a number or symbol from your balances: %{middle_account_tokens_set} 11. Next 22. Previous 00. Exit CON Chagua nambari au ishara kutoka kwa salio zako: %{middle_account_tokens_set} 11. Mbele 22. Rudi 00. Ondoka Sakura Sarafu: %{token_list} 99. Thi mbee 00. Syoka itina Shaghura Sarafu: %{token_list} 99. Mbere 00. Cooka thutha Tsagula Sarafu: %{token_list} 99. Enderera 00. Uya Nyuma Yier Sarafu: %{token_list} 99. Nyime 00. Dog chien Chaqui Sarafu: %{token_list} 99. Dhuur 00. Dheebi
16 last_account_tokens_set CON Choose a number or symbol from your balances: %{last_account_tokens_set} 22. Previous 00. Exit CON Chagua nambari au ishara kutoka kwa salio zako: %{last_account_tokens_set} 22. Rudi 00. Ondoka Sakura sarafu: %{token_list} 00. Syoka itina Sarafu: %{token_list} 00. Cooka thutha Tsagula Sarafu: %{token_list} 00. Uya Nyuma Yier Sarafu: %{token_list} 00. Dog chien Chagua Sarafu: %{token_list} 00. Dheebi
17 token_selection_pin_authorization.first CON %{token_data} Enter pin to select: CON %{token_data} Weka nambari ya siri kuchagua: 0. Back %{token_info} Sakua kwa kwikia pin yaku: 0. Syoka itina %{token_info} Ekera pin yaku gushaghura: 0. Cooka thutha %{token_info} Ika piniyo kutsagula Sarafu: 0. Uya Nyuma %{token_info} Ket pin ni iyier: 0. Dog chien %{token_info} Pin kake khai akh dibii chaguat 0. Dheebi
18 token_selection_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
19 account_management CON My account 1. My profile 2. Change language 3. Check balance 4. Check statement 5. PIN options 0. Back CON Akaunti yangu 1. Wasifu wangu 2. Chagua lugha utakayotumia 3. Angalia salio 4. Angalia taarifa ya matumizi 5. Mipangilio ya nambari ya siri 0. Rudi CON Kinandu Chakwa 1. Profile/wasifu wakwa 2. Sakua luka ila ukatumiaa 3. Syisya Mbalansi yaku 4. Syisya welesyo wa utumii 5. Chenja namba yaku ya siri 0. Syoka itina CON Akaunti yakwa 1. Maritwa makwa 2. Shaghura rothiumu ukuhuthira 3. Rora matigari 4. Rora rugano rwa mahuthira 5. Chengia namba ya thiri 0. Coka CON Akaunti yangu 1. Malagizo Gangu 2. Tsagula Luga Undohumira 3. Lola Sazo 4. Lola tarifa Za Mahumizi 5. Galuza Namba Fitse 0. Uya Nyuma CON Akaont na 1. Nyanonro mara 2. Yier dhok midwatiyogo 3. Ngi midong go 4. N'gi chal mar akaont 5. Lok nambani mopondo 0. Dog chien CON Akaunti khiy 1. Wasifu wangu 2. Afaan dubaad chaqui 3. laali balansi 4. Angalia taarifa ya matumizi 5. Gargarch namba 0. Dheebi
20 metadata_management CON My profile 1. Edit name 2. Edit gender 3. Edit age 4. Edit location 5. Edit products 6. View my profile 0. Back CON Wasifu wangu 1. Weka jina 2. Weka jinsia 3. Weka umri 4. Weka eneo 5. Weka bidhaa 6. Angalia wasifu wako 0. Rudi CON Profile/Wasifu wakwa 1. Ikia isyitwa 2. Ikia jinsia/gender yaku 3. Ikia miaka yaku 4. Ikia utui waku 5. Ikia syindu ila utesaa 6. Sisya profile/wasifu waku 0. Syoka itina CON Maondu maku 1. Ekera ritwa 2. Ekera kana we mundurume kana mutumia 3. Ekera miaka yaku 4. Ekera kuria uikaraga 5. Ikira kiria uendagia 6. Rora maundu maku 0. Coka thutha CON Malagizo Gangu 1. Ika dzinaro 2. Ika kala umulume ama Umuche 3. Ika umuri 4. Ika eneo 5. Ika Miyo ama viya uguzavyo 6. Lola malagizo Gangu 0. Uya nyuma CON Wasifu wangu 1. Ket nyingi 2. Ket kit chuech mari 3. Ket iki 4. Ket kumaidake 5. Ket gikmaiuso 6. Ng'i nyanonro mara 0. Dog chien CON Wasifu wangu 1. Maqa kekhai 2. Naam dira mo dubr 3. Gan kekhai 4. Fulaa itgalt kai 5. Mih kai 6. Angalia wasifu wako 0. Dheebi
21 display_user_metadata CON Your details are: Name: %{full_name} Gender: %{gender} Age: %{age} Location: %{location} You sell: %{products} 0. Back CON Wasifu wako una maelezo yafuatayo: Jina: %{full_name} Jinsia: %{gender} Umri: %{age} Eneo: %{location} Unauza: %{products} 0. Rudi CON Profile/Wasifu waku wina maelesyo aa: Isyitwa: %{full_name} Jinsia yaku/gender: %{gender} Miaka yaku: %{age} Utui/location yaku: %{location} Syindu ila uta: %{products} 0. Syoka itina CON Maundu maku mena rugano ruru: Maretwa: %{full_name} Mutumia kana muthuri: %{gender} Miaka : %{age} Kuria uikaraga : %{location} Kiria uendagia : %{products} 0. Coka thutha CON Malagizo gako gana moro uthuwizirao: Dzina: %{full_name} Umuche ama Mulume: %{gender} Umuri: %{age} Umbolako: %{location} Miyo uguzayo: %{products} 0. Uya nyuma CON Nyanonro mari en: Nying: %{full_name} Kit chuech: %{gender} Iga: %{age} Kumidake: %{location} Gima iuso: %{products} 0. Dog chien CON Wasifu wako una maelezo yafuatayo: JinaMakha: %{full_name} Jinsia: %{gender} Gan: %{age} Fulaa : %{location} Maan gurgurt: %{products} 0. Dheebi
22 select_preferred_language CON Choose language: %{first_language_set} 0. Back 11. Next 00. Exit CON Chagua lugha: %{first_language_set} 0. Rudi 11. Mbele 00. Ondoka CON Sakua luka 1. Kisungu 2. Kiswahili 3. Kikamba 0. Syoka itina CON Caghura ruthiomi 1. Githungu 2. Githweri 0. Coka CON Tsagula Luga 1. Kizungu 2. Kiswahili 0. Uya nyuma CON Yier dhok 1. Dho Ngere 2. Dho Oswayo 0. Dog chien CON Chagua lugha 1. Afaan ferenji 2. Afaan kiswahili 0. Dheebi
23 retry_pin_entry CON Incorrect PIN entered,please try again. You have %{remaining_attempts} attempts remaining. 0. Back CON Nambari uliyoweka si sahihi, jaribu tena. Una majaribio %{remaining_attempts} yaliyobaki. 0. Rudi CON Namba ila wekiya iyaile, tata kwikia ingi. Tata mala %{remaining_attempts} nimo matyele. 0. Itina CON Namba uikirite ti njega, geria ringi.Ni maita %{remaining_attempts} matigarete. 0. Gucoka thutha CON Nambari fitse urioika seyo, jeza kaheri. Usere Majezo %{remaining_attempts} Gaserego. 0. Uya nyuma CON Namba miketo oknikare, tem kendo. Idong gi temo di %{remaining_attempts} modong. 0. Chien CON Namba at keket suninit,laal amalle.Nafaas kaitdheebit %tanaataf 0. Dheebi
24 pin_management CON Pin options 1. Change PIN 2. Reset PIN 3. Guard PIN 0. Back CON Pin options 1. Badilisha nambari yangu ya siri 2. Tuma ombili la kubadilisha nambari ya siri 3. Linda nambari ya siri 0. Rudi
25 enter_current_pin.first CON Enter current PIN. 0. Back CON Weka nambari ya siri. 0. Rudi CON Ikia namba yaku ya siri. 0. Syoka itina CON Ekera namba ya thiri 0. Coka thutha CON Ika namba fitse. 0. Uya Nyuma CON Ket nambani mopondo. 0. Dog chien CON Namba ka namii imben kekhai 0. Dheebi
26 enter_current_pin.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
27 enter_new_pin CON Enter your new four number PIN 0. Back CON Weka nambari ya siri mpya 0. Rudi CON Ikia namba yaku ya siri ila yumbya 0. Syoka itina CON Ekera namba njeru ya thiri 0. coka thutha CON Ika namba fitse mbisha 0. Uya nyuma CON Ket namba mopondo maanyien. 0. Dog chien CON Namba hareti ka namii imben kekhai 0. Dheebi
28 new_pin_confirmation CON Enter your new four number PIN again 0. Back CON Weka nambari yako ya siri tena 0. Rudi CON Ikia namba yaku ya siri ingi 0. Syoka itina CON Ekera namba yaku ya thiri renge 0. Coka thutha CON Uyira kuika lwaphiri 0. Uya Nyuma CON Ket nambani mopondo kendo 0. Dog chien CON Namba hareti ka namii imben kekhai amalle 0. Dheebi
29 reset_guarded_pin CON Enter phone number you are the guardian to reset their pin 0. Back CON Weka nambari ya simu ili kutuma ombi la kubalisha nambari ya siri. 0. Rudi
30 reset_guarded_pin_authorization.first CON Enter YOUR pin to confirm %{guarded_account_information}'s reset 0. Back CON Weka nambari YAKO ya siri ili kudhibitisha ombi la kubadilisha nambari ya siri ya %{guarded_account_information}. 0. Rudi
31 reset_guarded_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry}
32 exit_pin_reset_initiated_success CON Success: You have initiated a PIN reset for %{guarded_account_information} 0. Back 9. Exit CON Ombi lako la kubadili nambari ya siri ya %{guarded_account_information} limetumwa. 0. Rudi 9. Ondoka
33 exit_not_authorized_for_pin_reset CON Failure: You are not authorized to reset that PIN. You must be a guardian! 0. Back 9. Exit CON Huruhusiwi kutuma ombi la kubadilisha nambari ya siri. 0. Rudi 9. Ondoka
34 guard_pin CON Pin guard 1. View guardians 2. Add guardian 3. Remove guardian 0. Back CON Linda nambari ya siri 1. Walinzi wa namabari ya siri 2. Ongeza mlinzi 3. Ondoa mlinzi 0. Rudi
35 guardian_list_pin_authorization.first CON Enter your pin to view set guardians 0. Back CON Weka nambari yako ya siri ili kuona walinzi uliowaongeza 0. Rudi
36 guardian_list_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry}
37 guardian_list CON %{guardians_list} 0. Back 9. Exit CON %{guardians_list} 0. Rudi 9. Ondoka
38 add_guardian CON Enter phone number to add as pin reset guardian 0. Back CON Weka nambari ya simu ili kuongeza mlinzi 0. Rudi
39 add_guardian_pin_authorization.first CON Enter your pin to add %{guardian_information} as your PIN reset guardian 0. Back CON Weka nambari YAKO ya siri ili kumwongeza %{guardian_information} kama mlinzi 0. Rudi
40 add_guardian_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry}
41 exit_guardian_addition_success CON Success: %{guardian_information} can now reset your PIN 0. Back 9. Exit CON Ombi lako la kumwongeza: %{guardian_information} kama mlinzi limefanikiwa 0. Rudi 9. Ondoka
42 exit_invalid_guardian_addition CON %{error_exit} 0. Back 9. Exit CON %{error_exit} 0. Rudi 9. Ondoka
43 remove_guardian CON Enter phone number to revoke guardianship: 0. Back CON Weka nambari ya simu ili kuondoa mlinzi 0. Rudi
44 remove_guardian_pin_authorization.first CON Enter your pin to remove %{guardian_information} as your PIN reset guardian 0. Back CON Weka nambari YAKO ya siri ili kumwondoa %{guardian_information} kama mlinzi 0. Rudi
45 remove_guardian_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry}
46 exit_guardian_removal_success CON Success: %{guardian_information} PIN reset guardianship is revoked 0. Back 9. Exit CON Ombi lako la kumwondoa: %{guardian_information} kama mlinzi limefanikiwa 0. Rudi 9. Ondoka
47 exit_invalid_guardian_removal CON %{error_exit} 0. Back 9. Exit CON %{error_exit} 0. Rudi 9. Ondoka
48 transaction_pin_authorization.first CON %{recipient_information} will receive %{transaction_amount} %{token_symbol} from %{sender_information}. Please enter your PIN to confirm. 0. Back CON %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}. Tafadhali weka nambari yako ya siri kudhibitisha. 0. Rudi CON %{recipient_information} nukwata %{transaction_amount} %{token_symbol} kuma kwa %{sender_information}. Tafadhali ikia namba yaku ya siri kuvitukithya. 0. Syoka itina CON %{recipient_information} akuamukira %{transaction_amount} %{token_symbol} kuuma kwa %{sender_information}. Ekera namba yaku ya thiri kuetekeria. 0. Coka thutha CON %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}. Unavoywa kuika nambayo fitse kugeluza. 0. Uya nyuma. CON %{recipient_information} dhiyudo %{transaction_amount} %{token_symbol} kowuok kuom %{sender_information}. Kiyie to ket nambani mopondo mondo iyie: 0. Dog chien CON %{recipient_information} in argad%{transaction_amount} %{token_symbol} ir %{sender_information}. Namba ka namii imben kekhai 0. Dheebi
49 transaction_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
50 display_metadata_pin_authorization.first CON Please enter your PIN 0. Back CON Tafadhali weka PIN yako 0. Rudi CON Tafadhali ikia PIN yaku 0. Syoka itina CON Ekera pin yaku 0. coka thutha CON Unavoywa kuika namayo fitswe 0. Uya Nyuma CON Kiyie to ket nambani mopondo 0. Dog chien CON Namba ka namii imben kekhai 0. Dheebi
51 display_metadata_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
52 account_balances_pin_authorization.first CON Please enter your PIN to view balances 0. Back CON Tafadhali weka PIN yako kuona salio. 0. Rudi CON Tafadhali ikia PIN yaku kwona utyalo. 0. Syoka itina CON Ekera pin yaku kuona matigari maku 0. Coka CON Unavoywa namba fitswe kulola Sazo. 0. Uya nyuma CON Kiyie to ket nambani mopondo mondo ine modong' 0. Dog chien CON Namba ka namii imbeen kekhai ak balansi kake lalt 0. Dheebi
53 account_balances_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
54 account_statement_pin_authorization.first CON Please enter your PIN to view statement 0. Back CON Tafadhali weka PIN yako kuona taarifa ya matumizi. 0. Rudi CON Tafadhali ikia PIN yaku kwona welesyo wa utumii. 0. Syoka itina CON Ekera pin yaku kuona rugano rwa mahuthira maku 0. coka thutha CON Unavoywa namba fitswe kupata maerezo ga mahumizi Gako. 0. Uya Nyuma CON Kiyie to ket nambani mar siri mondo ine chenro mar tiyo. 0. Dog chien CON Tafadhali weka PIN yako kuona taarifa ya matumizi. 0. Dheebi
55 account_statement_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
56 name_edit_pin_authorization.first CON Please enter your PIN 0. Back CON Tafadhali weka PIN yako 0. Rudi CON Tafadhali ikia PIN yaku 0. Syoka itina CON Ekera pin yaku 0. coka thutha CON Unavoywa namba fitse 0. Uya Nyuma CON Ket nambani mopondo 0. Dog chien CON Namba ka namii imben kekhai 0. Dheebi
57 name_edit_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
58 dob_edit_pin_authorization.first CON Please enter your PIN 0. Back CON Tafadhali weka PIN yako 0. Rudi CON Tafadhali ikia PIN yaku 0. Syoka itina CON Ekera namba yaku ya thiri 0. Rudi CON Unavoywa namba fitswe 0. Uya nyuma CON Kiyie to ket nambani mopondo 0. Dog chien CON Namba kake ka namii imbeen kekhai 0. Dheebi
59 dob_edit_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
60 gender_edit_pin_authorization.first CON Please enter your PIN 0. Back CON Tafadhali weka PIN yako 0. Rudi CON Tafadhali ikia PIN yaku 0. Syoka itina CON Ekera namba yaku ya thiri 0. coka thutha CON Unavoywa namba fitswe 0. Uya nyuma CON Kiyie to ket nambani mopondo 0. Dog chien CON Namba kake ka namii imbeen kekhai 0. Dheebi
61 gender_edit_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
62 location_edit_pin_authorization.first CON Please enter your PIN 0. Back CON Tafadhali weka PIN yako 0. Rudi CON Tafadhali ikia PIN yaku 0. Syoka itina CON Ekera namba yaku ya thiri 0. Coka thutha CON Unavoywa namba fitswe 0. Uya nyuma CON Kiyie to ket nambani mopondo 0. Dog chien CON Namba kake ka namii imbeen kekhai 0. Dheebi
63 location_edit_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
64 products_edit_pin_authorization.first CON Please enter your PIN 0. Back CON Tafadhali weka PIN yako 0. Rudi CON Tafadhali ikia PIN yaku 0. Syoka itina CON Ekera namba yaku ya thiri 0. Coka thutha CON Unavoywa namba fitswe 0. Uya nyuma CON Kiyie to ket nambani mopondo 0. Dog chien CON Namba kake ka namii imbeen kekhai 0. Dheebi
65 products_edit_pin_authorization.retry %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry} %{retry_pin_entry}
66 account_balances.available_balance CON Your balances are as follows: balance: %{available_balance} %{token_symbol} 0. Back CON Salio zako ni zifuatazo: salio: %{available_balance} %{token_symbol} 0. Rudi CON Utyalo waku ni uu: utyalo: %{available_balance} %{token_symbol} 0. Syoka itina CON Matigari maku ni maya: matigari: %{available_balance} %{token_symbol} 0. coka CON Masazogo nidzavivyo gatuwiravyo: Sazo: %{available_balance} %{token_symbol} 0. Uya Nyuma CON Dong mari en: Dong: %{available_balance} %{token_symbol} 0. Dog chien CON Balansi kake akan salio: %{available_balance} %{token_symbol} 0. Dheebi
67 account_balances.with_fees CON Your balances are as follows: balances: %{available_balance} %{token_symbol} fees: %{tax} %{token_symbol} 0. Back CON Salio zako ni zifuatazo: salio: %{available_balance} %{token_symbol} ushuru: %{tax} %{token_symbol} 0. Rudi CON Utyalo waku ni uu: utyalo: %{available_balance} %{token_symbol} tax/ushuru: %{tax} %{token_symbol} 0. Syoka itina CON Matigari maku ni maya: matigari: %{available_balance} %{token_symbol} ushuru: %{tax} %{token_symbol} 0. coka thutha CON Masazogo ni gatuwirago: Masazo: %{available_balance} %{token_symbol} Ushuuru: %{tax} %{token_symbol} 0. Uya nyuma CON Dong mari en: Dong: %{available_balance} %{token_symbol} osuru: %{tax} %{token_symbol} 0. Dog chien CON Balansi kake akan salio: %{available_balance} %{token_symbol} ushuru: %{tax} %{token_symbol} 0. Dheebi
68 account_balances.with_rewards CON Your balances are as follows: balance: %{available_balance} %{token_symbol} fees: %{tax} %{token_symbol} rewards: %{bonus} %{token_symbol} 0. Back CON Salio zako ni zifuatazo: salio: %{available_balance} %{token_symbol} ushuru: %{tax} %{token_symbol} tuzo: %{bonus} %{token_symbol} 0. Rudi CON Utyalo waku ni uu: Utyalo: %{available_balance} %{token_symbol} Tax/ushuru: %{tax} %{token_symbol} muthinzio: %{bonus} %{token_symbol} 0. Syoka itina CON Salio zako ni zifuatazo: salio: %{available_balance} %{token_symbol} ushuru: %{tax} %{token_symbol} tuzo: %{bonus} %{token_symbol} 0. coka thutha CON Sazoro ni rituwiraro: Sazo: %{available_balance} %{token_symbol} Ushuuru: %{tax} %{token_symbol} Zawadi: %{bonus} %{token_symbol} 0. Uya Nyuma CON Dong mari en: Dong: %{available_balance} %{token_symbol} osuru: %{tax} %{token_symbol} mich: %{bonus} %{token_symbol} 0. Dog chien CON Balansi kake akan salio: %{available_balance} %{token_symbol} ushuru: %{tax} %{token_symbol} tuzo: %{bonus} %{token_symbol} 0. Dheebi
69 first_transaction_set CON %{first_transaction_set} 0. Back 11. Next 00. Exit CON %{first_transaction_set} 0. Rudi 11. Mbele 00. Ondoka CON %{first_transaction_set} 1. Mbee 00. Ondoka CON %{first_transaction_set} 1. Mbere 00. uma CON %{first_transaction_set} 1. Mbere 00. Uka CON %{first_transaction_set} 1. Nyime 00. Wuogi CON %{first_transaction_set} 1. Dhuur 00. Bai
70 middle_transaction_set CON %{middle_transaction_set} 11. Next 22. Previous 00. Exit CON %{middle_transaction_set} 11. Mbele 22. Rudi 00. Ondoka CON %{middle_transaction_set} 1. Mbee 2. Itina 00. Ondoka CON %{middle_transaction_set} 1. Mbere 2. coka thutha 00. Uma CON %{middle_transaction_set} 1. Mbere 2. Uya nyuma 00. Uka CON %{middle_transaction_set} 1. Nyime 2. Dog chien 00. Wuogi CON %{middle_transaction_set} 1. Dhuur 2. Dheebi 00. Bai
71 last_transaction_set CON %{last_transaction_set} 22. Previous 00. Exit CON %{last_transaction_set} 22. Rudi 00. Ondoka CON %{last_transaction_set} 2. Itina 00. Ondoka CON %{last_transaction_set} 2. Coka thutha 00. Uma CON %{last_transaction_set} 2. Uya Nyuma 00. Uka CON %{last_transaction_set} 2. Dog chien 00. Wuogi CON %{last_transaction_set} 2. Dhuur 00. Bai
72 exit END Thank you for using the service. END Asante kwa kutumia huduma. END Ni muvea kwa kutumia huduma ii. END Thegio ni kuhuthira mutabo END. Namvera kwa mahumizi ga ii huduma. END Erokamano kuom tiyo kodwa. END Asante kwa kutumia huduma.
73 exit_invalid_request END Invalid request. END Chaguo si sahihi. END Usakuo waku uyaile. END shaguro riaku ti riega END. Tsagulo karisawa sawa END Yiero okni kare END Ka at chaquad suninit
74 exit_invalid_menu_option CON Invalid menu option. For help,call %{support_phone}. 00. Back 99. Exit CON Chaguo lako sio sahihi. Kwa usaidizi piga simu %{support_phone} 00. Rudi 99. Ondoka CON Usakuo waku uyaile. Kwa utethyo kuna simu %{support_phone} 00. Itina 99. Ondoka CON Shaguro riaku ti riega.Kwa uteithio hura %{support_phone} 00. Coka thutha 99. Uma CON Tsaguloro karisawa sawa. Kuavizwa piga %{support_phone} 00. Uya Nyuma 99. Uka CON Yiero ni oknikare. Kuom kony go simu e %{support_phone} 00. Dog chien 99. Wuogi CON Ka at chaqui suninit.qarqarsa simu dai%{support_phone} 00. Dheebi 99. Bai
75 exit_invalid_input CON Invalid input. Nothing selected 00. Back 99. Exit CON Chaguo lako halipatikani. Hakuna kilichochaguliwa. 00. Rudi 99. Ondoka CON Usakuo waku wikwonekana.Vaii kindu kisakue. 00. Itina 99. Ondoka CON Shaguro riaku ritironekana. Hatiri kindu washaghura. 00. Coka thutha 99. Uma CON Tsaguloro karipatikana. Kakuna Kutsagurire chochosi. 00. Uya nyuma 99. Uka CON Yiero ni okyudre. Onge gima iyiero. 00. Dog chien 99. Wuogi CON Ka at chaguad injirt. oo 00. dheebi 99. Bai
76 exit_pin_blocked END Your PIN has been blocked. For help, please call %{support_phone}. END PIN yako imefungwa. Kwa usaidizi tafadhali piga simu %{support_phone}. END PIN yaku niyavingwa. Kutethwa kuna simu ino %{support_phone}. END PIN yaku niyahingwo. Kwa uteithio hura thimu %{support_phone}. END. Namba fitse yakwako ifungwa. Kwa kuavizwa unaangwa upige simu %{support_phone}. END Nambani mopondo olor. Kuom kony go simu e %{support_phone}. END Pin kake yahidat. Qarqarsa simu dai %{support_phone}.
77 exit_invalid_pin END The PIN you have entered is invalid. PIN must consist of 4 digits. For help, call %{support_phone}. END PIN uliyobonyeza sio sahihi. PIN lazima iwe na nambari nne. Kwa usaidizi piga simu %{support_phone}. END PIN ila wekia iyaile. Ni lasima PIN ithiwe na namba inya. Kutethwa kuna namba ii %{support_phone}. END PIN iria wekera tii njega. PIN nomoka ikorwo na namba inya. Kwa uteithio hura thimu %{support_phone}. END Namba fitse urohofya seyo, kaisawa. Namba fitswe inamalwa ikale na namba nee. Kwa kuvizwa, piga simu%{support_phone}. END. Namba mopondo miketo oknikare. Nyaka obed gi nembni ang'wen. Kuom kony go simu e %{support_phone}. END PIN ka at keket suninit. PIN Pin namba afuuri tatatu. Qarqarsa simu dai %{support_phone}.
78 exit_invalid_new_pin END The PIN you have entered is invalid. PIN must be different from your current PIN. For help, call %{support_phone}. END PIN uliyobonyeza sio sahihi. PIN lazima iwe tofauti na pin yako ya sasa. Kwa usaidizi piga simu %{support_phone}. END PIN ila wekia iyaile. PIN ni lasima ithiwe tofauti na pin yaku ya oyu. Kutethwa kuna namba ii %{support_phone}. END PIN uria wekera ti njega. PIN nomohaka ikorwo na namba ndiganu na ya riu . Kwa uteithio hora thimu %{support_phone}. END Namba fitswe uriohopya siyo ya karakara. Namba fitswe inahenzekana ikale itofauti na uhumirayo vivi. Kwa maavizo piga simu %{support_phone}. END Namba mopondo miketo oknikare. Nyaka obed mopogore gi nambani mopondo masani. Kuom kony gochi e %{support_phone}. END PIN ka at keket suninit.Pin kake walinfakaatin.Qarqars simu dai %{support_phone}.
79 exit_pin_mismatch END The new PIN does not match the one you entered. Please try again. For help, call %{support_phone}. END PIN mpya na udhibitisho wa pin mpya hazilingani. Tafadhali jaribu tena. Kwa usaidizi piga simu %{support_phone}. END PIN yumbya na uhakikisho wa pin yumbya syivwanene. Tafadhali tata ingi. Kutethwa kuna simu %{support_phone}. END PIN njeru na pin ya guetekeria shitira hianana . Geria ringi. Kwa uteithio hora thimu %{support_phone}. END Namba fitse uzdoinjiza kaikara kara na uriyohopya laphiri. Unavoywa ujeze kaheri. Kwa kuavizwa piga simu %{support_phone}. GIKO. Namba mopondo miketo opogore gi manikuongo keto. Kiyie to ket kendo. Kuom kony gochi e %{support_phone}. END PIN mpya na udhibitisho wa pin mpya hazilingani. Pin hareti ka at kekeet walinfakan. it dheebi amaale.Qarqars simu dai%{support_phone}.
80 exit_invalid_recipient CON Recipient's phone number is not registered or is invalid: 00. Retry 99. Exit CON Mpokeaji wa nambari hapatikani au sio sahihi. 00. Jaribu tena 99. Ondoka CON Mukwati wa namba ndokwatikana kana ii namba iyaile kana ti sahihi. 00. Tata ingi 99. Ondoka CON Mpokeaji wa nambari hapatikani au sio sahihi.Namba ya mutumirwo ndiranyitikana kana ti njega 00. Geria ringi 99. Uma CON Muphokezi wa namba kapatikana ama namba kai karakara. 00. Jeza Kaheri 99. Uka CON Jayuto mar nambani okyudre kata oknikare. 00. Tem kendo 99. Wuogi CON Mpokeaji wa nambari hapatikani au sio sahihi. 00. 99. Ondoka
81 exit_successful_transaction CON Your request has been sent. %{recipient_information} will receive %{transaction_amount} %{token_symbol} from %{sender_information}. 00. Back 99. Exit CON Ombi lako limetumwa. %{recipient_information} atapokea %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}. 00. Rudi 99. Ondoka CON Woni waku niwatumwa. %{recipient_information} nukupokea %{transaction_amount} %{token_symbol} kuma kwa %{sender_information}. 00. Itina 99. Ondoka CON Mahoya maku nimatomwo. %{recipient_information} akuamukira%{transaction_amount} %{token_symbol} kuma kwa %{sender_information}. 00. Coka 99. Uma CON Mavoyogo gahumwa. %{recipient_information} undaphokera %{transaction_amount} %{token_symbol} kuombola kwa %{sender_information}. 00. Uya Nyuma 99. Uka CON Kwayo ni oseor. %{recipient_information} oboyudo %{transaction_amount} %{token_symbol} kowuok kuom %{sender_information}. 00. Dog chien 99. Wuogi CON Qarqar kake yaergad. %{recipient_information} inargat %{transaction_amount} %{token_symbol} kutoka kwa %{sender_information}. 00. Dheebi 99. Bai
82 exit_insufficient_balance CON Payment of %{amount} %{token_symbol} to %{recipient_information} has failed due to insufficient balance. Your Sarafu-Network balances is: %{token_balance} 00. Back 99. Exit CON Malipo ya %{amount} %{token_symbol} kwa %{recipient_information} halijakamilika kwa sababu salio lako haitoshi. Akaunti yako ya Sarafu ina salio ifuatayo: %{token_balance} 00. Rudi 99. Ondoka CON Ndivi ya %{amount} %{token_symbol} kwa %{recipient_information} inavitukithwa nundu utyalyo waku ni munini. Kinandu chaku cha Sarafu kina utyalo uu: %{token_balance} 00. Itina 99. Ondoka CON Marehi ma %{amount} %{token_symbol} kwa %{recipient_information} matinarekereka tondu matigari maku matiraigana. Akaunti yako ya Sarafu ina salio ifuatayo: %{token_balance} 00. Coka 99. Uma CON Maripho ga %{amount} %{token_symbol} kwa %{recipient_information} Karidzangwe kukamirika Kwaukala sazoro Karitosha. Akauntiyo vivi ina sazo dza rituwiranavyo: %{token_balance} 00. Uya nyuma 99. Uka CON Chudo mar %{amount} %{token_symbol} kuom %{recipient_information} okotieki nikech dong ni okrom. Akaont ni mar Sarafu ni gi dong mar: %{token_balance} 00. Dog chien 99. Wuogi CON Malipo ka%{amount} %{token_symbol} kwa %{recipient_information} Inkamilikee balansi kake ingau Akaunti kake balansi akan kabd: %{token_balance} 00. Dheebi 99. Bai
83 exit_successful_token_selection CON Success! %{token_symbol} is your active Sarafu. 00. Back 99. Exit CON Chaguo lako limekamilika, %{token_symbol} ni sarafu itakayotumika. 00. Rudi 99. Ondoka
84 invalid_service_code Please dial %{valid_service_code} to access Sarafu Network Bonyeza %{valid_service_code} kutumia mtandao wa Sarafu Vinyia %{valid_service_code} kutumia mutandao wa Sarafu Hihinya%{valid_service_code} kuhudhira mutabo wa Sarafu Hofya %{valid_service_code} Kuhumira Mutandao wa sarafu Dii %{valid_service_code} mondo iti gi Sarafu Bonyeza %{valid_service_code} kutumia mtandao wa Sarafu
85 help CON For assistance call %{support_phone} 00. Back 99. Exit CON Kwa usaidizi piga simu %{support_phone} 0. Rudi 9. Ondoka CON Kwa utethyo kuna simu %{support_phone} 0. Itina 9. Ondoka CON Kwa uteithio hora thimu %{support_phone} 0. Coka 9. Uma CON Kwa Kuavizwa piga simu %{support_phone} 0. Uya nyuma 9. Uka CON Kuom kony go simu e %{support_phone} 0. Dog chien 9. Wuogi CON Qarqars simu dai%{support_phone} 0. Dheebi 9. Bai
86 complete CON Your request has been sent. You will receive an SMS shortly. 00. Back 99. Exit CON Ombi lako limetumwa. Utapokea uthibitishaji wa SMS kwa muda mfupi. 00. Rudi 99. Ondoka CON Woni waku niwatumwa. Nukwata SMS ya kwonya ivinda ite yasa. 00. Itina 99. Ondoka CON Mahoya maku nimatomwo. Niukuamukira SMS ya guitikirika ihinda ikuhi . 00. Coka 99. Uma CON Vyoyoro rihumwa. Undaphokera Uthibitishaji wa SMS kwa muda mufuhi. 00. Uya nyuma 99. Uka CON Kwayo ni oseor. Iboyudo mesej mar ote ni bang' saa matin. 00. Dog chien 99. Wuogi CON Qarqars kake yaergad. Utapokea uthibitishaji wa SMS kwa muda mfupi. 00. Dheebi 99. Bai
87 account_creation_prompt END Your account is being created. You will receive an SMS when your account is ready. END Akaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari. END Akaunti yako ya Sarafu yendeye usovwa. Nukwata SMS akaunti yaku yasovwa. END Akaunti yaku ya Sarafu niiraharirio.Niugutumirwo SMS akauti yaku ya rikio kuharirio END Akauntiyo ya sarafu idzikoni. Undaphokera ujumbe wa SMS ichikala tayari. END Akaont ni mar Sarafu iloso. Iboyudo mesej ka akaont ni otieki. END Akaunti yako ya Sarafu inatayarishwa. Utapokea ujumbe wa SMS akaunti yako ikiwa tayari.
88 initial_middle_language_set CON Choose language: %{middle_language_set} 11. Next 22. Previous 00. Exit CON Chagua lugha: %{middle_language_set} 11. Mbele 22. Rudi 00. Ondoka
89 initial_last_language_set CON Choose language: %{last_language_set} 22. Previous 00. Exit CON Choose language: %{last_language_set} 22. Rudi 00. Ondoka
90 middle_language_set CON Choose language: %{middle_language_set} 11. Next 22. Previous 00. Exit CON Chagua lugha: %{middle_language_set} 11. Mbele 22. Rudi 00. Ondoka
91 last_language_set CON Choose language: %{last_language_set} 22. Previous 00. Exit CON Choose language: %{last_language_set} 22. Rudi 00. Ondoka

52
docker-compose.build.yml Normal file
View File

@ -0,0 +1,52 @@
version: '3.2'
services:
cic-cache:
image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-cache:${TAG:-latest}
build:
context: apps/cic-cache
dockerfile: docker/Dockerfile
cache_from:
- ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-cache:latest
cic-eth:
image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-eth:${TAG:-latest}
build:
context: apps/cic-eth
dockerfile: docker/Dockerfile
cache_from:
- ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-eth:latest
cic-meta:
image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-meta:${TAG:-latest}
build:
context: apps/cic-meta
dockerfile: docker/Dockerfile
cache_from:
- ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-meta:latest
cic-notify:
image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-notify:${TAG:-latest}
build:
context: apps/cic-notify
dockerfile: docker/Dockerfile
cache_from:
- ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-notify:latest
funga-eth:
image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/funga-eth:${TAG:-latest}
build:
context: apps/cic-signer
dockerfile: Dockerfile
cache_from:
- ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/funga-eth:latest
cic-ussd:
image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-ussd:${TAG:-latest}
build:
context: apps/cic-ussd
dockerfile: docker/Dockerfile
cache_from:
- ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-ussd:latest
cic-contract-bootstrap:
image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-contract-bootstrap:${TAG:-latest}
build:
context: apps/contract-migration
dockerfile: docker/Dockerfile
cache_from:
- ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-ussd:latest

View File

@ -0,0 +1,23 @@
#!/usr/bin/env sh
# dependencies:
# - docker-compose >= v1.25.0
# - sbot >= v1.0.0.
set -e
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
PREDICTED_TAG=$(sbot predict version -m auto)
docker-compose -f docker-compose.build.yml build --progress plain
export TAG=v$PREDICTED_TAG
docker-compose -f docker-compose.build.yml build --progress plain
docker-compose -f docker-compose.build.yml push
export TAG=latest
docker-compose -f docker-compose.build.yml push