File Coverage

blib/lib/Data/Validate/Sanctions/Redis.pm
Criterion Covered Total %
statement 27 69 39.1
branch 0 8 0.0
condition 0 22 0.0
subroutine 9 16 56.2
pod 4 4 100.0
total 40 119 33.6


line stmt bran cond sub pod time code
1             package Data::Validate::Sanctions::Redis;
2              
3 7     7   489831 use strict;
  7         42  
  7         264  
4 7     7   55 use warnings;
  7         20  
  7         216  
5              
6 7     7   44 use parent 'Data::Validate::Sanctions';
  7         17  
  7         77  
7              
8 7     7   548 use Data::Validate::Sanctions::Fetcher;
  7         16  
  7         188  
9 7     7   46 use Scalar::Util qw(blessed);
  7         19  
  7         429  
10 7     7   50 use List::Util qw(max);
  7         17  
  7         460  
11 7     7   2252 use JSON::MaybeUTF8 qw(encode_json_utf8 decode_json_utf8);
  7         17797  
  7         446  
12 7     7   1688 use YAML::XS qw(DumpFile);
  7         11154  
  7         362  
13 7     7   46 use Syntax::Keyword::Try;
  7         24  
  7         74  
14              
15             our $VERSION = '0.16'; # VERSION
16              
17             sub new {
18 0     0 1   my ($class, %args) = @_;
19              
20 0           my $self = {};
21              
22 0 0         $self->{connection} = $args{connection} or die 'Redis connection is missing';
23              
24 0           $self->{sources} = [keys Data::Validate::Sanctions::Fetcher::config(eu_token => 'dummy')->%*];
25              
26 0           $self->{args} = {%args};
27              
28 0           $self->{last_modification} = 0;
29 0           $self->{last_index} = 0;
30 0           $self->{last_data_load} = 0;
31              
32 0   0       my $object = bless $self, ref($class) || $class;
33 0           $object->_load_data();
34              
35 0           return $object;
36             }
37              
38             sub set_sanction_file {
39 0     0 1   die 'Not applicable';
40             }
41              
42             sub get_sanction_file {
43 0     0 1   die 'Not applicable';
44             }
45              
46             sub get_sanctioned_info {
47 0     0 1   my $self = shift;
48              
49 0 0         die "This function can only be called on an object" unless $self;
50              
51 0           return Data::Validate::Sanctions::get_sanctioned_info($self, @_);
52             }
53              
54             sub _load_data {
55 0     0     my $self = shift;
56              
57 0   0       $self->{last_modification} //= 0;
58 0   0       $self->{last_index} //= 0;
59 0   0       $self->{_data} //= {};
60 0   0       $self->{_sanctioned_name_tokens} //= {};
61 0   0       $self->{_token_sanctioned_names} //= {};
62              
63 0 0 0       return $self->{_data} if $self->{_data} and $self->{last_data_load} + $self->IGNORE_OPERATION_INTERVAL > time;
64              
65 0           my $latest_update = 0;
66 0           for my $source ($self->{sources}->@*) {
67             try {
68             $self->{_data}->{$source} //= {};
69              
70             my ($updated) = $self->{connection}->hget("SANCTIONS::$source" => 'updated');
71             $updated //= 0;
72             my $current_update_date = $self->{_data}->{$source}->{updated} // 0;
73             next if $current_update_date && $updated <= $current_update_date;
74              
75             my ($content, $verified, $error) = $self->{connection}->hmget("SANCTIONS::$source", qw/content verified error/)->@*;
76              
77             $self->{_data}->{$source}->{content} = decode_json_utf8($content // '[]');
78             $self->{_data}->{$source}->{verified} = $verified // 0;
79             $self->{_data}->{$source}->{updated} = $updated;
80             $self->{_data}->{$source}->{error} = $error // '';
81             $latest_update = $updated if $updated > $latest_update;
82             } catch ($e) {
83             $self->{_data}->{$source}->{content} = [];
84             $self->{_data}->{$source}->{updated} = 0;
85             $self->{_data}->{$source}->{verified} = 0;
86             $self->{_data}->{$source}->{error} = "Failed to load from Redis: $e";
87             }
88 0           }
89              
90 0           $self->{last_modification} = $latest_update;
91 0           $self->{last_data_load} = time;
92              
93 0 0         return $self->{_data} if $latest_update <= $self->{last_index};
94              
95 0           $self->_index_data();
96              
97 0           foreach my $sanctioned_name (keys $self->{_index}->%*) {
98 0           my @tokens = Data::Validate::Sanctions::_clean_names($sanctioned_name);
99 0           $self->{_sanctioned_name_tokens}->{$sanctioned_name} = \@tokens;
100 0           foreach my $token (@tokens) {
101 0           $self->{_token_sanctioned_names}->{$token}->{$sanctioned_name} = 1;
102             }
103             }
104              
105 0           return $self->{_data};
106             }
107              
108             sub _save_data {
109 0     0     my $self = shift;
110              
111 0           for my $source ($self->{sources}->@*) {
112 0           $self->{_data}->{$source}->{verified} = time;
113             $self->{connection}->hmset(
114             "SANCTIONS::$source",
115             updated => $self->{_data}->{$source}->{updated} // 0,
116             content => encode_json_utf8($self->{_data}->{$source}->{content} // []),
117             verified => $self->{_data}->{$source}->{verified},
118 0   0       error => $self->{_data}->{$source}->{error} // ''
      0        
      0        
119             );
120             }
121              
122 0           return;
123             }
124              
125             sub _default_sanction_file {
126 0     0     die 'Not applicable';
127             }
128              
129             1;
130             __END__
131              
132             =encoding utf-8
133              
134             =head1 NAME
135              
136             Data::Validate::Sanctions::Redis - An extension of L<Data::Validate::Sanctions::Redis> that stores sanction data in redis.
137              
138             =head1 SYNOPSIS
139             ## no critic
140             use Data::Validate::Sanctions::Redis;
141              
142             my $validator = Data::Validate::Sanctions::Redis->new(connection => $redis_read);
143              
144             # to validate clients by their name
145             print 'BAD' if $validator->is_sanctioned("$last_name $first_name");
146             # or by more profile data
147             print 'BAD' if $validator->get_sanctioned_info(first_name => $first_name, last_name => $last_name, date_of_birth => $date_of_birth)->{matched};
148              
149             # to update the sanction dataset (needs redis write access)
150             my $validator = Data::Validate::Sanctions::Redis->new(connection => $redis_write); ## no critic
151             $validator->update_data(eu_token => $token);
152              
153             # create object from the parent (factory) class
154             my $validator = Data::Validate::Sanctions->new(storage => 'redis', connection => $redis_write);
155              
156             =head1 DESCRIPTION
157              
158             Data::Validate::Sanctions::Redis is a simple validitor to validate a name against sanctions lists.
159             For more details about the sanction sources please refer to the parent module L<Data::Validate::Sanctions>.
160              
161             =head1 METHODS
162              
163             =head2 new
164              
165             Create the object with the redis object:
166              
167             my $validator = Data::Validate::Sanctions::Redis->new(connection => $redis);
168              
169             =head2 is_sanctioned
170              
171             Checks if the input profile info matches a sanctioned entity.
172             The arguments are the same as those of B<get_sanctioned_info>.
173              
174             It returns 1 if a match is found, otherwise 0.
175              
176             =cut
177              
178             =head2 get_sanctioned_info
179              
180             Tries to find a match a sanction entry matching the input profile args.
181             It takes arguments in two forms. In the new API, it takes a hashref containing the following named arguments:
182              
183             =over 4
184              
185             =item * first_name: first name
186              
187             =item * last_name: last name
188              
189             =item * date_of_birth: (optional) date of birth as a string or epoch
190              
191             =item * place_of_birth: (optional) place of birth as a country name or code
192              
193             =item * residence: (optional) name or code of the country of residence
194              
195             =item * nationality: (optional) name or code of the country of nationality
196              
197             =item * citizen: (optional) name or code of the country of citizenship
198              
199             =item * postal_code: (optional) postal/zip code
200              
201             =item * national_id: (optional) national ID number
202              
203             =item * passport_no: (oiptonal) passort number
204              
205             =back
206              
207             For backward compatibility it also supports the old API, taking the following args:
208              
209             =over 4
210              
211             =item * first_name: first name
212              
213             =item * last_name: last name
214              
215             =item * date_of_birth: (optional) date of birth as a string or epoch
216              
217             =back
218              
219             It returns a hash-ref containg the following data:
220              
221             =over 4
222              
223             =item - matched: 1 if a match was found; 0 otherwise
224              
225             =item - list: the source for the matched entry,
226              
227             =item - matched_args: a name-value hash-ref of the similar arguments,
228              
229             =item - comment: additional comments if necessary,
230              
231             =back
232              
233             =cut
234              
235             =head2 update_data
236              
237             Fetches latest versions of sanction lists, and updates corresponding sections of stored file, if needed
238              
239             =head2 last_updated
240              
241             Returns timestamp of when the latest list was updated.
242             If argument is provided - return timestamp of when that list was updated.
243              
244             =head2 _name_matches
245              
246             Pass in the client's name and sanctioned individual's name to see if they are similar or not
247              
248             =head1 AUTHOR
249              
250             Binary.com E<lt>fayland@binary.comE<gt>
251              
252             =head1 COPYRIGHT
253              
254             Copyright 2022- Binary.com
255              
256             =head1 LICENSE
257              
258             This library is free software; you can redistribute it and/or modify
259             it under the same terms as Perl itself.
260              
261             =head1 SEE ALSO
262              
263             L<Data::Validate::Sanctions>
264              
265             L<Data::Validate::Sanctions::Fetcher>
266              
267             =cut