How to implement the Plaid identity verification product in Laravel?

108 Views Asked by At

How to implement the Plaid identity verification product in Laravel? I am confused about implementing this product. I've tried the github.com/TomorrowIdeas/plaid-sdk-php package and successfully implemented the Investments product in Laravel. I want to implement an identity verification product in Laravel using a template from Plaid. Can someone explain how to implement the Plaid identity verification product in Laravel?

2

There are 2 best solutions below

0
On BEST ANSWER

This issue has been solved using the Plaid SDK PHP and jQuery. Below is an example of the codes.

plaid.js

$(document).ready(function(){
    window.isVerificationLinkClicked = false;
    window.isInitialVerificationChecked = false;
    if($('.identity-verification').length) {
        getVerificationLink();
        checkVerificationStatus();
    }

    $('.identity-verification').on('click', function (event) {
        event.preventDefault();
        window.open($(this).data('verificationLink'), '_blank');
        window.isVerificationLinkClicked = true;
        checkVerificationStatus();
    })

    function createLinkToken() {
        $.ajax({
            headers: {
                "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
            },
            url: "/createLinkToken",
            type: "GET",
            dataType: "json",
            success: function (response) {
                const data = JSON.parse(response.data);
                console.log('Link Token: ' + data.link_token);
                linkPlaidAccount(data.link_token);
            },
            error: function (err) {
                console.log('Error creating link token.');
                const errMsg = JSON.parse(err);
                alert(err.error_message);
                console.error("Error creating link token: ", err);
            }
        });
    }

    function linkPlaidAccount(linkToken) {
        var linkHandler = Plaid.create({
            token: linkToken,
            onSuccess: function (public_token, metadata) {
                var body = {
                    public_token: public_token,
                    accounts: metadata.accounts,
                    institution: metadata.institution,
                    link_session_id: metadata.link_session_id,
                    link_token: linkToken
                };
                $.ajax({
                    headers: {
                        "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
                    },
                    url: "/storePlaidAccount",
                    type: "POST",
                    data: body,
                    dataType: "json",
                    success: function (data) {
                        getInvestmentHoldings(data.item_id);
                    },
                    error: function (err) {
                        console.log('Error linking Plaid account.');
                        const errMsg = JSON.parse(err);
                        console.error("Error linking Plaid account: ", err);
                    }
                });
            },
            onExit: function (err, metadata) {
                console.log("linkBankAccount error=", err, metadata);
                const errMsg = JSON.parse(err);
                        console.error("Error linking Plaid account: ", err);

                linkHandler.destroy();
                if (metadata.link_session_id == null && metadata.status == "requires_credentials") {
                    createLinkToken();
                }
            }
        });
        linkHandler.open();
    }

    function checkVerificationStatus() {
        if(window.isInitialVerificationChecked == true && isVerificationLinkClicked == false) {
            return true;
        }
        window.isInitialVerificationChecked = true;

        $.ajax({
            url: '/check-identity-verification-status',
            type: 'GET',
            data: {
                _token: $("meta[name='csrf-token']").attr("content"),
            },
            success: function(response) {
                if (response.status == false) {
                    setTimeout(function() {
                        checkVerificationStatus();
                    }, 1000);
                } else {
                    updateContactInformation();
                    $('.identity-verification').fadeOut(500, function () {
                        $('.identity-verification').remove();
                        window.isVerificationLinkClicked = false;
                    });
                }
            },
            error: function(data) {
                console.log(data);
            }
        });
    }

    function getVerificationLink() {
        $.ajax({
            url: '/generate-verification-link',
            type: 'GET',
            data: {
                _token: $("meta[name='csrf-token']").attr("content"),
            },
            success: function(response) {
                $('.identity-verification').data('verification-link', response.verificationLink);
            },
            error: function(data) {
                console.log(data);
            }
        });
    }

    function updateContactInformation() {
        $.ajax({
            url: '/update-contact-information',
            type: 'GET',
            data: {
                _token: $("meta[name='csrf-token']").attr("content"),
            },
            success: function(response) {
                if(response.status == true) {
                    Swal.fire({
                        title: 'Information updated!',
                        text: "We have updated your contact information. ",
                        icon: 'info',
                        showCancelButton: false,
                        confirmButtonText: 'View',
                        allowEscapeKey: false,
                        allowOutsideClick: false,
                    }).then(function(result) {
                        if (result.isConfirmed) {
                            window.location.href = '/account-settings/personal-info';
                        }
                    });
                }
            },
            error: function(data) {
                console.log(data);
            }
        });
    }

});

