Exemple #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")
    send_message(user.phone, pin_reset_message)
Exemple #2
0
def send_onboarding_message(to_phone, first_name, amount, currency_name, 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'],
            amount if not None else 0,
            currency_name,
            one_time_code,
        )

        send_message(to_phone, receiver_message)
Exemple #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
    )

    send_message(user.phone, intro_message)

    send_terms_message_if_required(user)
Exemple #4
0
def save_device_info(device_info, user):
    add_device = False

    if device_info['uniqueId'] and not DeviceInfo.query.filter_by(
            unique_id=device_info['uniqueId']).first():
        # Add the device if the uniqueId is defined, and isn't already in db
        add_device = True

    if add_device:
        device = DeviceInfo()

        device.unique_id = device_info['uniqueId']
        device.brand = device_info['brand']
        device.model = device_info['model']
        device.width = device_info['width']
        device.height = device_info['height']
        send_message(user.phone, f"Your Sempo account is being used to log in from {device.brand} {device.model}. If you don't recognize this login please contact us immediately")

        device.user = user

        db.session.add(device)

        return device
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:
        send_message(
            to_phone=phone,
            message=
            'Hooray! Your identity has been successfully verified and Sempo account limits lifted.'
        )

    return make_response("", 200)
Exemple #6
0
def test_send_message(test_client, init_database, mock_sms_apis):
    from server.utils.phone import send_message

    send_message("+1401391419", 'bonjour')
    send_message("+961401391419", 'mon')
    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'
    }]
Exemple #7
0
 def send_sms(self, phone, message_key, **kwargs):
     message = i18n_for(self.user, "ussd.sempo.{}".format(message_key), **kwargs)
     send_message(phone, message)
Exemple #8
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)
     send_message(user.phone, message)
 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)
     send_message(self.recipient.phone, message)
Exemple #10
0
def send_sms(user, message_key):
    message = i18n_for(user, "user.{}".format(message_key))
    send_message(user.phone, message)
Exemple #11
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)
        send_message(to_phone, reciever_message)
Exemple #12
0
def send_terms_message_if_required(user):

    if not user.seen_latest_terms:
        terms_message = i18n_for(user, "general_sms.terms")
        send_message(user.phone, terms_message)
        user.seen_latest_terms = True
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:
                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:
                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 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:
            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:
                    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:
                            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:
                            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,
    }
Exemple #15
0
    def post(self):
        # Handle a file upload, or CSV in JSON
        if request.files:
            flask_file = request.files['file']
            stream = codecs.iterdecode(flask_file.stream, 'utf-8')
            data = [line for line in stream
                    ]  # Load file into memory to prevent preemptive closing
            reader = csv.DictReader(data)
        else:
            post_data = request.get_json()
            if not post_data:
                response_object = {'message': 'Please provide a CSV file'}
                return make_response(jsonify(response_object)), 400

            csv_data = post_data.get('csv_data', [])
            f = io.StringIO(csv_data)
            reader = csv.DictReader(f)

        transfers = []
        for line in reader:
            tid = line['Transfer ID']
            transfer = db.session.query(CreditTransfer).filter(
                CreditTransfer.id == tid).first()
            message = ''

            if not transfer:
                message = f'Transfer with ID {tid} not found!'
                transfers.append((tid, None, message))
                continue

            if transfer.transfer_type != TransferTypeEnum.WITHDRAWAL:
                message = f'Not a withdrawal!'
                transfers.append((tid, None, message))
                continue

            got_amount = round(dollars_to_cents(line["UnitAmount"]))
            expected_amount = round(transfer.transfer_amount)
            if got_amount != expected_amount:
                message = f'Transfer Amounts do not match (got {cents_to_dollars(got_amount)}, expected {cents_to_dollars(expected_amount)})!'
                transfers.append((tid, None, message))
                continue

            try:
                if line['Payment Has Been Made'].upper(
                ) == 'TRUE' and line['Bank Payment Date']:
                    transfer.add_approver_and_resolve_as_completed()
                    message = 'Transfer Success'
                    if transfer.sender_user.phone and config.PAYOUT_SMS:
                        message = i18n_for(
                            transfer.sender_user,
                            "general_sms.payout_message",
                            first_name=transfer.sender_user.first_name,
                            amount=transfer.rounded_transfer_amount,
                            token=transfer.token.symbol)
                        send_message(transfer.sender_user.phone, message)
                elif line['Payment Has Been Made'] == 'FALSE':
                    transfer.resolve_as_rejected()
                    message = 'Transfer Rejected'
            except Exception as e:
                message = str(e)

            transfers.append((tid, transfer, message))

        output = io.StringIO()
        writer = csv.writer(output)

        writer.writerow([
            'Transfer ID', 'Vendor Account ID', 'Phone', 'First Name',
            'Last Name', 'Transfer Created', 'Transfer Type',
            'Transfer Amount', 'Transfer Status', 'Message'
        ])
        for tid, t, m in transfers:
            writer.writerow([
                tid, t and t.sender_transfer_account.id, t
                and t.sender_transfer_account.primary_user.phone, t
                and t.sender_transfer_account.primary_user.first_name, t
                and t.sender_transfer_account.primary_user.last_name, t
                and t.created, t and t.transfer_type.value, t
                and cents_to_dollars(t.transfer_amount), t
                and t.transfer_status.value, m
            ])
        bytes_output = io.BytesIO()
        bytes_output.write(output.getvalue().encode('utf-8'))
        bytes_output.seek(0)
        return send_file(bytes_output,
                         as_attachment=True,
                         attachment_filename='vendor_payout.csv',
                         mimetype='text/csv')