File Coverage

blib/lib/Business/CPI/Gateway/PayPal.pm
Criterion Covered Total %
statement 44 70 62.8
branch 11 22 50.0
condition 17 26 65.3
subroutine 14 18 77.7
pod 4 4 100.0
total 90 140 64.2


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   54957 use Moo;
  3         75904  
  3         20  
5 3     3   11415 use DateTime;
  3         666555  
  3         135  
6 3     3   3722 use DateTime::Format::Strptime;
  3         34993  
  3         254  
7 3     3   2629 use Business::CPI::Gateway::PayPal::IPN;
  3         11  
  3         125  
8 3     3   3099 use Business::PayPal::NVP;
  3         4654  
  3         104  
9 3     3   3413 use Data::Dumper;
  3         27406  
  3         245  
10 3     3   31 use Carp 'croak';
  3         7  
  3         5949  
11              
12             our $VERSION = '0.904'; # 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 1     1 1 35 my ( $self, $req ) = @_;
85              
86 1         6 my $ipn = Business::CPI::Gateway::PayPal::IPN->new(
87             query => $req,
88             gateway_url => $self->checkout_url,
89             );
90              
91 1 50       9 croak 'Invalid IPN request' unless $ipn->is_valid;
92              
93 1         3 my %vars = %{ $ipn->vars };
  1         29  
94              
95 1         54 $self->log->info("Received notification $vars{ipn_track_id} for transaction $vars{txn_id}.");
96              
97 1   33     7 my $r = {
      50        
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             };
111              
112 1 50       10 if ($self->log->is_debug) {
113 0         0 $self->log->debug("The notification data is:\n" . Dumper($r));
114 0         0 $self->log->debug("The request data is:\n" . Dumper($req));
115             }
116              
117 1         69 return $r;
118             }
119              
120             sub _interpret_status {
121 12     12   1548 my ($self, $status) = @_;
122              
123 12         28 for ($status) {
124 12 100 100     114 /^Completed$/ ||
125             /^Processed$/ and return 'completed';
126              
127 9 100 100     80 /^Denied$/ ||
      100        
128             /^Expired$/ ||
129             /^Failed$/ and return 'failed';
130              
131 6 100 100     55 /^Voided$/ ||
      100        
132             /^Refunded$/ ||
133             /^Reversed$/ and return 'refunded';
134              
135 3 100       17 /^Pending$/ and return 'processing';
136             }
137              
138 2         10 return 'unknown';
139             }
140              
141             sub query_transactions {
142 0     0 1 0 my ($self, $info) = @_;
143              
144 0   0     0 my $final_date = $info->{final_date} || DateTime->now(time_zone => 'UTC');
145 0   0     0 my $initial_date = $info->{initial_date} || $final_date->clone->subtract(days => 30);
146              
147 0         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       0 if ($search{ACK} ne 'Success') {
154 0         0 croak "Error in the query: " . Dumper(\%search);
155             }
156              
157 0         0 while (my ($k, $v) = each %search) {
158 0 0       0 if ($k =~ /^L_TYPE(.*)$/) {
159 0         0 my $deleted_key = "L_TRANSACTIONID$1";
160 0 0       0 if (lc($v) ne 'payment') {
161 0         0 delete $search{$deleted_key};
162             }
163             }
164             }
165              
166 0         0 my @transaction_ids = map { $search{$_} } grep { /^L_TRANSACTIONID/ } keys %search;
  0         0  
  0         0  
167              
168 0         0 my @transactions = map { $self->get_transaction_details($_) } @transaction_ids;
  0         0  
169              
170             return {
171 0         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 0 my ( $self, $id ) = @_;
180              
181 0         0 my %details = $self->nvp->send(
182             METHOD => 'GetTransactionDetails',
183             TRANSACTIONID => $id,
184             );
185              
186 0 0       0 if ($details{ACK} ne 'Success') {
187 0         0 croak "Error in the details fetching: " . Dumper(\%details);
188             }
189              
190             return {
191 0         0 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             };
200             }
201              
202             sub _checkout_form_main_map {
203             {
204 1     1   533 receiver_id => 'business',
205             currency => 'currency_code',
206             form_encoding => 'charset',
207             }
208             }
209              
210             sub _checkout_form_item_map {
211 1     1   13 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   0 coerce => sub { $_[0] }, # think about weight_unit
221             },
222 1         18 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   0 coerce => sub { uc $_[0] },
237             },
238 1     1   98 address_zip_code => 'zip',
239             }
240             }
241              
242             sub _checkout_form_cart_map {
243             {
244 1     1   12 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 1     1 1 352668 my ($self, $info) = @_;
271              
272             return (
273             # -- make paypal accept multiple items (cart)
274 1 50       8 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             $self->_get_hidden_inputs_for_cart($info->{cart}),
286             );
287             }
288              
289             1;
290              
291             __END__