PlaidController.php

<?php

namespace App\Http\Controllers;

use App\Interfaces\PlaidRepositoryInterface;
use App\Models\PlaidAccount;
use App\Models\PlaidIdentityVerification;
use Illuminate\Http\Request;
use TomorrowIdeas\Plaid\Plaid;
use Illuminate\Support\Facades\Log;
use TomorrowIdeas\Plaid\Entities\User;
use TomorrowIdeas\Plaid\PlaidRequestException;

class PlaidController extends Controller
{
    private PlaidRepositoryInterface $plaidRepository;

    public function __construct(PlaidRepositoryInterface $plaidRepository)
    {
        $this->plaidRepository = $plaidRepository;
    }

    public function createLinkToken()
    {
        $user_id = 1;
        $plaidUser = new User($user_id);
        $plaid = new Plaid(env('PLAID_CLIENT_ID'), env('PLAID_SECRET'), env('PLAID_ENV'));
        $response = $plaid->tokens->create('Plaid Test', 'en', ['US'], $plaidUser, ['investments'], env('PLAID_WEBHOOK'));

        return response()->json([
            'result' => 'success',
            'data' => json_encode($response)
        ], 200);
    }

    public function storePlaidAccount(Request $request)
    {
        $validator = \Validator::make($request->all(), [
            'public_token' => ['required', 'string']
        ]);

        if ($validator->fails()) {
            return response()->json(['result' => 'error', 'message' => $validator->errors()], 201);
        }

        $user_id = 1;
        Log::info('-----------------------------------------');
        Log::info('Plaid public_token : ' . $request->public_token . ', link_token: ' . $request->link_token);
        $plaid = new Plaid(env('PLAID_CLIENT_ID'), env('PLAID_SECRET'), env('PLAID_ENV'));
        $obj = $plaid->items->exchangeToken($request->public_token);
        Log::info('Plaid exchange token : ' . json_encode($obj));

        try {
            \DB::transaction(function () use($request, $obj, $user_id) {
                foreach($request->accounts as $account) {
                    $query = PlaidAccount::where('account_id', isset($account['id']) ? $account['id'] : $account['account_id']);
                    if ($query->count() > 0) {
                        Log::info('[Update Plaid Account]: ' . json_encode($account));
                        $new_account = $query->first();
                        $new_account->plaid_item_id = $obj->item_id;
                        $new_account->plaid_access_token = $obj->access_token;
                        $new_account->plaid_public_token = $request->public_token;
                        $new_account->link_session_id = $request->link_session_id;
                        $new_account->link_token = $request->link_token;
                        $new_account->institution_id = $request->institution['institution_id'];
                        $new_account->institution_name = $request->institution['name'];
                        $new_account->account_id = isset($account['id']) ? $account['id'] : $account['account_id'];
                        $new_account->account_name = isset($account['name']) ? $account['name'] : $account['account_name'];
                        $new_account->account_mask = isset($account['account_number']) ? $account['account_number'] : $account['mask'];
                        $new_account->account_mask = null;
                        $new_account->account_type = isset($account['type']) ? $account['type'] : $account['account_type'];
                        $new_account->account_subtype = isset($account['subtype']) ? $account['subtype'] : $account['account_sub_type'];
                        $new_account->user_id = $user_id;
                        $new_account->save();
                    } else {
                        Log::info('[New Plaid Account]: ' . json_encode($account));
                        $new_account = ([
                            'plaid_item_id' => $obj->item_id,
                            'plaid_access_token' => $obj->access_token,
                            'plaid_public_token' => $request->public_token,
                            'link_session_id' => $request->link_session_id,
                            'link_token' => $request->link_token,
                            'institution_id'    => $request->institution['institution_id'],
                            'institution_name' => $request->institution['name'],
                            'account_id' => isset($account['id']) ? $account['id'] : $account['account_id'],
                            'account_name' => isset($account['name']) ? $account['name'] : $account['account_name'],
                            'account_mask' => isset($account['account_number']) ? $account['account_number'] : $account['mask'],
                            'account_mask' => null,
                            'account_type' => isset($account['type']) ? $account['type'] : $account['account_type'],
                            'account_subtype' => isset($account['subtype']) ? $account['subtype'] : $account['account_sub_type'],
                            'user_id' => $user_id
                        ]);
                        PlaidAccount::create($new_account);
                    }
                }
            });
        } catch (\Exception $e) {
            Log::error('An error occurred linking a Plaid account: ' . $e->getMessage());
            return response()->json([
                'message' => 'An error occurred attempting to link a Plaid account.'
            ], 200);
        }
        return response()->json([
            'message' => 'Successfully linked plaid account.',
            'item_id' => $obj->item_id,
        ], 200);
    }

