File Coverage

blib/lib/WebService/Async/Onfido.pm
Criterion Covered Total %
statement 259 309 83.8
branch 9 26 34.6
condition 14 28 50.0
subroutine 103 115 89.5
pod 27 37 72.9
total 412 515 80.0


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