File Coverage

blib/lib/WebService/BR/Vindi.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 17 19 89.4


line stmt bran cond sub pod time code
1             package WebService::BR::Vindi;
2              
3 1     1   14036 use 5.010001;
  1         4  
4 1     1   5 use strict;
  1         2  
  1         20  
5 1     1   5 use warnings;
  1         5  
  1         51  
6              
7             our @ISA = qw();
8              
9             our $VERSION = '0.01';
10              
11              
12             # Preloaded methods go here.
13 1     1   460 use MIME::Base64;
  1         574  
  1         55  
14 1     1   215 use JSON::XS;
  0            
  0            
15             use utf8;
16              
17             # Configura as URLs de destino baseado na tag raiz
18             $WebService::BR::Vindi::Target = {
19             prod => {
20             '*' => 'https://app.vindi.com.br:443/api/v1',
21             },
22             };
23              
24             # Error messages translator
25             $WebService::BR::Vindi::ErrorMessages = {
26             # First is the id
27             global => {
28             invalid_parameter => {
29             __look_for__ => 'parameter',
30             payment_company_code => 'Número do Cartão de Crédito parece ser inválido.',
31             card_expiration => 'Data de validade do Cartão inválida: %s.',
32             card_number => 'Número do Cartão %s.',
33             merchant => 'Erro interno: %s.',
34             },
35             },
36             };
37              
38             #
39             # @param $type Tipo da requisição (nome do end-point)
40             # @return $URL URL de destino a utilizar para a requisição
41             #
42             sub _getURL {
43             my $class = shift;
44             if ( exists( $WebService::BR::Vindi::Target->{$class->{target}}->{$_[0]} ) ) {
45             return $WebService::BR::Vindi::Target->{$class->{target}}->{$_[0]};
46             } else {
47             return $WebService::BR::Vindi::Target->{$class->{target}}->{'*'}."/$_[0]";
48             }
49             }
50              
51             #
52             # Contrutor
53             #
54             sub new {
55             my $self = shift;
56              
57             my $class = $#_ == 0 && ref($_[0]) eq 'HASH' ? $_[0] : { @_ };
58              
59             require LWP::UserAgent;
60             require HTTP::Request::Common;
61             require IO::Socket::SSL;
62              
63             #IO::Socket::SSL::set_ctx_defaults(
64             # SSL_verify_mode => 0,
65             # SSL_version => "TLSv1"
66             #);
67              
68             $class->{target} ||= 'prod';
69             $class->{timeout} ||= 120;
70             $class->{charset} ||= 'UTF-8';
71             $class->{api_key} ||= '';
72              
73             $class->{ua} ||= LWP::UserAgent->new( agent => 'WebService-BR-Vindi.pm',
74             timeout => $class->{timeout} || 120 );
75             $class->{ua}->ssl_opts( verify_hostname => 0 );
76             $class->{ua}->env_proxy;
77              
78             # JSON helper
79             $class->{json} = JSON::XS->new->allow_nonref->utf8;
80              
81              
82             bless( $class, $self );
83             }
84              
85             #
86             # Make the next request (only the first next request) failsafe: if it fails, send it to the Message Bus.
87             #
88             sub failsafe {
89             die 'You need to specify the "app" parameter to the constructor to use this feature (this is a proprietary implementation).' if !$_[0]->{app};
90             $_[0]->{failsafe} = 1;
91             $_[0];
92             }
93              
94             sub ua { shift->{ua} }
95             sub json { shift->{json} }
96             sub app { shift->{app} }
97              
98              
99              
100             #
101             # Faz uma requisição
102             #
103             # @param $Endpoint Tipo da Requisição (equivalente à tag raiz, exemplo: "requisicao-transacao")
104             # \%Params HASH a enviar com os dados (será convertido em JSON string).
105             # @param $Endpoint Tipo da Requisição (equivalente à tag raiz, exemplo: "requisicao-transacao")
106             # $Params JSON string a enviar com os dados.
107             #
108             sub post {
109             my $class = shift;
110             $class->{response} = $class->request( 'post', @_ );
111             }
112             sub get {
113             my $class = shift;
114             $class->{response} = $class->request( 'get', @_ );
115             }
116             sub put {
117             my $class = shift;
118             $class->{response} = $class->request( 'put', @_ );
119             }
120             sub delete {
121             my $class = shift;
122             $class->{response} = $class->request( 'delete', @_ );
123             }
124              
125              
126             #
127             # Custom helpers
128             #
129              
130             #
131             # Delete all objects of a given kind
132             #
133             # @param $endpoint Endpoint, optionally with a query filter specification of what objects to delete
134             #
135             sub delete_all {
136             my $class = shift;
137             my $endpoint = shift;
138              
139             my ( $object, $query ) = split( /\?/, $endpoint );
140              
141             # Get objects that match the query
142             my $all = $class->get( $endpoint );
143              
144             my @deleted = ();
145            
146             # If we got data, delete one by one
147             if ( $all && $all->{$object} && $#{$all->{$object}} > -1 ) {
148             for my $row ( @{ $all->{$object} } ) {
149              
150             warn "delete( $object/$row->{id} )..." if $class->{debug};
151              
152             my $res = $class->delete( "$object/$row->{id}" );
153              
154             if ( $res && ref( $res ) && $res->{ErrorStatus} ) {
155             } else {
156             push( @deleted, $row->{id} );
157             }
158            
159             }
160             }
161              
162             return { deleted => [ @deleted ] };
163             }
164              
165             #
166             # Retorna resposata da última requisição como HASHREF
167             #
168             # @return \%Response
169             #
170             sub response {
171             if ( $_[0]->{response}->is_success ) {
172             $_[0]->{json}->decode( $_[0]->{response}->decoded_content );
173             } else {
174             { ErrorStatus => $_[0]->{response}->status_line };
175             }
176             }
177              
178             #
179             # Retorna resposata da última requisição como HASHREF
180             #
181             # @return \%Response
182             #
183             sub responseAsJSON {
184             if ( $_[0]->{response}->is_success ) {
185             $_[0]->{response}->decoded_content;
186             } else {
187             $_[0]->{json}->encode( { ErrorStatus => $_[0]->{response}->status_line } );
188             }
189             }
190              
191             #
192             # Realiza uma requisição de dados. Uso interno.
193             #
194             # @see #get #post #put #delete
195             # @param $Method get post etc
196             # $Endpoint script name, relative
197             # $Data Dados a enviar (em geral o JSON)
198             # \%Headers Cabeçalhos a enviar (nenhum necessário em geral)
199             #
200             sub request {
201             my $class = shift;
202             my $Method = shift || 'get';
203             my $Endpoint = shift;
204             my $Content = shift;
205             my $Headers = shift || {};
206              
207            
208             # Encode $Content in to JSON string if needed
209             $Content = ref( $Content ) ?
210             $class->{json}->encode( $Content ) :
211             $Content;
212              
213              
214             ( $Endpoint, my $QueryStr ) = split( /\?/, $Endpoint );
215             my $URL = $class->_getURL( $Endpoint ).( $QueryStr ? "?$QueryStr" : '' );
216              
217             warn "REQUEST [$URL]: ".$Content if $class->{debug};
218            
219             my $res;
220              
221             # POST
222             $res = $class->{ua}->$Method(
223             $URL,
224             # Content_Type => 'application/x-www-form-urlencoded',
225             Content_Type => 'application/json',
226             Content_Charset => 'text/json;charset=UTF-8',
227             Authorization => "Basic ".MIME::Base64::encode_base64( $class->{api_key} ),
228             %{$Headers},
229             Content => $Content,
230             );
231              
232             # Debug only
233             warn "RESPONSE CODE: ".$res->status_line if $class->{debug};
234             warn "RESPOSSE DATA: ".$res->decoded_content if $class->{debug};
235              
236             # return $res;
237              
238              
239              
240              
241             # Sucesso
242             if ( $res->is_success ) {
243             $class->{failsafe} = 0;
244              
245             return $class->translateErrors( $class->{json}->decode( $res->decoded_content ) );
246              
247             # Erro
248             } else {
249              
250             # Send it to Message Queue if on failsafe mode
251             if ( $class->{failsafe} ) {
252             $class->app->message->post(
253             'Vindi',
254             { METHOD => $Method,
255             PATH => $Endpoint.( $QueryStr ? "?$QueryStr" : '' ),
256             DATA => $Content || undef,
257             LOG => $res->decoded_content || '',
258             });
259             }
260              
261             my $err = { ErrorStatus => $res->status_line };
262             eval {
263             my $json = $class->{json}->decode( $res->decoded_content );
264             $err = $class->translateErrors( $json ) if $json->{errors} && $json->{errors}->[0];
265             };
266            
267             $class->{failsafe} = 0;
268             return $err;
269             }
270              
271             }
272              
273              
274             #
275             # Translete Vindi error messages into human friendly messages
276             #
277             sub translateErrors {
278             my $class = shift;
279             my $json = shift;
280              
281             # Internal/unknown error?
282             if ( !ref( $json ) ) {
283             warn $json if $class->{debug};
284             return { ErrorStatus => 'Erro ao comunicar com a operadora de Cobrança. Por favor verifique os dados passados e tente novamente mais tarde.' };
285              
286             # Translate it
287             } elsif ( $json->{errors} ) {
288             my $map = $WebService::BR::Vindi::ErrorMessages->{global};
289             my $errors = {};
290              
291             # Translate error by error
292             for my $error ( @{ $json->{errors} } ) {
293             # By "id"
294             if ( # Error id found on error map
295             $map->{ $error->{id} } &&
296             # Error object has the parameter it looks for
297             $error->{ $map->{ $error->{id} }->{__look_for__} } &&
298             # The value of the parameter of the error has an entry on the map to translate it
299             $map->{ $error->{id} }->{ $error->{ $map->{ $error->{id} }->{__look_for__} } } ) {
300              
301             my $uid = $error->{id}.':'.$error->{ $map->{ $error->{id} }->{__look_for__} };
302             $errors->{ $uid } ||= { messages => [], error => $map->{ $error->{id} }->{ $error->{ $map->{ $error->{id} }->{__look_for__} } } };
303             push( @{ $errors->{ $uid }->{messages} }, $error->{message} ) if $#{$errors->{ $uid }->{messages}} == -1 || $errors->{ $uid }->{messages}->[-1] ne $error->{message};
304             }
305             }
306              
307             # We have a translation
308             if ( $#{ [ keys %{$errors} ] } > -1 ) {
309             return { ErrorStatus => join( '; ', map { sprintf( $_->{error}, join( ', ', @{$_->{messages}} ) ) } values( %{$errors} ) ),
310             errors => $json->{errors} };
311              
312             # No error found on ErrorMessage map
313             } else {
314             return { ErrorStatus => "Erro ao comunicar com a operadora de Cobrança ($json->{errors}->[0]->{id}: $json->{errors}->[0]->{parameter}: $json->{errors}->[0]->{message}). Por favor verifique os dados passados e tente novamente mais tarde.",
315             errors => $json->{errors} };
316             }
317            
318             # No errors, pass it on.
319             } else {
320             return $json;
321             }
322            
323             }
324              
325             #
326             # Retorna o nome da badeira do cartão baseado no número
327             #
328             # @param $numero Numero do cartao
329             # @return $bandeira Badeira do cartão, já no formato Vindi a ser enviado no JSON.
330             #
331             sub cardtype {
332             my $number = $_[1];
333              
334             my $type = 'unknown';
335              
336             if ( $number =~ /^4[0-9]{12}(?:[0-9]{3})/ ) {
337             $type = 'visa';
338              
339             } elsif ( $number =~ /^5[1-5][0-9]{14}/ ) {
340             $type = 'mastercard';
341              
342             } elsif ( $number =~ /^3[47][0-9]{13}/ ) {
343             $type = 'amex';
344              
345             } elsif ( $number =~ /^3(?:0[0-5]|[68][0-9])[0-9]{11}/ ) {
346             $type = 'diners';
347              
348             } elsif ( $number =~ /^6(?:011|5[0-9]{2})[0-9]{12}/ ) {
349             $type = 'discover';
350              
351             } elsif ( $number =~ /^(?:2131|1800|35\d{3})\d{11}/ ) {
352             $type = 'jcb';
353             }
354              
355             # TODO: "elo" e "aura"
356            
357             return $type;
358             }
359              
360             1;
361              
362             __END__