    public function checkIdentityVerificationStatus() {
        $status = $this->plaidRepository->checkVerificationStatus();
        return response()->json([
            'status' => $status
        ]);
    }

    public function generateVerificationLink()
    {
        return response()->json([
            'verificationLink' => $this->plaidRepository->generateVerificationLink(),
        ]);
    }

    public function updateContactInformation() {
        $status = $this->plaidRepository->updateContactInformation();

        return response()->json([
            'status' => $status,
        ]);
    }
}

PlaidRepositoryInterface.php

<?php


namespace App\Interfaces;


interface PlaidRepositoryInterface
{
    /**
     * To generate the Verification link
     * @return String
     */
    public function generateVerificationLink();

    /**
     * To check if the user has completed the verification or not
     * @return boolean
     */
    public function checkVerificationStatus();

    /**
     * To update the verification status
     * @param String $identityVerificationId
     * @return mixed
     */
    public function updateVerificationStatus($identityVerificationId);

    /**
     * To update the contact information if the information does not match with the IDV.
     * @return boolean
     */
    public function updateContactInformation();

    /**
     * To save the uploaded documents in our system
     * @param $response
     * @return void
     */
    public function saveDocumentFiles($response);

    /**
     * To restart the verification process from the starting
     * @return mixed
     */
    public function retryVerificationProcess();
}

PlaidRepository.php

<?php


namespace App\Repositories;


use App\Interfaces\PlaidRepositoryInterface;
use App\Models\Country;
use App\Models\PlaidIdentityVerification;
use App\Models\State;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

class PlaidRepository implements PlaidRepositoryInterface
{
    private function getEnvironmentURL()
    {
        $plaidEnvironments = [
            "production" => "https://production.plaid.com/",
            "development" => "https://development.plaid.com/",
            "sandbox" => "https://sandbox.plaid.com/",
        ];

        return $plaidEnvironments[env('PLAID_ENV') ?? 'production'];
    }

    public function generateVerificationLink()
    {

        if ($this->checkVerificationStatus() == true) {
            return true;
        }

        $user = Auth::user();

        $plaidIdentityVerification = PlaidIdentityVerification::where('user_id', Auth::user()->id)->first();

        if (is_null($plaidIdentityVerification) == false) {
            if ($plaidIdentityVerification->status == 'failed') {
                return $this->retryVerificationProcess();
            }
            return $plaidIdentityVerification->verification_link;
        }


        $curl = curl_init();

        $payload = [
            "client_id" => env('PLAID_CLIENT_ID'),
            "secret" => env('PLAID_SECRET'),
            "template_id" => env('PLAID_IDENTITY_VERIFICATION_TEMPLATE_ID'),
            "gave_consent" => false,
            "is_shareable" => true,
            "is_idempotent" => true,
            "user" => [
                "client_user_id" => 'user-' . $user->id,
                "email_address" => $user->email,
            ]
        ];

        curl_setopt_array($curl, array(
            CURLOPT_URL => $this->getEnvironmentURL() . 'identity_verification/create',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_HTTPHEADER => array(
                'Content-Type: application/json'
            ),
        ));

        $response = json_decode(curl_exec($curl), true);

        curl_close($curl);

        $plaidIdentityVerification = new PlaidIdentityVerification();
        $plaidIdentityVerification->user_id = $user->id;
        $plaidIdentityVerification->identity_verification_id = $response['id'];
        $plaidIdentityVerification->verification_link = $response['shareable_url'];
        $plaidIdentityVerification->save();

        return $response['shareable_url'];
    }

