Group
Extension

WebService-Async-Onfido/lib/WebService/Async/Onfido.pm

package WebService::Async::Onfido;

# ABSTRACT: Webservice to connect to Onfido API

use strict;
use warnings;

our $VERSION = '0.007';

use parent qw(IO::Async::Notifier);

=head1 NAME

WebService::Async::Onfido - unofficial support for the Onfido identity verification service

=head1 SYNOPSIS


=head1 DESCRIPTION

=cut

use mro;
no indirect;

use Syntax::Keyword::Try;
use Dir::Self;
use URI;
use URI::QueryParam;
use URI::Template;
use Ryu::Async;

use Future;
use Future::Utils qw(repeat);
use File::Basename;
use Path::Tiny;
use Net::Async::HTTP;
use HTTP::Request::Common;
use JSON::MaybeUTF8 qw(:v1);
use JSON::MaybeXS;
use File::ShareDir;
use URI::Escape  qw(uri_escape_utf8);
use Scalar::Util qw(blessed);

use WebService::Async::Onfido::Applicant;
use WebService::Async::Onfido::Address;
use WebService::Async::Onfido::Document;
use WebService::Async::Onfido::Photo;
use WebService::Async::Onfido::Video;
use WebService::Async::Onfido::Check;
use WebService::Async::Onfido::Report;

use Log::Any qw($log);
use constant SUPPORTED_COUNTRIES_URL => 'https://documentation.onfido.com/identityISOsupported.json';

# Mapping file extension to mime type for currently
# supported document types
my %FILE_MIME_TYPE_MAPPING = (
    jpg  => 'image/jpeg',
    jpeg => 'image/jpeg',
    png  => 'image/png',
    pdf  => 'application/pdf',
);

sub configure {
    my ($self, %args) = @_;
    for my $k (qw(token requests_per_minute base_uri on_api_hit on_rate_limit rate_limit_delay)) {
        $self->{$k} = delete $args{$k} if exists $args{$k};
    }

    $self->{rate_limit_delay} //= 60;
    return $self->next::method(%args);
}

=head2 hook

Executes a hook, if specified at configure time.

Takes the following:

=over 4

=item * C<$hook> - the hook to execute

=item * C<$data> - data to pass to the sub

=back

It returns C<undef>

=cut

sub hook {
    my ($self, $hook, $data) = @_;

    return undef unless $self->{$hook};

    return undef unless ref($self->{$hook}) eq 'CODE';

    $self->{$hook}->($data);

    return undef;
}

=head2 applicant_list

Retrieves a list of all known applicants.

Returns a L<Ryu::Source> which will emit one L<WebService::Async::Onfido::Applicant> for
each applicant found.

=cut

sub applicant_list {
    my ($self) = @_;
    my $src    = $self->source;
    my $f      = $src->completed;
    my $uri    = $self->endpoint('applicants');
    (
        repeat {
            $log->tracef('GET %s', "$uri");
            $self->rate_limiting->then(
                sub {
                    $self->hook('on_api_hit', {GET => $uri});
                    $self->ua->GET($uri, $self->auth_headers,);
                }
            )->then(
                sub {
                    try {
                        my ($res) = @_;
                        my $data = decode_json_utf8($res->content);
                        $log->tracef('Have response %s', $data);
                        my ($total) = $res->header('X-Total-Count');
                        $log->tracef('Expected total count %d', $total);
                        for (@{$data->{applicants}}) {
                            return $f if $f->is_ready;
                            $src->emit(WebService::Async::Onfido::Applicant->new(%$_, onfido => $self));
                        }
                        $log->tracef('Links are %s', [$res->header('Link')]);
                        my %links = $self->extract_links($res->header('Link'));
                        if (my $next = $links{next}) {
                            ($uri) = $next;
                        } else {
                            $src->finish;
                        }
                        return Future->done;
                    } catch {
                        my ($err) = $@;
                        $log->errorf('Failed - %s', $err);
                        return Future->fail($err);
                    }
                },
                sub {
                    my ($err, @details) = @_;
                    $log->errorf('Failed to request document_list: %s', $err);
                    $src->fail($err, @details) unless $src->is_ready;
                    Future->fail($err, @details);
                })
        }
        until => sub { $f->is_ready })->retain;
    return $src;
}

=head2 paging

Supports paging through HTTP GET requests.

=over 4

=item * C<$starting_uri> - the initial L<URI> to request

