File Coverage

blib/lib/WebService/Async/Onfido.pm
Criterion Covered Total %
statement 300 348 86.2
branch 14 30 46.6
condition 17 30 56.6
subroutine 105 116 90.5
pod 28 38 73.6
total 464 562 82.5


line stmt bran cond sub pod time code
1             package WebService::Async::Onfido;
2              
3             # ABSTRACT: Webservice to connect to Onfido API
4              
5 2     2   327274 use strict;
  2         12  
  2         62  
6 2     2   16 use warnings;
  2         4  
  2         88  
7              
8             our $VERSION = '0.006';
9              
10 2     2   968 use parent qw(IO::Async::Notifier);
  2         608  
  2         12  
11              
12             =head1 NAME
13              
14             WebService::Async::Onfido - unofficial support for the Onfido identity verification service
15              
16             =head1 SYNOPSIS
17              
18              
19             =head1 DESCRIPTION
20              
21             =cut
22              
23 2     2   35381 use mro;
  2         6  
  2         23  
24 2     2   989 no indirect;
  2         2301  
  2         11  
25              
26 2     2   1161 use Syntax::Keyword::Try;
  2         4406  
  2         11  
27 2     2   1064 use Dir::Self;
  2         949  
  2         14  
28 2     2   1242 use URI;
  2         10203  
  2         71  
29 2     2   880 use URI::QueryParam;
  2         1716  
  2         74  
30 2     2   1012 use URI::Template;
  2         11809  
  2         99  
31 2     2   1129 use Ryu::Async;
  2         296919  
  2         91  
32              
33 2     2   20 use Future;
  2         7  
  2         69  
34 2     2   11 use Future::Utils qw(repeat);
  2         5  
  2         142  
35 2     2   15 use File::Basename;
  2         4  
  2         236  
36 2     2   917 use Path::Tiny;
  2         14663  
  2         122  
37 2     2   1450 use Net::Async::HTTP;
  2         103132  
  2         95  
38 2     2   29 use HTTP::Request::Common;
  2         5  
  2         152  
39 2     2   479 use JSON::MaybeUTF8 qw(:v1);
  2         8348  
  2         282  
40 2     2   28 use JSON::MaybeXS;
  2         5  
  2         130  
41 2     2   1199 use File::ShareDir;
  2         57323  
  2         118  
42 2     2   14 use URI::Escape qw(uri_escape_utf8);
  2         51  
  2         108  
43 2     2   14 use Scalar::Util qw(blessed);
  2         11  
  2         136  
44              
45 2     2   1128 use WebService::Async::Onfido::Applicant;
  2         7  
  2         77  
46 2     2   858 use WebService::Async::Onfido::Address;
  2         5  
  2         73  
47 2     2   826 use WebService::Async::Onfido::Document;
  2         9  
  2         124  
48 2     2   840 use WebService::Async::Onfido::Photo;
  2         5  
  2         75  
49 2     2   810 use WebService::Async::Onfido::Video;
  2         36  
  2         76  
50 2     2   873 use WebService::Async::Onfido::Check;
  2         7  
  2         77  
51 2     2   893 use WebService::Async::Onfido::Report;
  2         11  
  2         81  
52              
53 2     2   13 use Log::Any qw($log);
  2         4  
  2         10  
54 2     2   423 use constant SUPPORTED_COUNTRIES_URL => 'https://documentation.onfido.com/identityISOsupported.json';
  2         6  
  2         15025  