    public function checkVerificationStatus()
    {
        $plaidIdentityVerification = PlaidIdentityVerification::where('user_id', Auth::user()->id)->first();

        if (is_null($plaidIdentityVerification) == false) {
            $this->updateVerificationStatus($plaidIdentityVerification->identity_verification_id);
            if ($plaidIdentityVerification->status == 'success') {
                return true;
            }
        }

        return false;
    }

    public function updateVerificationStatus($identityVerificationId)
    {
        $payload = [
            "client_id" => env('PLAID_CLIENT_ID'),
            "secret" => env('PLAID_SECRET'),
            "identity_verification_id" => $identityVerificationId,
        ];

        $curl = curl_init();

        curl_setopt_array($curl, array(
            CURLOPT_URL => $this->getEnvironmentURL() . 'identity_verification/get',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_HTTPHEADER => array(
                'Content-Type: application/json'
            ),
        ));

        $response = json_decode(curl_exec($curl), true);

        if ($response['documentary_verification'] !== null) {
            $plaidIdentityVerification = PlaidIdentityVerification::where('user_id', Auth::user()->id)->first();
            $plaidIdentityVerification->status = $response['documentary_verification']['status'];

            if (isset($response['documentary_verification']['status']) && $response['documentary_verification']['status'] == 'success') {
                Auth::user()->removeRole('User');
                Auth::user()->assignRole('Host');
            }

            $plaidIdentityVerification->response = json_encode($response);

            $lastKey = array_keys($response['documentary_verification']['documents']);
            $lastKey = array_values(array_reverse($lastKey))[0];

            $plaidIdentityVerification->document_type = ucfirst(str_replace('_', ' ', $response['documentary_verification']['documents'][$lastKey]['extracted_data']['category']));
            $plaidIdentityVerification->save();
        }

        curl_close($curl);
    }

    /**
     * To update the contact information if the information does not match with the IDV.
     *
     * @return bool
     */
    public function updateContactInformation()
    {
        $user = Auth::user();
        $userProfile = $user->profile;

        $plaidIdentificationVerification = $user->plaidIdentityVerification;
        $plaidIdentificationVerificationResponse = json_decode($plaidIdentificationVerification->response, true);

        $isInformationUpdated = false;
        if (!empty($plaidIdentificationVerificationResponse)) {
            if (is_null($userProfile->country) || $userProfile->country->iso2 != $plaidIdentificationVerificationResponse['user']['address']['country']) {
                $country = Country::where('iso2', $plaidIdentificationVerificationResponse['user']['address']['country'])->first();
                $user->profile->update([
                    'country_id' => $country->id,
                ]);
                $isInformationUpdated = true;
            }

            if (is_null($userProfile->state) || $userProfile->state->iso2 != $plaidIdentificationVerificationResponse['user']['address']['region']) {
                $state = State::where('iso2', $plaidIdentificationVerificationResponse['user']['address']['region'])->first();
                $user->profile->update([
                    'state_id' => $state->id,
                ]);
                $isInformationUpdated = true;
            };

            if ($userProfile->city != $plaidIdentificationVerificationResponse['user']['address']['city']) {
                $user->profile->update([
                    'city' => $plaidIdentificationVerificationResponse['user']['address']['city'],
                ]);
                $isInformationUpdated = true;
            };

            if ($userProfile->address1 != $plaidIdentificationVerificationResponse['user']['address']['street']) {
                $user->profile->update([
                    'address1' => $plaidIdentificationVerificationResponse['user']['address']['street'],
                ]);
                $isInformationUpdated = true;
            };

            if ($userProfile->address2 != $plaidIdentificationVerificationResponse['user']['address']['street2']) {
                $user->profile->update([
                    'address2' => $plaidIdentificationVerificationResponse['user']['address']['street2'],
                ]);
                $isInformationUpdated = true;
            };

            if ($userProfile->zip_code != $plaidIdentificationVerificationResponse['user']['address']['postal_code']) {
                $user->profile->update([
                    'zip_code' => $plaidIdentificationVerificationResponse['user']['address']['postal_code'],
                ]);
                $isInformationUpdated = true;
            };

            if ($userProfile->dob != $plaidIdentificationVerificationResponse['user']['date_of_birth']) {
                $user->profile->update([
                    'dob' => $plaidIdentificationVerificationResponse['user']['date_of_birth'],
                ]);
                $isInformationUpdated = true;
            };

            if ($user->first_name != $plaidIdentificationVerificationResponse['user']['name']['given_name']) {
                $user->first_name = $plaidIdentificationVerificationResponse['user']['name']['given_name'];
                $isInformationUpdated = true;
            };

            if ($user->last_name != $plaidIdentificationVerificationResponse['user']['name']['family_name']) {
                $user->last_name = $plaidIdentificationVerificationResponse['user']['name']['family_name'];
                $isInformationUpdated = true;
            };

            if ($user->phone != $plaidIdentificationVerificationResponse['user']['phone_number']) {
                $user->phone = $plaidIdentificationVerificationResponse['user']['phone_number'];
                $user->phone_verified_at = date('Y-m-d H:i:s');
                $isInformationUpdated = true;
            };

            $user->save();

        }

        $this->saveDocumentFiles($plaidIdentificationVerificationResponse);

        return $isInformationUpdated;
    }

