diff --git a/apps/cic-ussd/cic_ussd/account/balance.py b/apps/cic-ussd/cic_ussd/account/balance.py index eddeb4aa..904cf737 100644 --- a/apps/cic-ussd/cic_ussd/account/balance.py +++ b/apps/cic-ussd/cic_ussd/account/balance.py @@ -20,7 +20,7 @@ def get_balances(address: str, asynchronous: bool = False, callback_param: any = None, callback_queue='cic-ussd', - callback_task='cic_ussd.tasks.callback_handler.process_balances_callback') -> Optional[list]: + callback_task='cic_ussd.tasks.callback_handler.balances_callback') -> Optional[list]: """This function queries cic-eth for an account's balances, It provides a means to receive the balance either asynchronously or synchronously.. It returns a dictionary containing the network, outgoing and incoming balances. :param address: Ethereum address of an account. diff --git a/apps/cic-ussd/cic_ussd/account/transaction.py b/apps/cic-ussd/cic_ussd/account/transaction.py index fdd44daa..02b22270 100644 --- a/apps/cic-ussd/cic_ussd/account/transaction.py +++ b/apps/cic-ussd/cic_ussd/account/transaction.py @@ -117,18 +117,18 @@ def transaction_actors(transaction: dict) -> Tuple[Dict, Dict]: return recipient_transaction_data, sender_transaction_data -def validate_transaction_account(session: Session, transaction: dict) -> Account: +def validate_transaction_account(blockchain_address: str, role: str, session: Session) -> Account: """This function checks whether the blockchain address specified in a parsed transaction object resolves to an account object in the ussd system. - :param session: Database session object. - :type session: Session - :param transaction: Parsed transaction data object. - :type transaction: dict + :param blockchain_address: + :type blockchain_address: + :param role: + :type role: + :param session: + :type session: :return: :rtype: """ - blockchain_address = transaction.get('blockchain_address') - role = transaction.get('role') session = SessionBase.bind_session(session) account = session.query(Account).filter_by(blockchain_address=blockchain_address).first() if not account: diff --git a/apps/cic-ussd/cic_ussd/processor/util.py b/apps/cic-ussd/cic_ussd/processor/util.py index fc8ea38e..83b4c600 100644 --- a/apps/cic-ussd/cic_ussd/processor/util.py +++ b/apps/cic-ussd/cic_ussd/processor/util.py @@ -67,6 +67,7 @@ def resume_last_ussd_session(last_state: str) -> Document: 'exit', 'exit_invalid_pin', 'exit_invalid_new_pin', + 'exit_invalid_recipient', 'exit_invalid_request', 'exit_pin_blocked', 'exit_pin_mismatch', diff --git a/apps/cic-ussd/cic_ussd/state_machine/logic/transaction.py b/apps/cic-ussd/cic_ussd/state_machine/logic/transaction.py index a80e2317..1667908d 100644 --- a/apps/cic-ussd/cic_ussd/state_machine/logic/transaction.py +++ b/apps/cic-ussd/cic_ussd/state_machine/logic/transaction.py @@ -4,6 +4,7 @@ from typing import Tuple # third party imports import celery +from phonenumbers.phonenumberutil import NumberParseException # local imports from cic_ussd.account.balance import get_cached_available_balance @@ -21,23 +22,21 @@ logg = logging.getLogger(__file__) def is_valid_recipient(state_machine_data: Tuple[str, dict, Account, Session]) -> bool: - """This function checks that a user exists, is not the initiator of the transaction, has an active account status - and is authorized to perform standard transactions. + """This function checks that a phone number provided as the recipient of a transaction does not match the sending + party's own phone number. :param state_machine_data: A tuple containing user input, a ussd session and user object. :type state_machine_data: tuple - :return: A user's validity + :return: A recipient account's validity for a transaction :rtype: bool """ user_input, ussd_session, account, session = state_machine_data - phone_number = process_phone_number(user_input, E164Format.region) - session = SessionBase.bind_session(session=session) - recipient = Account.get_by_phone_number(phone_number=phone_number, session=session) - SessionBase.release_session(session=session) + try: + phone_number = process_phone_number(user_input, E164Format.region) + except NumberParseException: + phone_number = None is_not_initiator = phone_number != account.phone_number - has_active_account_status = False - if recipient: - has_active_account_status = recipient.get_status(session) == AccountStatus.ACTIVE.name - return is_not_initiator and has_active_account_status and recipient is not None + is_present = Account.get_by_phone_number(phone_number, session) is not None + return phone_number is not None and phone_number.startswith('+') and is_present and is_not_initiator def is_valid_transaction_amount(state_machine_data: Tuple[str, dict, Account, Session]) -> bool: diff --git a/apps/cic-ussd/cic_ussd/tasks/callback_handler.py b/apps/cic-ussd/cic_ussd/tasks/callback_handler.py index de9125ba..c8350996 100644 --- a/apps/cic-ussd/cic_ussd/tasks/callback_handler.py +++ b/apps/cic-ussd/cic_ussd/tasks/callback_handler.py @@ -138,7 +138,7 @@ def transaction_balances_callback(self, result: list, param: dict, status_code: balances_data = result[0] available_balance = calculate_available_balance(balances_data) transaction = param - blockchain_address = param.get('blockchain_address') + blockchain_address = transaction.get('blockchain_address') transaction['available_balance'] = available_balance queue = self.request.delivery_info.get('routing_key') @@ -150,10 +150,10 @@ def transaction_balances_callback(self, result: list, param: dict, status_code: ) s_notify_account = celery.signature('cic_ussd.tasks.notifications.transaction', queue=queue) - if param.get('transaction_type') == 'transfer': + if transaction.get('transaction_type') == 'transfer': celery.chain(s_preferences_metadata, s_process_account_metadata, s_notify_account).apply_async() - if param.get('transaction_type') == 'tokengift': + if transaction.get('transaction_type') == 'tokengift': s_process_account_metadata = celery.signature( 'cic_ussd.tasks.processor.parse_transaction', [{}, transaction], queue=queue ) @@ -184,10 +184,11 @@ def transaction_callback(result: dict, param: str, status_code: int): source_token_value = result.get('source_token_value') recipient_metadata = { - "token_symbol": destination_token_symbol, - "token_value": destination_token_value, + "alt_blockchain_address": sender_blockchain_address, "blockchain_address": recipient_blockchain_address, "role": "recipient", + "token_symbol": destination_token_symbol, + "token_value": destination_token_value, "transaction_type": param } @@ -201,10 +202,11 @@ def transaction_callback(result: dict, param: str, status_code: int): if param == 'transfer': sender_metadata = { + "alt_blockchain_address": recipient_blockchain_address, "blockchain_address": sender_blockchain_address, + "role": "sender", "token_symbol": source_token_symbol, "token_value": source_token_value, - "role": "sender", "transaction_type": param } diff --git a/apps/cic-ussd/cic_ussd/tasks/notifications.py b/apps/cic-ussd/cic_ussd/tasks/notifications.py index c290bf51..e28f1567 100644 --- a/apps/cic-ussd/cic_ussd/tasks/notifications.py +++ b/apps/cic-ussd/cic_ussd/tasks/notifications.py @@ -29,7 +29,8 @@ def transaction(notification_data: dict): phone_number = notification_data.get('phone_number') preferred_language = notification_data.get('preferred_language') token_symbol = notification_data.get('token_symbol') - transaction_account_metadata = notification_data.get('metadata_id') + alt_metadata_id = notification_data.get('alt_metadata_id') + metadata_id = notification_data.get('metadata_id') transaction_type = notification_data.get('transaction_type') timestamp = datetime.datetime.now().strftime('%d-%m-%y, %H:%M %p') @@ -47,7 +48,8 @@ def transaction(notification_data: dict): preferred_language=preferred_language, amount=amount, token_symbol=token_symbol, - tx_sender_information=transaction_account_metadata, + tx_recipient_information=metadata_id, + tx_sender_information=alt_metadata_id, timestamp=timestamp, balance=balance) if role == 'sender': @@ -56,6 +58,7 @@ def transaction(notification_data: dict): preferred_language=preferred_language, amount=amount, token_symbol=token_symbol, - tx_recipient_information=transaction_account_metadata, + tx_recipient_information=alt_metadata_id, + tx_sender_information=metadata_id, timestamp=timestamp, balance=balance) diff --git a/apps/cic-ussd/cic_ussd/tasks/processor.py b/apps/cic-ussd/cic_ussd/tasks/processor.py index 4a614c15..287dbc58 100644 --- a/apps/cic-ussd/cic_ussd/tasks/processor.py +++ b/apps/cic-ussd/cic_ussd/tasks/processor.py @@ -11,6 +11,7 @@ from chainlib.hash import strip_0x from cic_ussd.account.statement import get_cached_statement from cic_ussd.account.transaction import aux_transaction_data, validate_transaction_account from cic_ussd.cache import cache_data, cache_data_key +from cic_ussd.db.models.account import Account from cic_ussd.db.models.base import SessionBase @@ -72,13 +73,17 @@ def parse_transaction(preferences: dict, transaction: dict) -> dict: preferred_language = preferences.get('preferred_language') if not preferred_language: preferred_language = i18n.config.get('fallback') - + transaction['preferred_language'] = preferred_language transaction = aux_transaction_data(preferred_language, transaction) session = SessionBase.create_session() - account = validate_transaction_account(session, transaction) - metadata_id = account.standard_metadata_id() - transaction['metadata_id'] = metadata_id + role = transaction.get('role') + alt_blockchain_address = transaction.get('alt_blockchain_address') + blockchain_address = transaction.get('blockchain_address') + account = validate_transaction_account(blockchain_address, role, session) + alt_account = session.query(Account).filter_by(blockchain_address=alt_blockchain_address).first() + if alt_account: + transaction['alt_metadata_id'] = alt_account.standard_metadata_id() + transaction['metadata_id'] = account.standard_metadata_id() transaction['phone_number'] = account.phone_number - session.commit() session.close() return transaction diff --git a/apps/cic-ussd/requirements.txt b/apps/cic-ussd/requirements.txt index 57af4f31..fbe7304f 100644 --- a/apps/cic-ussd/requirements.txt +++ b/apps/cic-ussd/requirements.txt @@ -13,11 +13,5 @@ redis==3.5.3 semver==2.13.0 SQLAlchemy==1.3.20 tinydb==4.2.0 -phonenumbers==8.12.12 -redis==3.5.3 -celery==4.4.7 -python-i18n[YAML]==0.3.9 -pyxdg==0.27 -bcrypt==3.2.0 -uWSGI==2.0.19.1 transitions==0.8.4 +uWSGI==2.0.19.1 \ No newline at end of file diff --git a/apps/cic-ussd/tests/cic_ussd/account/test_transaction.py b/apps/cic-ussd/tests/cic_ussd/account/test_transaction.py index aaf979e5..de8ac516 100644 --- a/apps/cic-ussd/tests/cic_ussd/account/test_transaction.py +++ b/apps/cic-ussd/tests/cic_ussd/account/test_transaction.py @@ -75,17 +75,21 @@ def test_transaction_actors(activated_account, transaction_result, valid_recipie def test_validate_transaction_account(activated_account, init_database, transactions_list): sample_transaction = transactions_list[0] recipient_transaction, sender_transaction = transaction_actors(sample_transaction) - recipient_account = validate_transaction_account(init_database, recipient_transaction) - sender_account = validate_transaction_account(init_database, sender_transaction) + recipient_account = validate_transaction_account( + recipient_transaction.get('blockchain_address'), recipient_transaction.get('role'), init_database) + sender_account = validate_transaction_account( + sender_transaction.get('blockchain_address'), sender_transaction.get('role'), init_database) assert isinstance(recipient_account, Account) assert isinstance(sender_account, Account) sample_transaction = transactions_list[1] recipient_transaction, sender_transaction = transaction_actors(sample_transaction) with pytest.raises(UnknownUssdRecipient) as error: - validate_transaction_account(init_database, recipient_transaction) + validate_transaction_account( + recipient_transaction.get('blockchain_address'), recipient_transaction.get('role'), init_database) assert str( error.value) == f'Tx for recipient: {recipient_transaction.get("blockchain_address")} has no matching account in the system.' - validate_transaction_account(init_database, sender_transaction) + validate_transaction_account( + sender_transaction.get('blockchain_address'), sender_transaction.get('role'), init_database) assert f'Tx from sender: {sender_transaction.get("blockchain_address")} has no matching account in system.' diff --git a/apps/cic-ussd/tests/cic_ussd/state_machine/logic/test_transaction_logic.py b/apps/cic-ussd/tests/cic_ussd/state_machine/logic/test_transaction_logic.py index f2acf054..6038f687 100644 --- a/apps/cic-ussd/tests/cic_ussd/state_machine/logic/test_transaction_logic.py +++ b/apps/cic-ussd/tests/cic_ussd/state_machine/logic/test_transaction_logic.py @@ -30,8 +30,6 @@ def test_is_valid_recipient(activated_account, valid_recipient): state_machine = ('0112365478', generic_ussd_session, valid_recipient, init_database) assert is_valid_recipient(state_machine) is False - state_machine = (pending_account.phone_number, generic_ussd_session, valid_recipient, init_database) - assert is_valid_recipient(state_machine) is False state_machine = (valid_recipient.phone_number, generic_ussd_session, activated_account, init_database) assert is_valid_recipient(state_machine) is True diff --git a/apps/cic-ussd/tests/cic_ussd/tasks/test_notifications_tasks.py b/apps/cic-ussd/tests/cic_ussd/tasks/test_notifications_tasks.py index 61e19761..8ad7faab 100644 --- a/apps/cic-ussd/tests/cic_ussd/tasks/test_notifications_tasks.py +++ b/apps/cic-ussd/tests/cic_ussd/tasks/test_notifications_tasks.py @@ -24,7 +24,8 @@ def test_transaction(celery_session_worker, phone_number = notification_data.get('phone_number') preferred_language = notification_data.get('preferred_language') token_symbol = notification_data.get('token_symbol') - transaction_account_metadata = notification_data.get('metadata_id') + alt_metadata_id = notification_data.get('alt_metadata_id') + metadata_id = notification_data.get('metadata_id') timestamp = datetime.datetime.now().strftime('%d-%m-%y, %H:%M %p') s_transaction = celery.signature( 'cic_ussd.tasks.notifications.transaction', [notification_data] @@ -36,7 +37,8 @@ def test_transaction(celery_session_worker, preferred_language=preferred_language, amount=amount, token_symbol=token_symbol, - tx_recipient_information=transaction_account_metadata, + tx_recipient_information=alt_metadata_id, + tx_sender_information=metadata_id, timestamp=timestamp, balance=balance) assert mock_notifier_api.get('message') == message @@ -52,7 +54,8 @@ def test_transaction(celery_session_worker, preferred_language=preferred_language, amount=amount, token_symbol=token_symbol, - tx_sender_information=transaction_account_metadata, + tx_recipient_information=metadata_id, + tx_sender_information=alt_metadata_id, timestamp=timestamp, balance=balance) assert mock_notifier_api.get('message') == message diff --git a/apps/cic-ussd/tests/fixtures/transaction.py b/apps/cic-ussd/tests/fixtures/transaction.py index 9dacfaa5..78953da4 100644 --- a/apps/cic-ussd/tests/fixtures/transaction.py +++ b/apps/cic-ussd/tests/fixtures/transaction.py @@ -5,12 +5,18 @@ import random import pytest # local import +from cic_ussd.account.balance import get_cached_available_balance # tests imports @pytest.fixture(scope='function') -def notification_data(activated_account, cache_person_metadata, cache_preferences, preferences): +def notification_data(activated_account, + cache_person_metadata, + cache_preferences, + cache_balances, + preferences, + valid_recipient): return { 'blockchain_address': activated_account.blockchain_address, 'token_symbol': 'GFT', @@ -18,6 +24,7 @@ def notification_data(activated_account, cache_person_metadata, cache_preference 'role': 'sender', 'action_tag': 'Sent', 'direction_tag': 'To', + 'alt_metadata_id': valid_recipient.standard_metadata_id(), 'metadata_id': activated_account.standard_metadata_id(), 'phone_number': activated_account.phone_number, 'available_balance': 50.0, diff --git a/apps/cic-ussd/var/lib/locale/sms.en.yml b/apps/cic-ussd/var/lib/locale/sms.en.yml index a999c799..678fbb15 100644 --- a/apps/cic-ussd/var/lib/locale/sms.en.yml +++ b/apps/cic-ussd/var/lib/locale/sms.en.yml @@ -2,9 +2,9 @@ en: 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}. received_tokens: |- - Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp}. New balance is %{balance} %{token_symbol}. + Successfully received %{amount} %{token_symbol} from %{tx_sender_information} %{timestamp} to %{tx_recipient_information}. New balance is %{balance} %{token_symbol}. sent_tokens: |- - Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp}. New balance is %{balance} %{token_symbol}. + Successfully sent %{amount} %{token_symbol} to %{tx_recipient_information} %{timestamp} from %{tx_sender_information}. New balance is %{balance} %{token_symbol}. terms: |- By using the service, you agree to the terms and conditions at http://grassecon.org/tos upsell_unregistered_recipient: |- diff --git a/apps/cic-ussd/var/lib/locale/sms.sw.yml b/apps/cic-ussd/var/lib/locale/sms.sw.yml index df07d132..e2647cf2 100644 --- a/apps/cic-ussd/var/lib/locale/sms.sw.yml +++ b/apps/cic-ussd/var/lib/locale/sms.sw.yml @@ -2,9 +2,9 @@ sw: account_successfully_created: |- Umesajiliwa kwa huduma ya Sarafu! Kutumia bonyeza *384*96# Safaricom ama *483*46# kwa utandao tofauti. Kwa Usaidizi %{support_phone}. received_tokens: |- - Umepokea %{amount} %{token_symbol} kutoka kwa %{tx_sender_information} %{timestamp}. Salio lako ni %{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: |- - Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp}. Salio lako ni %{balance} %{token_symbol}. + Umetuma %{amount} %{token_symbol} kwa %{tx_recipient_information} %{timestamp} kutoka kwa %{tx_sender_information}. Salio lako ni %{balance} %{token_symbol}. terms: |- Kwa kutumia hii huduma, umekubali sheria na masharti yafuatayo http://grassecon.org/tos upsell_unregistered_recipient: |- diff --git a/apps/cic-ussd/var/lib/locale/ussd.en.yml b/apps/cic-ussd/var/lib/locale/ussd.en.yml index ac7d4e5c..2fe70f30 100644 --- a/apps/cic-ussd/var/lib/locale/ussd.en.yml +++ b/apps/cic-ussd/var/lib/locale/ussd.en.yml @@ -7,10 +7,8 @@ en: 3. Help initial_pin_entry: |- CON Please enter a new four number PIN for your account. - 0. Back initial_pin_confirmation: |- CON Enter your four number PIN again - 0. Back enter_given_name: |- CON Enter first name 0. Back @@ -183,7 +181,7 @@ en: exit_pin_mismatch: |- END The new PIN does not match the one you entered. Please try again. For help, call %{support_phone}. exit_invalid_recipient: |- - CON Recipient phone number is incorrect. + CON Recipient's phone number is not registered or is invalid: 00. Retry 99. Exit exit_successful_transaction: |- diff --git a/apps/cic-ussd/var/lib/locale/ussd.sw.yml b/apps/cic-ussd/var/lib/locale/ussd.sw.yml index 4c16c1cf..5c001f75 100644 --- a/apps/cic-ussd/var/lib/locale/ussd.sw.yml +++ b/apps/cic-ussd/var/lib/locale/ussd.sw.yml @@ -7,13 +7,10 @@ sw: 3. Help initial_pin_entry: |- CON Tafadhali weka pin mpya yenye nambari nne kwa akaunti yako - 0. Nyuma initial_pin_confirmation: |- CON Weka PIN yako tena - 0. Nyuma enter_given_name: |- CON Weka jina lako la kwanza - 0. Nyuma enter_family_name: |- CON Weka jina lako la mwisho 0. Nyuma