File Coverage

blib/lib/Business/CPI/Gateway/PayPal.pm
Criterion Covered Total %
statement 21 70 30.0
branch 0 22 0.0
condition 0 26 0.0
subroutine 7 18 38.8
pod 4 4 100.0
total 32 140 22.8


line stmt bran cond sub pod time code
1             package Business::CPI::Gateway::PayPal;
2             # ABSTRACT: Business::CPI's PayPal driver
3              
4 3     3   102695 use Moo;
  3         25287  
  3         14  
5 3     3   5327 use DateTime;
  3         1209288  
  3         126  
6 3     3   1871 use DateTime::Format::Strptime;
  3         161227  
  3         19  
7 3     3   1722 use Business::CPI::Gateway::PayPal::IPN;
  3         12  
  3         94  
8 3     3   1343 use Business::PayPal::NVP;
  3         2942  
  3         78  
9 3     3   1440 use Data::Dumper;
  3         17595  
  3         218  
10 3     3   30 use Carp 'croak';
  3         7  
  3         3370  
11              
12             our $VERSION = '0.903'; # TRIAL VERSION
13              
14             extends 'Business::CPI::Gateway::Base';
15             with 'Business::CPI::Role::Gateway::FormCheckout';
16              
17             has sandbox => (
18             is => 'rw',
19             default => sub { 0 },
20             );
21              
22             has '+checkout_url' => (
23             default => sub {
24             my $sandbox = shift->sandbox ? 'sandbox.' : '';
25             return "https://www.${sandbox}paypal.com/cgi-bin/webscr";
26             },
27             lazy => 1,
28             );
29              
30             has '+currency' => (
31             default => sub { 'USD' },
32             );
33              
34             # TODO: make it lazy, and croak if needed
35             has api_username => (
36             is => 'ro',
37             required => 0,
38             );
39              
40             has api_password => (
41             is => 'ro',
42             required => 0,
43             );
44              
45             has signature => (
46             is => 'ro',
47             required => 0,
48             );
49              
50             has nvp => (
51             is => 'ro',
52             lazy => 1,
53             default => sub {
54             my $self = shift;
55              
56             return Business::PayPal::NVP->new(
57             test => {
58             user => $self->api_username,
59             pwd => $self->api_password,
60             sig => $self->signature,
61             },
62             live => {
63             user => $self->api_username,
64             pwd => $self->api_password,
65             sig => $self->signature,
66             },
67             branch => $self->sandbox ? 'test' : 'live'
68             );
69             }
70             );
71              
72             has date_format => (
73             is => 'ro',
74             lazy => 1,
75             default => sub {
76             DateTime::Format::Strptime->new(
77             pattern => '%Y-%m-%dT%H:%M:%SZ',
78             time_zone => 'UTC',
79             );
80             },
81             );
82              
83             sub notify {
84 0     0 1   my ( $self, $req ) = @_;
85              
86 0           my $ipn = Business::CPI::Gateway::PayPal::IPN->new(
87             query => $req,
88             gateway_url => $self->checkout_url,
89             );
90              
91 0 0         croak 'Invalid IPN request' unless $ipn->is_valid;
92              
93 0           my %vars = %{ $ipn->vars };
  0            
94              
95 0           $self->log->info("Received notification $vars{ipn_track_id} for transaction $vars{txn_id}.");
96              
97             my $r = {
98             payment_id => $vars{invoice},
99             status => $self->_interpret_status($vars{payment_status}),
100             gateway_transaction_id => $vars{txn_id},
101             exchange_rate => $vars{exchange_rate},
102             net_amount => ($vars{settle_amount} || $vars{mc_gross}) - ($vars{mc_fee} || 0),
103             amount => $vars{mc_gross},
104             fee => $vars{mc_fee},
105             date => $vars{payment_date},
106             payer => {
107             name => $vars{first_name} . ' ' . $vars{last_name},
108             email => $vars{payer_email},
109             }
110 0   0       };
      0        
111              
112 0 0         if ($self->log->is_debug) {
113 0           $self->log->debug("The notification data is:\n" . Dumper($r));
114 0           $self->log->debug("The request data is:\n" . Dumper($req));
115             }
116              
117 0           return $r;
118             }
119              
120             sub _interpret_status {
121 0     0     my ($self, $status) = @_;
122              
123 0           for ($status) {
124 0 0 0       /^Completed$/ ||
125             /^Processed$/ and return 'completed';
126              
127 0 0 0       /^Denied$/ ||
      0        
128             /^Expired$/ ||
129             /^Failed$/ and return 'failed';
130              
131 0 0 0       /^Voided$/ ||
      0        
132             /^Refunded$/ ||
133             /^Reversed$/ and return 'refunded';
134              
135 0 0         /^Pending$/ and return 'processing';
136             }
137              
138 0           return 'unknown';
139             }
140              
141             sub query_transactions {
142 0     0 1   my ($self, $info) = @_;
143              
144 0   0       my $final_date = $info->{final_date} || DateTime->now(time_zone => 'UTC');
145 0   0       my $initial_date = $info->{initial_date} || $final_date->clone->subtract(days => 30);
146              
147 0           my %search = $self->nvp->send(
148             METHOD => 'TransactionSearch',
149             STARTDATE => $initial_date->strftime('%Y-%m-%dT%H:%M:%SZ'),
150             ENDDATE => $final_date->strftime('%Y-%m-%dT%H:%M:%SZ'),
151             );
152              
153 0 0         if ($search{ACK} ne 'Success') {
154 0           croak "Error in the query: " . Dumper(\%search);
155             }
156              
157 0           while (my ($k, $v) = each %search) {
158 0 0         if ($k =~ /^L_TYPE(.*)$/) {
159 0           my $deleted_key = "L_TRANSACTIONID$1";
160 0 0         if (lc($v) ne 'payment') {
161 0           delete $search{$deleted_key};
162             }
163             }
164             }
165              
166 0           my @transaction_ids = map { $search{$_} } grep { /^L_TRANSACTIONID/ } keys %search;
  0            
  0            
167              
168 0           my @transactions = map { $self->get_transaction_details($_) } @transaction_ids;
  0            
169              
170             return {
171 0           current_page => 1,
172             results_in_this_page => scalar @transaction_ids,
173             total_pages => 1,
174             transactions => \@transactions,
175             };
176             }
177              
178             sub get_transaction_details {
179 0     0 1   my ( $self, $id ) = @_;
180              
181 0           my %details = $self->nvp->send(
182             METHOD => 'GetTransactionDetails',
183             TRANSACTIONID => $id,
184             );
185              
186 0 0         if ($details{ACK} ne 'Success') {
187 0           croak "Error in the details fetching: " . Dumper(\%details);
188             }
189              
190             return {
191             payment_id => $details{INVNUM},
192             status => lc $details{PAYMENTSTATUS},
193             amount => $details{AMT},
194             net_amount => $details{SETTLEAMT},
195             tax => $details{TAXAMT},
196             exchange_rate => $details{EXCHANGERATE},
197             date => $self->date_format->parse_datetime($details{ORDERTIME}),
198             buyer_email => $details{EMAIL},
199 0           };
200             }
201              
202             sub _checkout_form_main_map {
203             {
204 0     0     receiver_email => 'business',
205             currency => 'currency_code',
206             form_encoding => 'charset',
207             }
208             }
209              
210             sub _checkout_form_item_map {
211 0     0     my ($self, $i) = @_;
212              
213             {
214             id => "item_number_$i",
215             description => "item_name_$i",
216             price => "amount_$i",
217             quantity => "quantity_$i",
218             weight => {
219             name => "weight_$i",
220 0     0     coerce => sub { $_[0] }, # think about weight_unit
221             },
222 0           shipping => "shipping_$i",
223             shipping_additional => "shipping2_$i",
224             }
225             }
226              
227             sub _checkout_form_buyer_map {
228             {
229             email => 'email',
230             address_line1 => 'address1',
231             address_line2 => 'address2',
232             address_city => 'city',
233             address_state => 'state',
234             address_country => {
235             name => 'country',
236 0     0     coerce => sub { uc $_[0] },
237             },
238 0     0     address_zip_code => 'zip',
239             }
240             }
241              
242             sub _checkout_form_cart_map {
243             {
244 0     0     discount => 'discount_amount_cart',
245             handling => 'handling_cart',
246             tax => 'tax_cart',
247             }
248             }
249              
250             around _get_hidden_inputs_for_items => sub {
251             my ($orig, $self, $items) = @_;
252              
253             my $add_weight_unit = sub {
254             for (@$items) {
255             return 1 if $_->weight;
256             }
257             return 0;
258             }->();
259              
260             my @result = $self->$orig($items);
261              
262             if ($add_weight_unit) {
263             push @result, ( "weight_unit" => 'kgs' );
264             }
265              
266             return @result;
267             };
268              
269             sub get_hidden_inputs {
270 0     0 1   my ($self, $info) = @_;
271              
272             return (
273             # -- make paypal accept multiple items (cart)
274             cmd => '_ext-enter',
275             redirect_cmd => '_cart',
276             upload => 1,
277             # --
278              
279             invoice => $info->{payment_id},
280             no_shipping => $info->{buyer}->address_line1 ? 0 : 1,
281              
282             $self->_get_hidden_inputs_main(),
283             $self->_get_hidden_inputs_for_buyer($info->{buyer}),
284             $self->_get_hidden_inputs_for_items($info->{items}),
285 0 0         $self->_get_hidden_inputs_for_cart($info->{cart}),
286             );
287             }
288              
289             1;
290              
291             __END__
292              
293             =pod
294              
295             =encoding UTF-8
296              
297             =head1 NAME
298              
299             Business::CPI::Gateway::PayPal - Business::CPI's PayPal driver
300              
301             =head1 VERSION
302              
303             version 0.903
304              
305             =head1 ATTRIBUTES
306              
307             =head2 sandbox
308              
309             Boolean attribute to set whether it's running on sandbox or not. If it is, it
310             will post the form to the sandbox url in PayPal.
311              
312             =head2 api_username
313              
314             =head2 api_password
315              
316             =head2 signature
317              
318             =head2 nvp
319              
320             Business::PayPal::NVP object, built using the api_username, api_password and
321             signature attributes.
322              
323             =head2 date_format
324              
325             DateTime::Format::Strptime object, to format dates in a way PayPal understands.
326              
327             =head1 METHODS
328              
329             =head2 notify
330              
331             Translate IPN information from PayPal to a standard hash, the same way other
332             Business::CPI gateways do.
333              
334             =head2 query_transactions
335              
336             Searches transactions made by this account.
337              
338             =head2 get_transaction_details
339              
340             Get more data about a given transaction.
341              
342             =head2 get_hidden_inputs
343              
344             Get all the inputs to make a checkout form.
345              
346             =head1 SPONSORED BY
347              
348             Aware - L<http://www.aware.com.br>
349              
350             =head1 AUTHOR
351              
352             André Walker <andre@andrewalker.net>
353              
354             =head1 CONTRIBUTORS
355              
356             =over 4
357              
358             =item *
359              
360             Olaf Alders <olaf@wundersolutions.com>
361              
362             =item *
363              
364             Renato CRON <rentocron@cpan.org>
365              
366             =back
367              
368             =head1 COPYRIGHT AND LICENSE
369              
370             This software is copyright (c) 2013 by André Walker.
371              
372             This is free software; you can redistribute it and/or modify it under
373             the same terms as the Perl 5 programming language system itself.
374              
375             =cut