    public function saveDocumentFiles($response)
    {
        $user = Auth::user();
        $path = Storage::disk('public')->path('');

        if (file_exists($path . '/idv/') == false) {
            mkdir($path . '/idv/', 0755, true);
        }

        if (file_exists($path . '/idv/' . $user->id) == false) {
            mkdir($path . '/idv/' . $user->id, 0775, true);
        }


        if (isset($response['documentary_verification']['documents']) && !empty($response['documentary_verification']['documents'])) {
            $lastKey = array_keys($response['documentary_verification']['documents']);
            $lastKey = array_values(array_reverse($lastKey))[0];

            if (file_exists($path . '/idv/' . $user->id . '/photo.jpg') == false) {
                Storage::disk('public')->put('/idv/' . $user->id . '/photo.jpg', file_get_contents($response['documentary_verification']['documents'][$lastKey]['images']['face']));
            }
            if (file_exists($path . '/idv/' . $user->id . '/front_photo.jpg') == false) {
                Storage::disk('public')->put('/idv/' . $user->id . '/front_photo.jpg', file_get_contents($response['documentary_verification']['documents'][$lastKey]['images']['original_front']));
            }
            if (file_exists($path . '/idv/' . $user->id . '/back_photo.jpg') == false) {
                Storage::disk('public')->put('/idv/' . $user->id . '/back_photo.jpg', file_get_contents($response['documentary_verification']['documents'][$lastKey]['images']['original_back']));
            }
        }
    }

    public function retryVerificationProcess()
    {
        $user = Auth::user();

        $curl = curl_init();

        $payload = [
            "client_id" => env('PLAID_CLIENT_ID'),
            "secret" => env('PLAID_SECRET'),
            "template_id" => env('PLAID_IDENTITY_VERIFICATION_TEMPLATE_ID'),
            "strategy" => "reset",
            "client_user_id" => 'user-' . $user->id,
        ];

        curl_setopt_array($curl, array(
            CURLOPT_URL => $this->getEnvironmentURL() . 'identity_verification/retry',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_HTTPHEADER => array(
                'Content-Type: application/json'
            ),
        ));

        $response = json_decode(curl_exec($curl), true);

        curl_close($curl);

        $user->plaidIdentityVerification()->update([
            'identity_verification_id' => $response['id'],
            'verification_link' => $response['shareable_url'],
        ]);

        return $response['shareable_url'];
    }
}
4
On

The PHP library for Plaid is a third party, unofficial client library, and as indicated in its README, it does not have support for Identity Verification. You have several options for using Plaid Identity Verification with PHP:

  1. Do not use a Plaid client library and instead make direct POST calls to the Plaid API using the HTTP library of your choice.

  2. Submit a PR to the PHP client library to add support for the Identity Verification endpoints you will need to use in your integration.

  3. Generate your own PHP client library using Plaid's OpenAPI specification and a tool such as OpenAPI Generator.