def admin_reset_user_pin(user: User): pin_reset_token = user.encode_single_use_JWS('R') user.save_pin_reset_token(pin_reset_token) user.failed_pin_attempts = 0 pin_reset_message = i18n_for(user, "general_sms.pin_reset") message_processor.send_message(user.phone, pin_reset_message)
def send_onboarding_message(to_phone, first_name, credits, one_time_code): if to_phone: receiver_message = '{}, you have been registered for {}. You have {} {}. Your one-time code is {}. ' \ 'Download Sempo for Android: https://bit.ly/2UVZLqf' \ .format( first_name, current_app.config['PROGRAM_NAME'], credits if not None else 0, current_app.config['CURRENCY_NAME'], one_time_code, current_app.config['CURRENCY_NAME'] ) message_processor.send_message(to_phone, receiver_message)
def send_onboarding_sms_messages(user): # First send the intro message organisation = getattr(g, 'active_organisation', None) or user.default_organisation intro_message = i18n_for( user, "general_sms.welcome.{}".format(organisation.custom_welcome_message_key or 'generic'), first_name=user.first_name, balance=rounded_dollars(user.transfer_account.balance), token=user.transfer_account.token.name) message_processor.send_message(user.phone, intro_message) send_terms_message_if_required(user)
def approve_user(message_blocks, username, message_ts, phone): new_message_blocks = message_blocks[:len(message_blocks) - 1] new_message_blocks.append( dict(type='context', elements=[{ "type": 'mrkdwn', "text": ':white_check_mark: *@{}* Completed a verification!'.format( username) }])) client.chat_update(channel=CHANNEL_ID, ts=message_ts, blocks=new_message_blocks) if phone: message_processor.send_message( to_phone=phone, message= 'Hooray! Your identity has been successfully verified and Sempo account limits lifted.' ) return make_response("", 200)
def test_send_message(test_client, init_database, mock_sms_apis): from server import message_processor message_processor.send_message("+1401391419", 'bonjour') message_processor.send_message("+961401391419", 'mon') message_processor.send_message("+254796918514", 'chéri') messages = mock_sms_apis assert len(messages) == 3 assert messages == [{ 'phone': '+1401391419', 'message': 'bonjour' }, { 'phone': '+961401391419', 'message': 'mon' }, { 'phone': '+254796918514', 'message': 'chéri' }]
def send_sms(self, message_key, **kwargs): # if we use directory listing similarly for other countries later, can generalize country to init message = i18n_for(self.recipient, "ussd.kenya.{}".format(message_key), **kwargs) message_processor.send_message(self.recipient.phone, message)
def send_sms(user, message_key, **kwargs): # if we use token processor similarly for other countries later, can generalize country to init message = i18n_for(user, "ussd.kenya.{}".format(message_key), **kwargs) message_processor.send_message(user.phone, message)
def send_sms(self, phone, message_key, **kwargs): message = i18n_for(self.user, "ussd.kenya.{}".format(message_key), **kwargs) message_processor.send_message(phone, message)
def handle_trulioo_response(response=None, kyc_application=None): # Record.RecordStatus = match. means successful verification record_errors = None document_errors = None phone = None user = User.query.get(kyc_application.user_id) if user is not None: phone = user.phone authenticity_reasons = ["DatacomparisonTooLow", "ExpiredDocument", "ValidationFailure", "LivePhotoNOMatch", "UnclassifiedDocument", "SuspiciousDocument"] status = response['Record']['RecordStatus'] if status == 'match': kyc_application.kyc_status = 'VERIFIED' if phone is not None: message_processor.send_message(to_phone=phone, message='Hooray! Your identity has been successfully verified and Sempo account limits lifted.') if status == 'nomatch' or status == 'missing': # currently only handle 1 datasource (i.e. document) errors = response['Record']['DatasourceResults'][0]['Errors'] if len(response['Record']['DatasourceResults'][0]['Errors']) > 0: record_errors = [error['Code'] for error in errors] if '3100' or '3101' in record_errors: # Blurry or Glare Image, retry, send text kyc_application.kyc_status = 'INCOMPLETE' kyc_application.kyc_actions = ['retry'] if phone is not None: message_processor.send_message(to_phone=phone, message="Unfortunately, we couldn't verify your identity with the documents provided. Please open the Sempo app to retry.") for key in response['Record']['DatasourceResults'][0]['AppendedFields']: if key['FieldName'] == 'AuthenticityReasons': document_errors = key['Data'] if document_errors in authenticity_reasons: if document_errors == 'SuspiciousDocument': # document has been rejected, contact support, send text. kyc_application.kyc_status = 'REJECTED' kyc_application.kyc_actions = ['support'] if phone is not None: message_processor.send_message(to_phone=phone, message="Unfortunately, we couldn't verify your identity with the documents provided. Please contact Sempo customer support in the app.") else: # document has been rejected, try again, send text. kyc_application.kyc_status = 'INCOMPLETE' kyc_application.kyc_actions = ['retry'] if phone is not None: message_processor.send_message(to_phone=phone, message="Unfortunately, we couldn't verify your identity with the documents provided. Please open the Sempo app to retry.") # return verification status, document Authenticity Reasons and OCR appended fields return { 'status': status, 'record_errors': record_errors, 'document_errors': document_errors, }
def slack_controller(payload): # Parse the request payload if payload["type"] == "block_actions": # the user has interacted with the message if "start" in payload['actions'][0]['value']: # Show the user details dialog to the user user_id = payload['actions'][0]['value'].split('-')[1] user = get_user_from_id(user_id, payload) documents = user.kyc_applications[0].uploaded_documents doc_types = [ document.reference + '_' + str(ix) for ix, document in enumerate(documents) ] document_validity_mrkdwn = [{ "label": "{} Document Validity".format(doc_type.title()), "type": "select", "name": "{}_doc_validity".format(doc_type), "options": [{ "label": ":white_check_mark: {} document is valid and non-expired". format(doc_type), "value": "valid" }, { "label": ":x: {} document is not valid or expired".format(doc_type), "value": "{}_doc_non_valid".format(doc_type) }, { "label": ":x: {} document is partial. Important information is covered." .format(doc_type), "value": "{}_doc_partial".format(doc_type) }, { "label": ":x: {} document is damaged.".format(doc_type), "value": "{}_doc_damaged".format(doc_type) }, { "label": ":x: {} document is not in English.".format(doc_type), "value": "{}_doc_non_english".format(doc_type) }, { "label": ":x: {} document or selfie is too blurry".format(doc_type), "value": "{}_doc_blurry".format(doc_type) }] } for doc_type in doc_types] dialog_elements_mrkdwn = [{ "label": "ID Validity", "type": "select", "name": "id_validity", "options": [ { "label": ":white_check_mark: ID is valid, non-expired and photo matches selfie", "value": "valid" }, { "label": ":x: ID document is not valid or expired", "value": "id_non_valid" }, { "label": ":x: ID document is partial. Important information is covered.", "value": "id_partial" }, { "label": ":x: ID document is damaged. ID is unreadable due to physical damage.", "value": "id_damaged" }, { "label": ":x: ID document is not in English.", "value": "id_non_english" }, { "label": ":x: ID document or selfie is too blurry", "value": "id_blurry" }, { "label": ":x: ID photo does not match selfie", "value": "selfie_no_match" }, { "label": ":x: ID document is not present in selfie image", "value": "selfie_id_non_present" }, { "label": ":x: Part of the face in the selfie image is covered by a hand, ID, etc.", "value": "selfie_covered" }, ] }, { "type": "text", "label": "First Name", "name": "first_name", "optional": True }, { "type": "text", "label": "Last Name", "name": "last_name", "optional": True }, { "type": "text", "label": "Date of Birth (DD/MM/YYYY) or (YYYY)", "name": "date_of_birth", "optional": True }, { "type": "text", "label": "Residential Address", "name": "address", "optional": True }] if len(document_validity_mrkdwn) > 0: dialog_elements_mrkdwn.extend(document_validity_mrkdwn) client.dialog_open(trigger_id=payload["trigger_id"], dialog={ "title": "Verify User", "submit_label": "Next", "callback_id": "user_details_form-" + user_id, "state": payload['message']['ts'], "elements": dialog_elements_mrkdwn }) # Update the message to show that we're in the process of verifying a user message_blocks = payload['message']['blocks'] new_message_blocks = message_blocks[:len(message_blocks) - 2] new_message_blocks.append( dict(type='context', elements=[{ "type": 'mrkdwn', "text": ':pencil: *@{}* started verifying...'.format( payload['user']['username']) }])) action_blocks = generate_actions( generate_start_button(user_id=user_id)) new_message_blocks.append(action_blocks) client.chat_update(channel=CHANNEL_ID, ts=payload["message"]["ts"], blocks=new_message_blocks) elif "approve" in payload['actions'][0]['value']: user_id = payload['actions'][0]['value'].split('-')[1] user = get_user_from_id(user_id, payload) user.kyc_applications[0].kyc_status = 'VERIFIED' # Update the message to show we've verified a user message_blocks = payload['message']['blocks'] return approve_user(message_blocks, username=payload['user']['username'], message_ts=payload["message"]["ts"], phone=user.phone) elif "deny" in payload['actions'][0]['value']: user_id = payload['actions'][0]['value'].split('-')[1] user = get_user_from_id(user_id, payload) kyc = user.kyc_applications[0] kyc.kyc_status = 'REJECTED' # Update the message to show we've verified a user message_blocks = payload['message']['blocks'] new_message_blocks = message_blocks[:len(message_blocks) - 1] new_message_blocks.append( dict( type='context', elements=[{ "type": 'mrkdwn', "text": ':x: *@{}* Rejected a verification or deferred to support.' .format(payload['user']['username']) }])) client.chat_update(channel=CHANNEL_ID, ts=payload["message"]["ts"], blocks=new_message_blocks) if user.phone: message_processor.send_message( to_phone=user.phone, message= "Unfortunately, we couldn't verify your identity with the documents provided. Please open the Sempo app to retry or contact our customer support." ) return make_response("", 200) elif payload["type"] == "dialog_submission": # The user has submitted the dialog user_id = payload['callback_id'].split('-')[1] user = get_user_from_id(user_id, payload) submission = payload['submission'] kyc = user.kyc_applications[0] # most recent kyc.first_name = submission.get('first_name', kyc.first_name) kyc.last_name = submission.get('last_name', kyc.last_name) kyc.dob = submission.get('date_of_birth', kyc.dob) kyc.street_address = submission.get('address', kyc.street_address) db.session.flush() # so that the response message updates user details message_blocks = generate_populated_message(user_id=user.id) new_message_blocks = message_blocks[:len(message_blocks) - 1] documents = user.kyc_applications[0].uploaded_documents doc_outcomes = [ str(payload['submission']['{}_doc_validity'.format( document.reference + '_' + str(ix))]) for ix, document in enumerate(documents) ] if payload['submission']['id_validity'] == 'valid' or all( [doc == 'valid' for doc in doc_outcomes]): # Update the message to show that we're in the process of verifying a user new_message_blocks.append( dict(type='context', elements=[{ "type": 'mrkdwn', "text": ':female-detective: Running AML checks...' }])) client.chat_update(channel=CHANNEL_ID, ts=payload["state"], blocks=new_message_blocks) result = run_namescam_aml_check(first_name=kyc.first_name, last_name=kyc.last_name, dob=kyc.dob, country=kyc.country) if result.status_code >= 200: aml_result = json.loads(result.text) try: kyc.namescan_scan_id = aml_result['scan_id'] except KeyError: send_namescan_error_msg(new_message_blocks, payload, result) raise NameScanException( 'Unknown NameScan error: {}'.format(aml_result)) else: send_namescan_error_msg(new_message_blocks, payload, result) raise NameScanException( 'Unknown NameScan error: {}'.format(result)) match_rates = [ x['match_rate'] for x in aml_result.get('persons', []) ] avg_match_rate = 0 if len(match_rates) > 0: avg_match_rate = sum(match_rates) / len(match_rates) if aml_result['number_of_matches'] == 0 or avg_match_rate < 75: # Instant Approval kyc.kyc_status = 'VERIFIED' return approve_user(new_message_blocks, username=payload['user']['name'], message_ts=payload["state"], phone=user.phone) else: # Manual Review Required generate_aml_message(parent_message_ts=payload["state"], aml_result=aml_result, avg_match_rate=avg_match_rate) new_message_blocks.append( dict(type='context', elements=[{ "type": 'mrkdwn', "text": ':pencil: *@{}* Manual review needed.'.format( payload['user']['name']) }])) action_blocks = generate_actions( generate_approve_button(user_id), generate_deny_button(user_id)) new_message_blocks.append(action_blocks) client.chat_update(channel=CHANNEL_ID, ts=payload["state"], blocks=new_message_blocks) return make_response("", 200) else: # Update the slack message to indicate failed ID check. failure_options = { "id_non_valid": ":x: ID document is not valid or expired", "id_partial": ":x: ID document is partial. Important parts (name, DOB, ID number) are covered by fingers, glared, cut off", "id_damaged": ":x: ID document is damaged. ID is unreadable due to physical damage.", "id_non_english": ":x: ID document is not in English.", "id_blurry": ":x: ID document or selfie is too blurry", "selfie_no_match": ":x: ID photo does not match selfie", "selfie_id_non_present": ":x: ID document is not present in selfie image", "selfie_covered": ":x: Part of the face in the selfie image is covered by a hand, ID, anything else" } user = get_user_from_id(user_id, payload) documents = user.kyc_applications[0].uploaded_documents doc_types = [ document.reference + '_' + str(ix) for ix, document in enumerate(documents) ] # adding other failure options [ failure_options.update({ "{}_doc_non_valid".format(doc_type): ":x: {} document is not valid or expired".format(doc_type), "{}_doc_partial".format(doc_type): ":x: {} document is partial. Important information is covered." .format(doc_type), "{}_doc_damaged".format(doc_type): ":x: {} document is damaged.".format(doc_type), "{}_doc_non_english".format(doc_type): ":x: {} document is not in English.".format(doc_type), "{}_doc_blurry".format(doc_type): ":x: {} document or selfie is too blurry".format(doc_type) }) for doc_type in doc_types ] # list of all doc outcomes (keys -> failure_options) doc_outcomes = [ str(payload['submission']['{}_doc_validity'.format( document.reference + '_' + str(ix))]) for ix, document in enumerate(documents) ] doc_outcomes.extend([str(payload['submission']['id_validity'])]) print(failure_options) # standard failure doc_outcomes_mrkdwn = [{ "type": 'mrkdwn', "text": failure_options[payload['submission']['id_validity']] }] def _filterdoctypes(_doc_types): formatted_mrkdwn = [] for doc in doc_types: outcome = payload['submission']['{}_doc_validity'.format( doc)] if outcome != 'valid': formatted_mrkdwn.extend([{ "type": 'mrkdwn', "text": failure_options[outcome] }]) return formatted_mrkdwn # other doc failures doc_outcomes_mrkdwn.extend(_filterdoctypes(doc_types)) new_message_blocks.append( dict(type='context', elements=doc_outcomes_mrkdwn)) kyc.kyc_status = 'INCOMPLETE' kyc.kyc_actions = doc_outcomes if user.phone: message_processor.send_message( to_phone=user.phone, message= "Unfortunately, we had a problem verifying your identity. Please open the Sempo app to retry or contact our customer support." ) db.session.flush() client.chat_update(channel=CHANNEL_ID, ts=payload["state"], blocks=new_message_blocks) return make_response("", 200)
def send_sms(user, message_key): message = i18n_for(user, "user.{}".format(message_key)) message_processor.send_message(user.phone, message)
def send_phone_verification_message(to_phone, one_time_code): if to_phone: reciever_message = 'Your Sempo verification code is: {}'.format( one_time_code) message_processor.send_message(to_phone, reciever_message)
def send_terms_message_if_required(user): if not user.seen_latest_terms: terms_message = i18n_for(user, "general_sms.terms") message_processor.send_message(user.phone, terms_message) user.seen_latest_terms = True