=item * C<$factory> - a C<sub> that we will call with a L<Ryu::Source> and expect to return
a second response-processing C<sub>.

=back

Returns a L<Ryu::Source>.

=cut

sub paging {
    my ($self, $starting_uri, $factory) = @_;
    my $uri =
        ref($starting_uri)
        ? $starting_uri->clone
        : URI->new($starting_uri);

    my $src  = $self->source;
    my $f    = $src->completed;
    my $code = $factory->($src);
    (
        repeat {
            $log->tracef('GET %s', "$uri");
            $self->rate_limiting->then(
                sub {
                    $self->hook('on_api_hit', {GET => $uri});
                    $self->ua->GET($uri, $self->auth_headers,);
                }
            )->then(
                sub {
                    try {
                        my ($res) = @_;
                        my $data = decode_json_utf8($res->content);
                        $log->tracef('Have response %s', $data);
                        my ($total) = $res->header('X-Total-Count');
                        $log->tracef('Expected total count %d', $total);
                        $code->($data);
                        $log->tracef('Links are %s', [$res->header('Link')]);
                        my %links = $self->extract_links($res->header('Link'));

                        if (my $next = $links{next}) {
                            ($uri) = $next;
                        } else {
                            $src->finish;
                        }
                        return Future->done;
                    } catch {
                        my ($err) = $@;
                        $log->errorf('Failed - %s', $err);
                        return Future->fail($err);
                    }
                },
                sub {
                    my ($err, @details) = @_;
                    $log->errorf('Failed to request %s: %s', $uri, $err);
                    $src->fail($err, @details) unless $src->is_ready;
                    Future->fail($err, @details);
                })
        }
        until => sub { $f->is_ready })->retain;
    return $src;
}

=head2 extract_links

Given a set of strings representing the C<Link> headers in an HTTP response,
extracts the URIs based on the C<rel> attribute as described in
L<RFC5988|http://tools.ietf.org/html/rfc5988>.

Returns a list of key, value pairs where the key contains the lowercase C<rel> value
and the value is a L<URI> instance.

 my %links = $self->extract_links($res->header('Link'))
 print "Last page would be $links{last}"

=cut

sub extract_links {
    my ($self, @links) = @_;
    my %links;
    for (map { split /\h*,\h*/ } @links) {

        # Format is like:
        # <https://api.eu.onfido.com/v3.4/applicants?page=2>; rel="next"
        if (my ($url, $rel) = m{<(http[^>]+)>;\h*rel="([^"]+)"}) {
            $links{lc $rel} = URI->new($url);
        }
    }
    return %links;
}

=head2 applicant_create

Creates a new applicant record.

See accessors in L<WebService::Async::Onfido::Applicant> for a full list of supported attributes.
These can be passed as named parameters to this method.

Returns a L<Future> which resolves to a L<WebService::Async::Onfido::Applicant>
instance on successful completion.

=cut

