예제 #1
0
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)
예제 #2
0
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)
예제 #3
0
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)
예제 #4
0
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)
예제 #5
0
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)
예제 #7
0
 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)
예제 #9
0
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,
    }
예제 #10
0
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)
예제 #11
0
def send_sms(user, message_key):
    message = i18n_for(user, "user.{}".format(message_key))
    message_processor.send_message(user.phone, message)
예제 #12
0
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)
예제 #13
0
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