55              
56             # Mapping file extension to mime type for currently
57             # supported document types
58             my %FILE_MIME_TYPE_MAPPING = (
59             jpg => 'image/jpeg',
60             jpeg => 'image/jpeg',
61             png => 'image/png',
62             pdf => 'application/pdf',
63             );
64              
65             sub configure {
66 5     5 1 4045518 my ($self, %args) = @_;
67 5         57 for my $k (qw(token requests_per_minute base_uri on_api_hit on_rate_limit rate_limit_delay)) {
68 30 100       172 $self->{$k} = delete $args{$k} if exists $args{$k};
69             }
70              
71 5   100     100 $self->{rate_limit_delay} //= 60;
72 5         114 return $self->next::method(%args);
73             }
74              
75             =head2 hook
76              
77             Executes a hook, if specified at configure time.
78              
79             Takes the following:
80              
81             =over 4
82              
83             =item * C<$hook> - the hook to execute
84              
85             =item * C<$data> - data to pass to the sub
86              
87             =back
88              
89             It returns C<undef>
90              
91             =cut
92              
93             sub hook {
94 26     26 1 132 my ($self, $hook, $data) = @_;
95              
96 26 100       114 return undef unless $self->{$hook};
97              
98 23 100       83 return undef unless ref($self->{$hook}) eq 'CODE';
99              
100 22         83 $self->{$hook}->($data);
101              
102 22         183 return undef;
103             }
104              
105             =head2 applicant_list
106              
107             Retrieves a list of all known applicants.
108              
109             Returns a L<Ryu::Source> which will emit one L<WebService::Async::Onfido::Applicant> for
110             each applicant found.
111              
112             =cut
113              
114             sub applicant_list {
115 1     1 1 47 my ($self) = @_;
116 1         6 my $src = $self->source;
117 1         500 my $f = $src->completed;
118 1         457 my $uri = $self->endpoint('applicants');
119             (
120             repeat {
121 1     1   41 $log->tracef('GET %s', "$uri");
122             $self->rate_limiting->then(
123             sub {
124 1         179 $self->hook('on_api_hit', {GET => $uri});
125 1         4 $self->ua->GET($uri, $self->auth_headers,);
126             }
127             )->then(
128             sub {
129             try {
130             my ($res) = @_;
131             my $data = decode_json_utf8($res->content);
132             $log->tracef('Have response %s', $data);
133             my ($total) = $res->header('X-Total-Count');
134             $log->tracef('Expected total count %d', $total);
135             for (@{$data->{applicants}}) {
136             return $f if $f->is_ready;
137             $src->emit(WebService::Async::Onfido::Applicant->new(%$_, onfido => $self));
138             }
139             $log->tracef('Links are %s', [$res->header('Link')]);
140             my %links = $self->extract_links($res->header('Link'));
141             if (my $next = $links{next}) {
142             ($uri) = $next;
143             } else {
144             $src->finish;
145             }
146             return Future->done;
147 1         4085 } catch {
148             my ($err) = $@;
149             $log->errorf('Failed - %s', $err);
150             return Future->fail($err);
151             }
152             },
153             sub {
154 0         0 my ($err, @details) = @_;
155 0         0 $log->errorf('Failed to request document_list: %s', $err);
156 0 0       0 $src->fail($err, @details) unless $src->is_ready;
157 0         0 Future->fail($err, @details);
158             })
159 1         15 }
160 1     1   170 until => sub { $f->is_ready })->retain;
  1         126  
161 1         2821 return $src;
162             }
163              
164             =head2 paging
165              
166             Supports paging through HTTP GET requests.
167              
168             =over 4
169              
170             =item * C<$starting_uri> - the initial L<URI> to request
171              
172             =item * C<$factory> - a C<sub> that we will call with a L<Ryu::Source> and expect to return
173             a second response-processing C<sub>.
174              
175             =back
176              
177             Returns a L<Ryu::Source>.
178              
179             =cut
180              
181             sub paging {
182 0     0 1 0 my ($self, $starting_uri, $factory) = @_;
183 0 0       0 my $uri =
184             ref($starting_uri)
185             ? $starting_uri->clone
186             : URI->new($starting_uri);
187              
188 0         0 my $src = $self->source;
189 0         0 my $f = $src->completed;
190 0         0 my $code = $factory->($src);
191             (
192             repeat {
193 0     0   0 $log->tracef('GET %s', "$uri");
194             $self->rate_limiting->then(
195             sub {
196 0         0 $self->hook('on_api_hit', {GET => $uri});
197 0         0 $self->ua->GET($uri, $self->auth_headers,);
198             }
199             )->then(
200             sub {
201             try {
202             my ($res) = @_;
203             my $data = decode_json_utf8($res->content);
204             $log->tracef('Have response %s', $data);
205             my ($total) = $res->header('X-Total-Count');
206             $log->tracef('Expected total count %d', $total);
207             $code->($data);
208             $log->tracef('Links are %s', [$res->header('Link')]);
209             my %links = $self->extract_links($res->header('Link'));
210              
211             if (my $next = $links{next}) {
212             ($uri) = $next;
213             } else {
214             $src->finish;
215             }
216             return Future->done;
217 0         0 } catch {
218             my ($err) = $@;
219             $log->errorf('Failed - %s', $err);
220             return Future->fail($err);
221             }
222             },
223             sub {
224 0         0 my ($err, @details) = @_;
225 0         0 $log->errorf('Failed to request %s: %s', $uri, $err);
226 0 0       0 $src->fail($err, @details) unless $src->is_ready;
227 0         0 Future->fail($err, @details);
228             })
229 0         0 }
230 0     0   0 until => sub { $f->is_ready })->retain;
  0         0  
231 0         0 return $src;
232             }
233              
234             =head2 extract_links
235              
236             Given a set of strings representing the C<Link> headers in an HTTP response,
237             extracts the URIs based on the C<rel> attribute as described in
238             L<RFC5988|http://tools.ietf.org/html/rfc5988>.
239              
240             Returns a list of key, value pairs where the key contains the lowercase C<rel> value
241             and the value is a L<URI> instance.
242              
243             my %links = $self->extract_links($res->header('Link'))
244             print "Last page would be $links{last}"
245              
246             =cut
247              
248             sub extract_links {
249 1     1 1 79 my ($self, @links) = @_;
250 1         2 my %links;
251 1         7 for (map { split /\h*,\h*/ } @links) {
  0         0  
252              
253             # Format is like:
254             # <https://api.eu.onfido.com/v3.4/applicants?page=2>; rel="next"
255 0 0       0 if (my ($url, $rel) = m{<(http[^>]+)>;\h*rel="([^"]+)"}) {
256 0         0 $links{lc $rel} = URI->new($url);
257             }
258             }
259 1         3 return %links;
260             }
261              
262             =head2 applicant_create
263              
264             Creates a new applicant record.
265              
266             See accessors in L<WebService::Async::Onfido::Applicant> for a full list of supported attributes.
267             These can be passed as named parameters to this method.
268              
269             Returns a L<Future> which resolves to a L<WebService::Async::Onfido::Applicant>
270             instance on successful completion.
271              
272             =cut
273              
274             sub applicant_create {
275 1     1 1 473 my ($self, %args) = @_;
276             return $self->rate_limiting->then(
277             sub {
278 1     1   342 my $uri = $self->endpoint('applicants');
279 1         296 $self->hook(
280             'on_api_hit',
281             {
282             POST => $uri,
283             body => \%args,
284             });
285 1         6 $self->ua->POST(
286             $uri,
287             encode_json_utf8(\%args),
288             content_type => 'application/json',
289             $self->auth_headers,
290             );
291             }
292             )->then(
293             sub {
294             try {
295             my ($res) = @_;
296             my $data = decode_json_utf8($res->content);
297             $log->tracef('Have response %s', $data);
298             return Future->done(WebService::Async::Onfido::Applicant->new(%$data, onfido => $self));
299 1     1   70200 } catch {
300             my ($err) = $@;
301             $log->errorf('Applicant creation failed - %s', $err);
302             return Future->fail($err);
303             }
304 1         23 });
305             }
306              
307             =head2 applicant_update
308              
309             Updates a single applicant.
310              
311             Returns a L<Future> which resolves to empty on success.
312              
313             =cut
314              
315             sub applicant_update {
316 1     1 1 85 my ($self, %args) = @_;
317             return $self->rate_limiting->then(
318             sub {
319 1     1   220 my $uri = $self->endpoint('applicant', %args);
320 1         1008 $self->hook(
321             'on_api_hit',
322             {
323             PUT => $uri,
324             body => \%args,
325             });
326 1         6 $self->ua->PUT(
327             $uri,
328             encode_json_utf8(\%args),
329             content_type => 'application/json',
330             $self->auth_headers,
331             );
332             }
333             )->then(
334             sub {
335             try {
336             my ($res) = @_;
337             my $data = decode_json_utf8($res->content);
338             $log->tracef('Have response %s', $data);
339             return Future->done();
340 1     1   7830 } catch {
341             my ($err) = $@;
342             $log->errorf('Applicant update failed - %s', $err);
343             return Future->fail($err);
344             }
345 1         14 });
346             }
347              
348             =head2 applicant_delete
349              
350             Deletes a single applicant.
351              
352             Returns a L<Future> which resolves to empty on success.
353              
354             =cut
355              
356             sub applicant_delete {
357 1     1 1 70 my ($self, %args) = @_;
358             return $self->rate_limiting->then(
359             sub {
360 1     1   227 my $uri = $self->endpoint('applicant', %args);
361 1         422 $self->hook(
362             'on_api_hit',
363             {
364             DELETE => $uri,
365             });
366 1         5 $self->ua->do_request(
367             uri => $uri,
368             method => 'DELETE',
369             $self->auth_headers,
370             );
371             }
372             )->then(
373             sub {
374             try {
375             my ($res) = @_;
376             return Future->done if $res->code == 204;
377             my $data = decode_json_utf8($res->content);
378             $log->tracef('Have response %s', $data);
379             return Future->fail($data);
380 1     1   6662 } catch {
381             my ($err) = $@;
382             $log->errorf('Applicant delete failed - %s', $err);
383             return Future->fail($err);
384             }
385 1         7 });
386             }
387              
388             =head2 applicant_get
389              
390             Retrieve a single applicant.
391              
392             Returns a L<Future> which resolves to a L<WebService::Async::Onfido::Applicant>
393              
394             =cut
395              
396             sub applicant_get {
397 2     2 1 39 my ($self, %args) = @_;
398             return $self->rate_limiting->then(
399             sub {
400 2     2   312 my $uri = $self->endpoint('applicant', %args);
401 2         587 $self->hook(
402             'on_api_hit',
403             {
404             GET => $uri,
405             });
406 2         7 $self->ua->do_request(
407             uri => $uri,
408             method => 'GET',
409             $self->auth_headers,
410             );
411             }
412             )->then(
413             sub {
414             try {
415             my ($res) = @_;
416             my $data = decode_json_utf8($res->content);
417             $log->tracef('Have response %s', $data);
418             return Future->done(WebService::Async::Onfido::Applicant->new(%$data, onfido => $self));
419 2     2   12419 } catch {
420             my ($err) = $@;
421             $log->errorf('Failed - %s', $err);
422             return Future->fail($err);
423             }
424 2         8 });
425             }
426              
427             sub check_get {
428 1     1 0 75 my ($self, %args) = @_;
429             return $self->rate_limiting->then(
430             sub {
431 1     1   189 my $uri = $self->endpoint('check', %args);
432 1         290 $self->hook(
433             'on_api_hit',
434             {
435             GET => $uri,
436             });
437 1         4 $self->ua->do_request(
438             uri => $uri,
439             method => 'GET',
440             $self->auth_headers,
441             );
442             }
443             )->then(
444             sub {
445             try {
446             my ($res) = @_;
447             my $data = decode_json_utf8($res->content);
448             $log->tracef('Have response %s', $data);
449             return Future->done(WebService::Async::Onfido::Check->new(%$data, onfido => $self));
450 1     1   7231 } catch {
451             my ($err) = $@;
452             $log->errorf('Failed - %s', $err);
453             return Future->fail($err);
454             }
455 1         5 });
456             }
457              
458             =head2 document_list
459              
460             List all documents for a given L<WebService::Async::Onfido::Applicant>.
461              
462             Takes the following named parameters:
463              
464             =over 4
465              
466             =item * C<applicant_id> - the L<WebService::Async::Onfido::Applicant/id> for the applicant to query
467              
468             =back
469              
470             Returns a L<Ryu::Source> which will emit one L<WebService::Async::Onfido::Document> for
471             each document found.
472              
473             =cut
474              
475             sub document_list {
476 1     1 1 256 my ($self, %args) = @_;
477 1         5 my $src = $self->source;
478 1         198 my $uri = $self->endpoint('documents');
479 1         136 $uri->query('applicant_id=' . uri_escape_utf8($args{applicant_id}));
480              
481             $self->rate_limiting->then(
482             sub {
483 1     1   196 $self->hook(
484             'on_api_hit',
485             {
486             GET => $uri,
487             });
488 1         5 $self->ua->GET($uri, $self->auth_headers,);
489             }
490             )->then(
491             sub {
492             try {
493             my ($res) = @_;
494             $log->tracef("GET %s => %s", $uri, $res->decoded_content);
495             my $data = decode_json_utf8($res->content);
496             my $f = $src->completed;
497             $log->tracef('Have response %s', $data);
498             for (@{$data->{documents}}) {
499             return $f if $f->is_ready;
500             $src->emit(WebService::Async::Onfido::Document->new(%$_, onfido => $self));
501             }
502             $src->finish;
503             return Future->done;
504 1     1   4954 } catch {
505             my ($err) = $@;
506             $log->errorf('Failed - %s', $err);
507             $src->fail('Failed to get document list.')
508             unless $src->is_ready;
509             return Future->fail($err);
510             }
511 1         47 })->retain;
512 1         2888 return $src;
513             }
514              
515             =head2 get_document_details
516              
517             Gets a document object for a given L<WebService::Async::Onfido::Applicant>.
518              
519             Takes the following named parameters:
520              
521             =over 4
522              
523             =item * C<applicant_id> - the L<WebService::Async::Onfido::Applicant/id> for the applicant to query
524              
525             =item * C<document_id> - the L<WebService::Async::Onfido::Document/id> for the document to query
526              
527             =back
528              
529             Returns a Future object which consists of a L<WebService::Async::Onfido::Document>
530              
531             =cut
532              
533             sub get_document_details {
534 1     1 1 82 my ($self, %args) = @_;
535 1         7 my $uri = $self->endpoint('document', %args);
536             return $self->rate_limiting->then(
537             sub {
538 1     1   213 $self->hook(
539             'on_api_hit',
540             {
541             GET => $uri,
542             });
543 1         6 $self->ua->GET($uri, $self->auth_headers,);
544             }
545             )->then(
546             sub {
547             try {
548             my ($res) = @_;
549             $log->tracef("GET %s => %s", $uri, $res->decoded_content);
550             my $data = decode_json_utf8($res->content);
551             $log->tracef('Have response %s', $data);
552             return Future->done(WebService::Async::Onfido::Document->new(%$data, onfido => $self));
553 1     1   6791 } catch {
554             my ($err) = $@;
555             $log->errorf('Failed - %s', $err);
556             return Future->fail($err);
557             }
558 1         354 });
559             }
560              
561             =head2 photo_list
562              
563             List all photos for a given L<WebService::Async::Onfido::Applicant>.
564              
565             Takes the following named parameters:
566              
567             =over 4
568              
569             =item * C<applicant_id> - the L<WebService::Async::Onfido::Applicant/id> for the applicant to query
570              
571             =back
572              
573             Returns a L<Ryu::Source> which will emit one L<WebService::Async::Onfido::Photo> for
574             each photo found.
575              
576             =cut
577              
578             sub photo_list {
579 1     1 1 51 my ($self, %args) = @_;
580 1         4 my $src = $self->source;
581 1         153 my $uri = $self->endpoint('photos', %args);
582             $self->rate_limiting->then(
583             sub {
584 1     1   211 $self->hook(
585             'on_api_hit',
586             {
587             GET => $uri,
588             });
589 1         9 $self->ua->GET($uri, $self->auth_headers,);
590             }
591             )->then(
592             sub {
593             try {
594             my ($res) = @_;
595             $log->tracef("GET %s => %s", $uri, $res->decoded_content);
596             my $data = decode_json_utf8($res->content);
597             my $f = $src->completed;
598             $log->tracef('Have response %s', $data);
599             for (@{$data->{live_photos}}) {
600             return $f if $f->is_ready;
601             $src->emit(WebService::Async::Onfido::Photo->new(%$_, onfido => $self));
602             }
603             $src->finish;
604             return Future->done;
605 1     1   5443 } catch {
606             my ($err) = $@;
607             $log->errorf('Failed - %s', $err);
608             $src->fail('Failed to get photo list.') unless $src->is_ready;
609             return Future->fail($err);
610             }
611 1         385 })->retain;
612 1         2967 return $src;
613             }
614              
615             =head2 get_photo_details
616              
617             Gets a live_photo object for a given L<WebService::Async::Onfido::Applicant>.
618              
619             Takes the following named parameters:
620              
621             =over 4
622              
623             =item * C<live_photo_id> - the L<WebService::Async::Onfido::Photo/id> for the document to query
624              
625             =back
626              
627             Returns a Future object which consists of a L<WebService::Async::Onfido::Photo>
628              
629             =cut
630              
631             sub get_photo_details {
632 1     1 1 47 my ($self, %args) = @_;
633 1         4 my $uri = $self->endpoint('photo', %args);
634             return $self->rate_limiting->then(
635             sub {
636 1     1   191 $self->hook(
637             'on_api_hit',
638             {
639             GET => $uri,
640             });
641 1         4 $self->ua->GET($uri, $self->auth_headers,);
642             }
643             )->then(
644             sub {
645             try {
646             my ($res) = @_;
647             $log->tracef("GET %s => %s", $uri, $res->decoded_content);
648             my $data = decode_json_utf8($res->content);
649             $log->tracef('Have response %s', $data);
650             return Future->done(WebService::Async::Onfido::Photo->new(%$data, onfido => $self));
651 1     1   7475 } catch {
652             my ($err) = $@;
653             $log->errorf('Failed - %s', $err);
654             return Future->fail($err);
655             }
656 1         302 });
657             }
658              
659             =head2 document_upload
660              
661             Uploads a single document for a given applicant.
662              
663             Takes the following named parameters:
664              
665             =over 4
666              
667             =item * C<type> - can be C<passport>, C<photo>, C<poa>
668              
669             =item * C<side> - which side, either C<front> or C<back>
670              
671             =item * C<issuing_country> - which country this document is for
672              
673             =item * C<filename> - the file name to use for this item
674              
675             =item * C<data> - the bytes for this image file (must be in JPEG format)
676              
677             =back
678              
679             =cut
680              
681             sub document_upload {
682 1     1 1 92 my ($self, %args) = @_;
683 1         6 my $uri = $self->endpoint('documents');
684              
685             my $req = HTTP::Request::Common::POST(
686             $uri,
687             content_type => 'form-data',
688             content => [
689 4         17 %args{grep { exists $args{$_} } qw(type side issuing_country applicant_id)},
690             file => [
691             undef, $args{filename},
692             'Content-Type' => _get_mime_type($args{filename}),
693             Content => $args{data}
694             ],
695             ],
696 1         139 %{$self->auth_headers},
  1         8  
697             );
698             return $self->rate_limiting->then(
699             sub {
700 1     1   172 delete $args{data};
701 1         6 $self->hook(
702             'on_api_hit',
703             {
704             POST => $uri,
705             body => \%args
706             });
707 1         7 $self->ua->do_request(
708             request => $req,
709             );
710             }
711             )->catch(
712             http => sub {
713 0     0   0 my ($message, undef, $response, $request) = @_;
714 0         0 $log->errorf('Request %s received %s with full response as %s', $request->uri, $message, $response->content,);
715              
716             # Just pass it on
717 0         0 Future->fail(
718             $message,
719             http => $response,
720             $request
721             );
722             }
723             )->then(
724             sub {
725             try {
726             my ($res) = @_;
727             my $data = decode_json_utf8($res->content);
728             $log->tracef('Have response %s', $data);
729             return Future->done(WebService::Async::Onfido::Document->new(%$data, onfido => $self));
730 1     1   74216 } catch {
731             my ($err) = $@;
732             $log->errorf('Failed - %s', $err);
733             return Future->fail($err);
734             }
735 1         3232 });
736             }
737              
738             =head2 live_photo_upload
739              
740             Uploads a single "live photo" for a given applicant.
741              
742             Takes the following named parameters:
743              
744             =over 4
745              
746             =item * C<applicant_id> - ID for the person this photo relates to
747              
748             =item * C<advanced_validation> - perform additional validation (ensure we only have a single face)
749              
750             =item * C<filename> - the file name to use for this item
751              
752             =item * C<data> - the bytes for this image file (must be in JPEG format)
753              
754             =back
755              
756             =cut
757              
758             sub live_photo_upload {
759 1     1 1 100 my ($self, %args) = @_;
760 1         5 my $uri = $self->endpoint('photo_upload');
761 1 50       140 $args{advanced_validation} = $args{advanced_validation} ? 'true' : 'false';
762             my $req = HTTP::Request::Common::POST(
763             $uri,
764             content_type => 'form-data',
765             content => [
766 2         12 %args{grep { exists $args{$_} } qw(advanced_validation applicant_id)},
767             file => [
768             undef, $args{filename},
769             'Content-Type' => _get_mime_type($args{filename}),
770             Content => $args{data}
771             ],
772             ],
773 1         6 %{$self->auth_headers},
  1         6  
774             );
775 1         846 $log->tracef('Photo upload: %s', $req->as_string("\n"));
776             return $self->rate_limiting->then(
777             sub {
778 1     1   168 delete $args{data};
779 1         6 $self->hook(
780             'on_api_hit',
781             {
782             POST => $uri,
783             body => \%args
784             });
785 1         7 $self->ua->do_request(
786             request => $req,
787             );
788             }
789             )->catch(
790             http => sub {
791 0     0   0 my ($message, undef, $response, $request) = @_;
792 0         0 $log->errorf('Request %s received %s with full response as %s', $request->uri, $message, $response->content,);
793              
794             # Just pass it on
795 0         0 Future->fail(
796             $message,
797             http => $response,
798             $request
799             );
800             }
801             )->then(
802             sub {
803             try {
804             my ($res) = @_;
805             my $data = decode_json_utf8($res->content);
806             $log->tracef('Have response %s', $data);
807             return Future->done(WebService::Async::Onfido::Photo->new(%$data, onfido => $self));
808 1     1   55182 } catch {
809             my ($err) = $@;
810             $log->errorf('Failed - %s', $err);
811             return Future->fail($err);
812             }
813 1         221 });
814             }
815              
816             =head2 applicant_check
817              
818             Perform an identity check on an applicant.
819              
820             This is the main method for dealing with verification - once you have created
821             the applicant and uploaded some documents, call this to start the process of
822             checking the documents and details, and generating the reports.
823              
824             L<https://documentation.onfido.com/#check-object>
825              
826             Takes the following named parameters:
827              
828             =over 4
829              
830             =item * C<applicant_id> - the applicant requesting the check
831              
832             =item * C<document_ids> - arrayref of documents ids to be analyzed on this check
833              
834             =item * C<report_names> - arrayref of the reports to be made (e.g: document, facial_similarity_photo)
835              
836             =item * C<tags> - custom tags to apply to these reports
837              
838             =item * C<suppress_form_emails> - if true, do B<not> send out the email to
839             the applicant
840              
841             =item * C<asynchronous> - return immediately and perform check in the background (default true since v3)
842              
843             =item * C<charge_applicant_for_check> - the applicant must enter payment
844             details for this check, and it will not count towards the quota for this
845             service account
846              
847             =item * C<consider> - used for sandbox API testing only
848              
849             =back
850              
851             Returns a L<Future> which will resolve with the result.
852              
853             =cut
854              
855             sub applicant_check {
856 1     1 1 73 my ($self, %args) = @_;
857 2     2   20 use Path::Tiny;
  2         4  
  2         9929  
858             return $self->rate_limiting->then(
859             sub {
860 1     1   204 my $uri = $self->endpoint('checks');
861 1         137 $self->hook(
862             'on_api_hit',
863             {
864             POST => $uri,
865             body => \%args
866             });
867 1         9 $self->ua->POST(
868             $uri,
869             encode_json_utf8(\%args),
870             content_type => 'application/json',
871             $self->auth_headers,
872             );
873             }
874             )->catch(
875             http => sub {
876 0     0   0 my ($message, undef, $response, $request) = @_;
877              
878 0         0 $log->errorf('Request %s received %s with full response as %s', $request->uri, $message, $response->content,);
879              
880             # Just pass it on
881 0         0 Future->fail(
882             $message,
883             http => $response,
884             $request
885             );
886             }
887             )->then(
888             sub {
889             try {
890             my ($res) = @_;
891             my $data = decode_json_utf8($res->content);
892             $log->tracef('Have response %s', $data);
893             return Future->done(WebService::Async::Onfido::Check->new(%$data, onfido => $self));
894 1     1   54689 } catch {
895             my ($err) = $@;
896             $log->errorf('Failed - %s', $err);
897             return Future->fail($err);
898             }
899 1         7 });
900             }
901              
902             sub check_list {
903 5     5 0 606 my ($self, %args) = @_;
904 5 50       28 my $applicant_id = delete $args{applicant_id} or die 'Need an applicant ID';
905 5         21 my $src = $self->source;
906 5         825 my $f = $src->completed;
907 5         1095 my $uri = $self->endpoint('checks');
908 5         694 $uri->query('applicant_id=' . uri_escape_utf8($applicant_id));
909              
910 5         315 $log->tracef('GET %s', "$uri");
911             $self->rate_limiting->then(
912             sub {
913 5     5   651 $self->hook(
914             'on_api_hit',
915             {
916             GET => $uri,
917             });
918 5         33 $self->ua->do_request(
919             uri => $uri,
920             method => 'GET',
921             $self->auth_headers,
922             );
923             }
924             )->then(
925             sub {
926             try {
927             my ($res) = @_;
928             my $data = decode_json_utf8($res->content);
929             $log->tracef('Have response %s', $data);
930             my ($total) = $res->header('X-Total-Count');
931             $log->tracef('Expected total count %d', $total);
932             for (@{$data->{checks}}) {
933             return $f if $f->is_ready;
934             $src->emit(WebService::Async::Onfido::Check->new(%$_, onfido => $self));
935             }
936             $src->finish;
937             Future->done;
938 5     5   54812 } catch {
939             my ($err) = $@;
940             $log->errorf('Failed - %s', $err);
941             return Future->fail($err);
942             }
943 5         57 })->retain;
944 5         11472 return $src;
945             }
946              
947             sub report_get {
948 1     1 0 126 my ($self, %args) = @_;
949             return $self->rate_limiting->then(
950             sub {
951 1     1   164 my $uri = $self->endpoint('report', %args);
952 1         283 $self->hook(
953             'on_api_hit',
954             {
955             GET => $uri,
956             });
957 1         4 $self->ua->do_request(
958             uri => $uri,
959             method => 'GET',
960             $self->auth_headers,
961             );
962             }
963             )->then(
964             sub {
965             try {
966             my ($res) = @_;
967             my $data = decode_json_utf8($res->content);
968             $log->tracef('Have response %s', $data);
969             return Future->done(WebService::Async::Onfido::Report->new(%$data, onfido => $self));
970 1     1   6196 } catch {
971             my ($err) = $@;
972             $log->errorf('Failed - %s', $err);
973             return Future->fail($err);
974             }
975 1         4 });
976             }
977              
978             sub report_list {
979 1     1 0 18 my ($self, %args) = @_;
980              
981 1 50       19 my $check_id = delete $args{check_id} or die 'Need a check ID';
982              
983 1         6 my $src = $self->source;
984 1         129 my $f = $src->completed;
985              
986 1         204 my $uri = $self->endpoint('reports', check_id => $check_id);
987 1         137 $uri->query('check_id=' . uri_escape_utf8($check_id));
988 1         48 $log->tracef('GET %s', "$uri");
989              
990             $self->rate_limiting->then(
991             sub {
992 1     1   174 $self->hook(
993             'on_api_hit',
994             {
995             GET => $uri,
996             });
997 1         5 $self->ua->do_request(
998             uri => $uri,
999             method => 'GET',
1000             $self->auth_headers,
1001             );
1002             }
1003             )->then(
1004             sub {
1005             try {
1006             my ($res) = @_;
1007              
1008             my $data = decode_json_utf8($res->content);
1009             for (@{$data->{reports}}) {
1010             return $f if $f->is_ready;
1011             $src->emit(WebService::Async::Onfido::Report->new(%$_, onfido => $self));
1012             }
1013             $src->finish;
1014             Future->done;
1015 1     1   5317 } catch {
1016             my ($err) = $@;
1017             $log->errorf('Failed - %s', $err);
1018             return Future->fail($err);
1019             }
1020 1         14 })->retain;
1021 1         2511 return $src;
1022             }
1023              
1024             =head2 download_photo
1025              
1026             Gets a live_photo in a form of binary data for a given L<WebService::Async::Onfido::Photo>.
1027              
1028             Takes the following named parameters:
1029              
1030             =over 4
1031              
1032             =item * C<live_photo_id> - the L<WebService::Async::Onfido::Photo/id> for the document to query
1033              
1034             =back
1035              
1036             Returns a photo file blob
1037              
1038             =cut
1039              
1040             sub download_photo {
1041 1     1 1 123 my ($self, %args) = @_;
1042             return $self->rate_limiting->then(
1043             sub {
1044 1     1   229 my $uri = $self->endpoint('photo_download', %args);
1045 1         414 $self->hook(
1046             'on_api_hit',
1047             {
1048             GET => $uri,
1049             });
1050 1         7 $self->ua->do_request(
1051             uri => $uri,
1052             method => 'GET',
1053             $self->auth_headers,
1054             );
1055             }
1056             )->then(
1057             sub {
1058             try {
1059             my ($res) = @_;
1060             my $data = $res->content;
1061             return Future->done($data);
1062 1     1   7943 } catch {
1063             my ($err) = $@;
1064             $log->errorf('Failed - %s', $err);
1065             return Future->fail($err);
1066             }
1067 1         7 });
1068             }
1069              
1070             =head2 download_document
1071              
1072             Gets a document in a form of binary data for a given L<WebService::Async::Onfido::Document>.
1073              
1074             Takes the following named parameters:
1075              
1076             =over 4
1077              
1078             =item * C<applicant_id> - the L<WebService::Async::Onfido::Applicant/id> for the applicant to query
1079              
1080             =item * C<document_id> - the L<WebService::Async::Onfido::Document/id> for the document to query
1081              
1082             =back
1083              
1084             Returns a document file blob
1085              
1086             =cut
1087              
1088             sub download_document {
1089 1     1 1 115 my ($self, %args) = @_;
1090             return $self->rate_limiting->then(
1091             sub {
1092 1     1   223 my $uri = $self->endpoint('document_download', %args);
1093 1         295 $self->hook(
1094             'on_api_hit',
1095             {
1096             GET => $uri,
1097             });
1098 1         4 $self->ua->do_request(
1099             uri => $uri,
1100             method => 'GET',
1101             $self->auth_headers,
1102             );
1103             }
1104             )->then(
1105             sub {
1106             try {
1107             my ($res) = @_;
1108             my $data = $res->content;
1109             return Future->done($data);
1110 1     1   7392 } catch {
1111             my ($err) = $@;
1112             $log->errorf('Failed - %s', $err);
1113             return Future->fail($err);
1114             }
1115 1         7 });
1116             }
1117              
1118             =head2 countries_list
1119              
1120             Returns a hashref containing 3-letter country codes as keys and supporting status
1121             as their value.
1122              
1123             =cut
1124              
1125             sub countries_list {
1126 0     0 1 0 my ($self) = @_;
1127              
1128             return $self->ua->GET(SUPPORTED_COUNTRIES_URL)->then(
1129             sub {
1130             try {
1131             my ($res) = @_;
1132             my $onfido_countries = decode_json_utf8($res->content);
1133              
1134             my %countries_list =
1135             map { $_->{alpha3} => $_->{supported_identity_report} + 0 } @$onfido_countries;
1136             return Future->done(\%countries_list);
1137 0     0   0 } catch {
1138             my ($err) = $@;
1139             $log->errorf('Failed - %s', $err);
1140             return Future->fail($err);
1141             }
1142 0         0 });
1143             }
1144              
1145             =head2 supported_documents_list
1146              
1147             Returns an array of hashes of supported_documents for each country
1148              
1149             =cut
1150              
1151             sub supported_documents_list {
1152 2     2 1 6628 my $path = Path::Tiny::path(__DIR__)->parent(3)->child('share/supported_documents.json');
1153 2 50       1033 $path = Path::Tiny::path(File::ShareDir::dist_file('WebService-Async-Onfido', 'supported_documents.json')) unless $path->exists;
1154 2         661 my $supported_documents = decode_json_text($path->slurp_utf8);
1155 2         2928 return $supported_documents;
1156             }
1157              
1158             =head2 supported_documents_for_country
1159              
1160             Returns the supported_documents_list for the country
1161              
1162             =cut
1163              
1164             sub supported_documents_for_country {
1165 0     0 1 0 my ($self, $country_code) = @_;
1166              
1167             my %country_details =
1168 0         0 map { $_->{country_code} => $_ } @{supported_documents_list()};
  0         0  
  0         0  
1169              
1170 0   0     0 return $country_details{$country_code}->{doc_types_list} // [];
1171             }
1172              
1173             =head2 is_country_supported
1174              
1175             Returns 1 if country supported and 0 for unsupported
1176              
1177             =cut
1178              
1179             sub is_country_supported {
1180 0     0 1 0 my ($self, $country_code) = @_;
1181              
1182             my %country_details =
1183 0         0 map { $_->{country_code} => $_ } @{supported_documents_list()};
  0         0  
  0         0  
1184              
1185 0 0       0 return $country_details{$country_code} ? 1 : 0;
1186             }
1187              
1188             =head2 sdk_token
1189              
1190             Returns the generated Onfido Web SDK token for the applicant.
1191              
1192             L<https://documentation.onfido.com/#web-sdk-tokens>
1193              
1194             Takes the following named parameters:
1195              
1196             =over 4
1197              
1198             =item * C<applicant_id> - ID of the applicant to request the token for
1199              
1200             =item * C<referrer> - the URL of the web page where the Web SDK will be used
1201              
1202             =back
1203              
1204             =cut
1205              
1206             sub sdk_token {
1207 1     1 1 57 my ($self, %args) = @_;
1208             return $self->rate_limiting->then(
1209             sub {
1210 1     1   203 my $uri = $self->endpoint('sdk_token');
1211 1         143 $self->hook(
1212             'on_api_hit',
1213             {
1214             POST => $uri,
1215             body => \%args,
1216             });
1217 1         5 $self->ua->POST(
1218             $uri,
1219             encode_json_utf8(\%args),
1220             content_type => 'application/json',
1221             $self->auth_headers,
1222             );
1223             }
1224             )->then(
1225             sub {
1226             try {
1227             my ($res) = @_;
1228             my $data = decode_json_utf8($res->content);
1229             $log->tracef('Have response %s', $data);
1230             return Future->done($data);
1231 1     1   52157 } catch {
1232             my ($err) = $@;
1233             $log->errorf('Token generation failed - %s', $err);
1234             return Future->fail($err);
1235             }
1236 1         5 });
1237             }
1238              
1239             =head2 endpoints
1240              
1241             Returns an accessor for the endpoints data. This is a hashref containing URI
1242             templates, used by L</endpoint>.
1243              
1244             =cut
1245              
1246             sub endpoints {
1247 46     46 1 108 my ($self) = @_;
1248 46   66     345 return $self->{endpoints} ||= do {
1249 4         37 my $path =
1250             Path::Tiny::path(__DIR__)->parent(3)->child('share/endpoints.json');
1251 4 50       2094 $path = Path::Tiny::path(File::ShareDir::dist_file('WebService-Async-Onfido', 'endpoints.json')) unless $path->exists;
1252 4         1318 my $endpoints = decode_json_text($path->slurp_utf8);
1253 4         1408 my $base_uri = $self->base_uri;
1254 4         177 $_ = $base_uri . $_ for values %$endpoints;
1255 4         275 $endpoints;
1256             };
1257             }
1258              
1259             =head2 endpoint
1260              
1261             Expands the selected URI via L<URI::Template>. Each item is defined in our C<endpoints.json>
1262             file.
1263              
1264             Returns a L<URI> instance.
1265              
1266             =cut
1267              
1268             sub endpoint {
1269 46     46 1 4732 my ($self, $endpoint, %args) = @_;
1270 46         165 return URI::Template->new($self->endpoints->{$endpoint})->process(%args);
1271             }
1272              
1273             sub base_uri {
1274 4     4 0 11 my $self = shift;
1275 4 50       24 return $self->{base_uri} if blessed($self->{base_uri});
1276             $self->{base_uri} =
1277 4   50     54 URI->new($self->{base_uri} // 'https://api.eu.onfido.com');
1278 4         2065 return $self->{base_uri};
1279             }
1280              
1281 24     24 1 253 sub token { return shift->{token} }
1282              
1283             sub ua {
1284 24     24 0 58 my ($self) = @_;
1285 24   66     155 return $self->{ua} //= do {
1286 4         65 $self->add_child(
1287             my $ua = Net::Async::HTTP->new(
1288             fail_on_error => 1,
1289             decode_content => 1,
1290             pipeline => 0,
1291             stall_timeout => 60,
1292             max_connections_per_host => 2,
1293             user_agent => 'Mozilla/4.0 (WebService::Async::Onfido; DERIV@cpan.org; https://metacpan.org/pod/WebService::Async::Onfido)',
1294             ));
1295 4         1350 $ua;
1296             }
1297             }
1298              
1299             sub auth_headers {
1300 24     24 0 171 my ($self) = @_;
1301 24         88 return headers => {'Authorization' => 'Token token=' . $self->token};
1302             }
1303              
1304             sub ryu {
1305 9     9 0 21 my ($self) = @_;
1306 9   66     90 return $self->{ryu} //= do {
1307 4         56 $self->add_child(my $ryu = Ryu::Async->new);
1308 4         534 $ryu;
1309             }
1310             }
1311              
1312             =head2 is_rate_limited
1313              
1314             Returns true if we are currently rate limited, false otherwise.
1315              
1316             May eventually be updated to return number of seconds that you need to wait.
1317              
1318             =cut
1319              
1320             sub is_rate_limited {
1321 0     0 1 0 my ($self) = @_;
1322             return $self->{rate_limit}
1323 0   0     0 && $self->{request_count} >= $self->requests_per_minute;
1324             }
1325              
1326             =head2 rate_limiting
1327              
1328             Applies rate limiting check.
1329              
1330             Returns a L<Future> which will resolve once it's safe to send further requests.
1331              
1332             =cut
1333              
1334             sub rate_limiting {
1335 24     24 1 68 my ($self) = @_;
1336 24   66     123 $self->{rate_limit} //= do {
1337             $self->loop->delay_future(after => $self->{rate_limit_delay})->on_ready(
1338             sub {
1339 2     2   2003738 $self->{request_count} = 0;
1340 2         17 delete $self->{rate_limit};
1341 5         21 });
1342             };
1343              
1344             return Future->done
1345             unless $self->requests_per_minute
1346 24 100 66     9445 and ++$self->{request_count} >= $self->requests_per_minute;
1347             $self->hook(
1348             'on_rate_limit',
1349             {
1350             requests_count => $self->{request_count},
1351 2         14 requests_per_minute => $self->requests_per_minute,
1352             });
1353 2         26 return $self->{rate_limit};
1354             }
1355              
1356 50   100 50 0 390 sub requests_per_minute { return shift->{requests_per_minute} //= 300 }
1357              
1358             sub source {
1359 9     9 0 35 my ($self) = shift;
1360 9         32 return $self->ryu->source(@_);
1361             }
1362              
1363             sub _get_mime_type {
1364 2     2   6 my $filename = shift;
1365              
1366 2         215 my $ext = (fileparse($filename, "[^.]+"))[2];
1367              
1368 2   50     73 return $FILE_MIME_TYPE_MAPPING{lc($ext // '')} // 'application/octet-stream';
      50        
1369             }
1370              
1371             1;
1372              
1373             __END__
1374              
1375             =head1 AUTHOR
1376              
1377             deriv.com
1378              
1379             =head1 COPYRIGHT
1380              
1381             Copyright Deriv.com 2019.
1382              
1383             =head1 LICENSE
1384              
1385             Licensed under the same terms as Perl5 itself.
1386