sub applicant_create {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('applicants');
            $self->hook(
                'on_api_hit',
                {
                    POST => $uri,
                    body => \%args,
                });
            $self->ua->POST(
                $uri,
                encode_json_utf8(\%args),
                content_type => 'application/json',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Applicant->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Applicant creation failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 applicant_update

Updates a single applicant.

Returns a L<Future> which resolves to empty on success.

=cut

sub applicant_update {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('applicant', %args);
            $self->hook(
                'on_api_hit',
                {
                    PUT  => $uri,
                    body => \%args,
                });
            $self->ua->PUT(
                $uri,
                encode_json_utf8(\%args),
                content_type => 'application/json',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done();
            } catch {
                my ($err) = $@;
                $log->errorf('Applicant update failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 applicant_delete

Deletes a single applicant.

Returns a L<Future> which resolves to empty on success.

=cut

sub applicant_delete {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('applicant', %args);
            $self->hook(
                'on_api_hit',
                {
                    DELETE => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'DELETE',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                return Future->done if $res->code == 204;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->fail($data);
            } catch {
                my ($err) = $@;
                $log->errorf('Applicant delete failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 applicant_get

Retrieve a single applicant.

Returns a L<Future> which resolves to a L<WebService::Async::Onfido::Applicant>

=cut

sub applicant_get {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('applicant', %args);
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'GET',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Applicant->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

sub check_get {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('check', %args);
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'GET',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Check->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 document_list

List all documents for a given L<WebService::Async::Onfido::Applicant>.

Takes the following named parameters:

=over 4

=item * C<applicant_id> - the L<WebService::Async::Onfido::Applicant/id> for the applicant to query

=back

Returns a L<Ryu::Source> which will emit one L<WebService::Async::Onfido::Document> for
each document found.

=cut

sub document_list {
    my ($self, %args) = @_;
    my $src = $self->source;
    my $uri = $self->endpoint('documents');
    $uri->query('applicant_id=' . uri_escape_utf8($args{applicant_id}));

    $self->rate_limiting->then(
        sub {
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->GET($uri, $self->auth_headers,);
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                $log->tracef("GET %s => %s", $uri, $res->decoded_content);
                my $data = decode_json_utf8($res->content);
                my $f    = $src->completed;
                $log->tracef('Have response %s', $data);
                for (@{$data->{documents}}) {
                    return $f if $f->is_ready;
                    $src->emit(WebService::Async::Onfido::Document->new(%$_, onfido => $self));
                }
                $src->finish;
                return Future->done;
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                $src->fail('Failed to get document list.')
                    unless $src->is_ready;
                return Future->fail($err);
            }
        })->retain;
    return $src;
}

=head2 get_document_details

Gets a document object for a given L<WebService::Async::Onfido::Applicant>.

Takes the following named parameters:

=over 4

=item * C<applicant_id> - the L<WebService::Async::Onfido::Applicant/id> for the applicant to query

=item * C<document_id> - the L<WebService::Async::Onfido::Document/id> for the document to query

=back

Returns a Future object which consists of a L<WebService::Async::Onfido::Document>

=cut

sub get_document_details {
    my ($self, %args) = @_;
    my $uri = $self->endpoint('document', %args);
    return $self->rate_limiting->then(
        sub {
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->GET($uri, $self->auth_headers,);
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                $log->tracef("GET %s => %s", $uri, $res->decoded_content);
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Document->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 photo_list

List all photos for a given L<WebService::Async::Onfido::Applicant>.

Takes the following named parameters:

=over 4

=item * C<applicant_id> - the L<WebService::Async::Onfido::Applicant/id> for the applicant to query

=back

Returns a L<Ryu::Source> which will emit one L<WebService::Async::Onfido::Photo> for
each photo found.

=cut

sub photo_list {
    my ($self, %args) = @_;
    my $src = $self->source;
    my $uri = $self->endpoint('photos', %args);
    $self->rate_limiting->then(
        sub {
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->GET($uri, $self->auth_headers,);
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                $log->tracef("GET %s => %s", $uri, $res->decoded_content);
                my $data = decode_json_utf8($res->content);
                my $f    = $src->completed;
                $log->tracef('Have response %s', $data);
                for (@{$data->{live_photos}}) {
                    return $f if $f->is_ready;
                    $src->emit(WebService::Async::Onfido::Photo->new(%$_, onfido => $self));
                }
                $src->finish;
                return Future->done;
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                $src->fail('Failed to get photo list.') unless $src->is_ready;
                return Future->fail($err);
            }
        })->retain;
    return $src;
}

=head2 get_photo_details

Gets a live_photo object for a given L<WebService::Async::Onfido::Applicant>.

Takes the following named parameters:

=over 4

=item * C<live_photo_id> - the L<WebService::Async::Onfido::Photo/id> for the document to query

=back

Returns a Future object which consists of a L<WebService::Async::Onfido::Photo>

=cut

sub get_photo_details {
    my ($self, %args) = @_;
    my $uri = $self->endpoint('photo', %args);
    return $self->rate_limiting->then(
        sub {
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->GET($uri, $self->auth_headers,);
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                $log->tracef("GET %s => %s", $uri, $res->decoded_content);
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Photo->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 document_upload

Uploads a single document for a given applicant.

Takes the following named parameters:

=over 4

=item * C<type> - can be C<passport>, C<photo>, C<poa>

=item * C<side> - which side, either C<front> or C<back>

=item * C<issuing_country> - which country this document is for

=item * C<filename> - the file name to use for this item

=item * C<data> - the bytes for this image file (must be in JPEG format)

=back

=cut

sub document_upload {
    my ($self, %args) = @_;
    my $uri = $self->endpoint('documents');

    my $req = HTTP::Request::Common::POST(
        $uri,
        content_type => 'form-data',
        content      => [
            %args{grep { exists $args{$_} } qw(type side issuing_country applicant_id)},
            file => [
                undef, $args{filename},
                'Content-Type' => _get_mime_type($args{filename}),
                Content        => $args{data}
            ],
        ],
        %{$self->auth_headers},
    );
    return $self->rate_limiting->then(
        sub {
            delete $args{data};
            $self->hook(
                'on_api_hit',
                {
                    POST => $uri,
                    body => \%args
                });
            $self->ua->do_request(
                request => $req,
            );
        }
    )->catch(
        http => sub {
            my ($message, undef, $response, $request) = @_;
            $log->errorf('Request %s received %s with full response as %s', $request->uri, $message, $response->content,);

            # Just pass it on
            Future->fail(
                $message,
                http => $response,
                $request
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Document->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 live_photo_upload

Uploads a single "live photo" for a given applicant.

Takes the following named parameters:

=over 4

=item * C<applicant_id> - ID for the person this photo relates to

=item * C<advanced_validation> - perform additional validation (ensure we only have a single face)

=item * C<filename> - the file name to use for this item

=item * C<data> - the bytes for this image file (must be in JPEG format)

=back

=cut

sub live_photo_upload {
    my ($self, %args) = @_;
    my $uri = $self->endpoint('photo_upload');
    $args{advanced_validation} = $args{advanced_validation} ? 'true' : 'false';
    my $req = HTTP::Request::Common::POST(
        $uri,
        content_type => 'form-data',
        content      => [
            %args{grep { exists $args{$_} } qw(advanced_validation applicant_id)},
            file => [
                undef, $args{filename},
                'Content-Type' => _get_mime_type($args{filename}),
                Content        => $args{data}
            ],
        ],
        %{$self->auth_headers},
    );
    $log->tracef('Photo upload: %s', $req->as_string("\n"));
    return $self->rate_limiting->then(
        sub {
            delete $args{data};
            $self->hook(
                'on_api_hit',
                {
                    POST => $uri,
                    body => \%args
                });
            $self->ua->do_request(
                request => $req,
            );
        }
    )->catch(
        http => sub {
            my ($message, undef, $response, $request) = @_;
            $log->errorf('Request %s received %s with full response as %s', $request->uri, $message, $response->content,);

            # Just pass it on
            Future->fail(
                $message,
                http => $response,
                $request
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Photo->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 applicant_check

Perform an identity check on an applicant.

This is the main method for dealing with verification - once you have created
the applicant and uploaded some documents, call this to start the process of
checking the documents and details, and generating the reports.

L<https://documentation.onfido.com/#check-object>

Takes the following named parameters:

=over 4

=item * C<applicant_id> - the applicant requesting the check

=item * C<document_ids> - arrayref of documents ids to be analyzed on this check

=item * C<report_names> - arrayref of the reports to be made (e.g: document, facial_similarity_photo)

=item * C<tags> - custom tags to apply to these reports

=item * C<suppress_form_emails> - if true, do B<not> send out the email to
the applicant

=item * C<asynchronous> - return immediately and perform check in the background (default true since v3)

=item * C<charge_applicant_for_check> - the applicant must enter payment
details for this check, and it will not count towards the quota for this
service account

=item * C<consider> - used for sandbox API testing only

=back

Returns a L<Future> which will resolve with the result.

=cut

sub applicant_check {
    my ($self, %args) = @_;
    use Path::Tiny;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('checks');
            $self->hook(
                'on_api_hit',
                {
                    POST => $uri,
                    body => \%args
                });
            $self->ua->POST(
                $uri,
                encode_json_utf8(\%args),
                content_type => 'application/json',
                $self->auth_headers,
            );
        }
    )->catch(
        http => sub {
            my ($message, undef, $response, $request) = @_;

            $log->errorf('Request %s received %s with full response as %s', $request->uri, $message, $response->content,);

            # Just pass it on
            Future->fail(
                $message,
                http => $response,
                $request
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Check->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

sub check_list {
    my ($self, %args) = @_;
    my $applicant_id = delete $args{applicant_id} or die 'Need an applicant ID';
    my $src          = $self->source;
    my $f            = $src->completed;
    my $uri          = $self->endpoint('checks');
    $uri->query('applicant_id=' . uri_escape_utf8($applicant_id));

    $log->tracef('GET %s', "$uri");
    $self->rate_limiting->then(
        sub {
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'GET',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                my ($total) = $res->header('X-Total-Count');
                $log->tracef('Expected total count %d', $total);
                for (@{$data->{checks}}) {
                    return $f if $f->is_ready;
                    $src->emit(WebService::Async::Onfido::Check->new(%$_, onfido => $self));
                }
                $src->finish;
                Future->done;
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        })->retain;
    return $src;
}

sub report_get {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('report', %args);
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'GET',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done(WebService::Async::Onfido::Report->new(%$data, onfido => $self));
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

sub report_list {
    my ($self, %args) = @_;

    my $check_id = delete $args{check_id} or die 'Need a check ID';

    my $src = $self->source;
    my $f   = $src->completed;

    my $uri = $self->endpoint('reports', check_id => $check_id);
    $uri->query('check_id=' . uri_escape_utf8($check_id));
    $log->tracef('GET %s', "$uri");

    $self->rate_limiting->then(
        sub {
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'GET',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;

                my $data = decode_json_utf8($res->content);
                for (@{$data->{reports}}) {
                    return $f if $f->is_ready;
                    $src->emit(WebService::Async::Onfido::Report->new(%$_, onfido => $self));
                }
                $src->finish;
                Future->done;
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        })->retain;
    return $src;
}

=head2 download_check

Gets the PDF report for a given L<WebService::Async::Onfido::Check>.

Takes the following named parameters:

=over 4

=item * C<check_id> - the L<WebService::Async::Onfido::Check/id> for the check to query

=back

Returns a PDF file blob

=cut

sub download_check {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('check_download', %args);
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'GET',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = $res->content;
                return Future->done($data);
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 download_photo

Gets a live_photo in a form of binary data for a given L<WebService::Async::Onfido::Photo>.

Takes the following named parameters:

=over 4

=item * C<live_photo_id> - the L<WebService::Async::Onfido::Photo/id> for the document to query

=back

Returns a photo file blob

=cut

sub download_photo {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('photo_download', %args);
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'GET',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = $res->content;
                return Future->done($data);
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 download_document

Gets a document in a form of binary data for a given L<WebService::Async::Onfido::Document>.

Takes the following named parameters:

=over 4

=item * C<applicant_id> - the L<WebService::Async::Onfido::Applicant/id> for the applicant to query

=item * C<document_id> - the L<WebService::Async::Onfido::Document/id> for the document to query

=back

Returns a document file blob

=cut

sub download_document {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('document_download', %args);
            $self->hook(
                'on_api_hit',
                {
                    GET => $uri,
                });
            $self->ua->do_request(
                uri    => $uri,
                method => 'GET',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = $res->content;
                return Future->done($data);
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 countries_list

Returns a hashref containing 3-letter country codes as keys and supporting status
as their value.

=cut

sub countries_list {
    my ($self) = @_;

    return $self->ua->GET(SUPPORTED_COUNTRIES_URL)->then(
        sub {
            try {
                my ($res) = @_;
                my $onfido_countries = decode_json_utf8($res->content);

                my %countries_list =
                    map { $_->{alpha3} => $_->{supported_identity_report} + 0 } @$onfido_countries;
                return Future->done(\%countries_list);
            } catch {
                my ($err) = $@;
                $log->errorf('Failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 supported_documents_list

Returns an array of hashes of supported_documents for each country

=cut

sub supported_documents_list {
    my $path = Path::Tiny::path(__DIR__)->parent(3)->child('share/supported_documents.json');
    $path = Path::Tiny::path(File::ShareDir::dist_file('WebService-Async-Onfido', 'supported_documents.json')) unless $path->exists;
    my $supported_documents = decode_json_text($path->slurp_utf8);
    return $supported_documents;
}

=head2 supported_documents_for_country

Returns the supported_documents_list for the country

=cut

sub supported_documents_for_country {
    my ($self, $country_code) = @_;

    my %country_details =
        map { $_->{country_code} => $_ } @{supported_documents_list()};

    return $country_details{$country_code}->{doc_types_list} // [];
}

=head2 is_country_supported

Returns 1 if country supported and 0 for unsupported

=cut

sub is_country_supported {
    my ($self, $country_code) = @_;

    my %country_details =
        map { $_->{country_code} => $_ } @{supported_documents_list()};

    return $country_details{$country_code} ? 1 : 0;
}

=head2 sdk_token

Returns the generated Onfido Web SDK token for the applicant.

L<https://documentation.onfido.com/#web-sdk-tokens>

Takes the following named parameters:

=over 4

=item * C<applicant_id> - ID of the applicant to request the token for

=item * C<referrer> - the URL of the web page where the Web SDK will be used

=back

=cut

sub sdk_token {
    my ($self, %args) = @_;
    return $self->rate_limiting->then(
        sub {
            my $uri = $self->endpoint('sdk_token');
            $self->hook(
                'on_api_hit',
                {
                    POST => $uri,
                    body => \%args,
                });
            $self->ua->POST(
                $uri,
                encode_json_utf8(\%args),
                content_type => 'application/json',
                $self->auth_headers,
            );
        }
    )->then(
        sub {
            try {
                my ($res) = @_;
                my $data = decode_json_utf8($res->content);
                $log->tracef('Have response %s', $data);
                return Future->done($data);
            } catch {
                my ($err) = $@;
                $log->errorf('Token generation failed - %s', $err);
                return Future->fail($err);
            }
        });
}

=head2 endpoints

Returns an accessor for the endpoints data. This is a hashref containing URI
templates, used by L</endpoint>.

=cut

sub endpoints {
    my ($self) = @_;
    return $self->{endpoints} ||= do {
        my $path =
            Path::Tiny::path(__DIR__)->parent(3)->child('share/endpoints.json');
        $path = Path::Tiny::path(File::ShareDir::dist_file('WebService-Async-Onfido', 'endpoints.json')) unless $path->exists;
        my $endpoints = decode_json_text($path->slurp_utf8);
        my $base_uri  = $self->base_uri;
        $_ = $base_uri . $_ for values %$endpoints;
        $endpoints;
    };
}

=head2 endpoint

Expands the selected URI via L<URI::Template>. Each item is defined in our C<endpoints.json>
file.

Returns a L<URI> instance.

=cut

sub endpoint {
    my ($self, $endpoint, %args) = @_;
    return URI::Template->new($self->endpoints->{$endpoint})->process(%args);
}

sub base_uri {
    my $self = shift;
    return $self->{base_uri} if blessed($self->{base_uri});
    $self->{base_uri} =
        URI->new($self->{base_uri} // 'https://api.eu.onfido.com');
    return $self->{base_uri};
}

sub token { return shift->{token} }

sub ua {
    my ($self) = @_;
    return $self->{ua} //= do {
        $self->add_child(
            my $ua = Net::Async::HTTP->new(
                fail_on_error            => 1,
                decode_content           => 1,
                pipeline                 => 0,
                stall_timeout            => 60,
                max_connections_per_host => 2,
                user_agent => 'Mozilla/4.0 (WebService::Async::Onfido; DERIV@cpan.org; https://metacpan.org/pod/WebService::Async::Onfido)',
            ));
        $ua;
    }
}

sub auth_headers {
    my ($self) = @_;
    return headers => {'Authorization' => 'Token token=' . $self->token};
}

sub ryu {
    my ($self) = @_;
    return $self->{ryu} //= do {
        $self->add_child(my $ryu = Ryu::Async->new);
        $ryu;
    }
}

=head2 is_rate_limited

Returns true if we are currently rate limited, false otherwise.

May eventually be updated to return number of seconds that you need to wait.

=cut

sub is_rate_limited {
    my ($self) = @_;
    return $self->{rate_limit}
        && $self->{request_count} >= $self->requests_per_minute;
}

=head2 rate_limiting

Applies rate limiting check.

Returns a L<Future> which will resolve once it's safe to send further requests.

=cut

sub rate_limiting {
    my ($self) = @_;
    $self->{rate_limit} //= do {
        $self->loop->delay_future(after => $self->{rate_limit_delay})->on_ready(
            sub {
                $self->{request_count} = 0;
                delete $self->{rate_limit};
            });
    };

    return Future->done
        unless $self->requests_per_minute
        and ++$self->{request_count} >= $self->requests_per_minute;
    $self->hook(
        'on_rate_limit',
        {
            requests_count      => $self->{request_count},
            requests_per_minute => $self->requests_per_minute,
        });
    return $self->{rate_limit};
}

sub requests_per_minute { return shift->{requests_per_minute} //= 300 }

sub source {
    my ($self) = shift;
    return $self->ryu->source(@_);
}

sub _get_mime_type {
    my $filename = shift;

    my $ext = (fileparse($filename, "[^.]+"))[2];

    return $FILE_MIME_TYPE_MAPPING{lc($ext // '')} // 'application/octet-stream';
}

1;

__END__

=head1 AUTHOR

deriv.com

=head1 COPYRIGHT

Copyright Deriv.com 2019.

=head1 LICENSE

Licensed under the same terms as Perl5 itself.



Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.