File Coverage

blib/lib/WebService/PayPal/NVP.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package WebService::PayPal::NVP;
2              
3 1     1   52589 use Moo;
  1         14005  
  1         5  
4 1     1   1935 use DateTime;
  1         350418  
  1         56  
5 1     1   715 use Encode qw( decode );
  1         7750  
  1         97  
6 1     1   522 use LWP::UserAgent ();
  1         30418  
  1         41  
7 1     1   611 use MooX::Types::MooseLike::Base qw( InstanceOf );
  1         4601  
  1         85  
8 1     1   9 use URI::Escape qw/uri_escape uri_escape_utf8 uri_unescape/;
  1         1  
  1         48  
9 1     1   259 use WebService::PayPal::NVP::Response;
  0            
  0            
10              
11             our $VERSION = '0.005';
12              
13             has 'errors' => (
14             is => 'rw',
15             isa => sub {
16             die "errors expects an array reference!\n"
17             unless ref $_[0] eq 'ARRAY';
18             },
19             default => sub { [] },
20             );
21              
22             has 'ua' => (
23             is => 'ro',
24             isa => InstanceOf['LWP::UserAgent'],
25             builder => '_build_ua'
26             );
27              
28             has 'user' => ( is => 'rw', required => 1 );
29             has 'pwd' => ( is => 'rw', required => 1 );
30             has 'sig' => ( is => 'rw', required => 1 );
31             has 'url' => ( is => 'rw' );
32             has 'branch' => ( is => 'rw', default => sub { 'sandbox' } );
33             has 'api_ver' => ( is => 'rw', default => sub { 51.0 } );
34              
35             sub BUILDARGS {
36             my ($class, %args) = @_;
37             # detect URL if it's missing
38             if (not $args{url}) {
39             $args{url} = "https://api-3t.sandbox.paypal.com/nvp"
40             if $args{branch} eq 'sandbox';
41              
42             $args{url} = "https://api-3t.paypal.com/nvp"
43             if $args{branch} eq 'live';
44             }
45              
46             return \%args;
47             }
48             sub _build_ua {
49             my $self = shift;
50              
51             my $lwp = LWP::UserAgent->new;
52             $lwp->agent("p-Webservice-PayPal-NVP/${VERSION}");
53             return $lwp;
54             }
55              
56             sub _do_request {
57             my ($self, $args) = @_;
58              
59             my $authargs = {
60             user => $self->user,
61             pwd => $self->pwd,
62             signature => $self->sig,
63             version => $args->{version}||$self->api_ver,
64             subject => $args->{subject}||'',
65             };
66              
67             my $allargs = { %$authargs, %$args };
68             my $content = $self->_build_content( $allargs );
69             my $res = $self->ua->post(
70             $self->url,
71             'Content-Type' => 'application/x-www-form-urlencoded',
72             Content => $content,
73             );
74              
75             unless ($res->code == 200) {
76             $self->errors(["Failure: " . $res->code . ": " . $res->message]);
77             return;
78             }
79              
80             my $resp = { map { decode( 'UTF-8', uri_unescape($_) ) }
81             map { split '=', $_, 2 }
82             split '&', $res->content };
83              
84             my $res_object = WebService::PayPal::NVP::Response->new(
85             branch => $self->branch,
86             raw => $resp
87             );
88             ;
89             if ($resp->{ACK} ne 'Success') {
90             $res_object->errors([]);
91             my $i = 0;
92             while(my $err = $resp->{"L_LONGMESSAGE${i}"}) {
93             push @{$res_object->errors},
94             $resp->{"L_LONGMESSAGE${i}"};
95             $i += 1;
96             }
97              
98             $res_object->success(0);
99             }
100             else {
101             $res_object->success(1);
102             }
103              
104             {
105             no strict 'refs';
106             no warnings 'redefine';
107             foreach my $key (keys %$resp) {
108             my $val = $resp->{$key};
109             my $lc_key = lc $key;
110             if ($lc_key eq 'timestamp') {
111             if ($val =~ /(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)Z/) {
112             my ($day, $month, $year, $hour, $min, $sec)
113             = ($3, $2, $1, $4, $5, $6);
114              
115             $val = DateTime->new(
116             year => $year,
117             month => $month,
118             day => $day,
119             hour => $hour,
120             minute => $min,
121             second => $sec,
122             );
123             }
124             }
125             *{"WebService::PayPal::NVP::Response::$lc_key"} = sub {
126             return $val;
127             };
128             }
129             }
130             return $res_object;
131             }
132              
133             sub _build_content {
134             my ($self, $args) = @_;
135             my @args;
136             for my $key (keys %$args) {
137             $args->{$key} = defined $args->{$key} ? $args->{$key} : '';
138             push @args,
139             uc(uri_escape($key)) . '=' . uri_escape_utf8($args->{$key});
140             }
141              
142             return (join '&', @args) || '';
143             }
144              
145             sub has_errors {
146             my $self = shift;
147             return scalar @{$self->errors} > 0;
148             }
149              
150             sub set_express_checkout {
151             my ($self, $args) = @_;
152             $args->{method} = 'SetExpressCheckout';
153             $self->_do_request($args);
154             }
155              
156             sub do_express_checkout_payment {
157             my ($self, $args) = @_;
158             $args->{method} = 'DoExpressCheckoutPayment';
159             $self->_do_request($args);
160             }
161              
162             sub get_express_checkout_details {
163             my ($self, $args) = @_;
164             $args->{method} = 'GetExpressCheckoutDetails';
165             $self->_do_request($args);
166             }
167              
168             sub do_direct_payment {
169             my ($self, $args) = @_;
170             $args->{method} = 'DoDirectPayment';
171             $self->_do_request($args);
172             }
173              
174             sub create_recurring_payments_profile {
175             my ($self, $args) = @_;
176             $args->{method} = 'CreateRecurringPaymentsProfile';
177             $self->_do_request($args);
178             }
179              
180             sub get_recurring_payments_profile_details {
181             my ($self, $args) = @_;
182             $args->{method} = 'GetRecurringPaymentsProfileDetails';
183             $self->_do_request($args);
184             }
185              
186             sub get_transaction_details {
187             my ($self, $args) = @_;
188             $args->{method} = 'GetTransactionDetails';
189             $self->_do_request($args);
190             }
191              
192             sub manage_recurring_payments_profile_status {
193             my ($self, $args) = @_;
194             $args->{method} = 'ManageRecurringPaymentsProfileStatus';
195             $self->_do_request($args);
196             }
197              
198             sub mass_pay {
199             my ($self, $args) = @_;
200             $args->{method} = 'MassPay';
201             $self->_do_request($args);
202             }
203              
204             sub refund_transaction {
205             my ($self, $args) = @_;
206             $args->{method} = 'RefundTransaction';
207             $self->_do_request($args);
208             }
209              
210             sub transaction_search {
211             my ( $self, $args ) = @_;
212             $args->{method} = 'TransactionSearch';
213             $self->_do_request($args);
214             }
215              
216             1;
217             __END__
218              
219             =head1 NAME
220              
221             WebService::PayPal::NVP - PayPal NVP API
222              
223             =head1 DESCRIPTION
224              
225             A pure object oriented interface to PayPal's NVP API (Name-Value Pair). A lot of the logic in this module was taken from L<Business::PayPal::NVP>. I re-wrote it because it wasn't working with Catalyst adaptors and I couldn't save instances of it in Moose-type accessors. Otherwise it worked fine. So if you don't need that kind of support you should visit L<Business::PayPal::NVP>!.
226             Another difference with this module compared to Business::PayPal::NVP is that the keys may be passed as lowercase. Also, a response will return a WebService::PayPal::NVP::Response object where the response values are methods. Timestamps will automatically be converted to DateTime objects for your convenience.
227              
228              
229             =head1 SYNOPSIS
230              
231             my $nvp = WebService::PayPal::NVP->new(
232             user => 'user.tld',
233             pwd => 'xxx',
234             sig => 'xxxxxxx',
235             branch => 'sandbox',
236             );
237              
238             my $res = $nvp->set_express_checkout({
239             DESC => 'Payment for something cool',
240             AMT => 25.00,
241             CURRENCYCODE => 'GBP',
242             PAYMENTACTION => 'Sale',
243             RETURNURL => "http://returnurl.tld",
244             CANCELURL => "http//cancelurl.tld",
245             LANDINGPAGE => 'Login',
246             ADDOVERRIDE => 1,
247             SHIPTONAME => "Customer Name",
248             SHIPTOSTREET => "7 Customer Street",
249             SHIPTOSTREET2 => "",
250             SHIPTOCITY => "Town",
251             SHIPTOZIP => "Postcode",
252             SHIPTOEMAIL => "customer\@example.com",
253             SHIPTOCOUNTRYCODE => 'GB',
254             });
255              
256             if ($res->success) {
257             # timestamps turned into DateTime objects
258             say "Response received at "
259             . $res->timestamp->dmy . " "
260             . $res->timestamp->hms(':');
261              
262             say $res->token;
263              
264             for my $arg ($res->args) {
265             if ($res->has_arg($arg)) {
266             say "$arg => " . $res->$arg;
267             }
268             }
269              
270             # get a redirect uri to paypal express checkout
271             # the Response object will automatically detect if you have
272             # live or sandbox and return the appropriate url for you
273             if (my $redirect_user_to = $res->express_checkout_uri) {
274             $web_framework->redirect( $redirect_user_to );
275             }
276             }
277             else {
278             say $_
279             for @{$res->errors};
280             }
281              
282             =head1 METHODS
283              
284             =head2 api_ver
285              
286             The version of PayPal's NVP API which you would like to use. Defaults to 51.
287              
288             =head2 errors
289              
290             Returns an C<ArrayRef> of errors. The ArrayRef is empty when there are no
291             errors.
292              
293             =head2 has_errors
294              
295             Returns true if C<errors()> is non-empty.
296              
297             =head2 create_recurring_payments_profile( $HashRef )
298              
299             =head2 do_direct_payment( $HashRef )
300              
301             =head2 do_express_checkout_payment( $HashRef )
302              
303             =head2 get_express_checkout_details( $HashRef )
304              
305             =head2 get_recurring_payments_profile_details( $HashRef )
306              
307             =head2 get_transaction_details( $HashRef )
308              
309             =head2 manage_recurring_payments_profile_status( $HashRef )
310              
311             =head2 mass_pay( $HashRef )
312              
313             =head2 refund_transaction( $HashRef )
314              
315             =head2 set_express_checkout( $HashRef )
316              
317             =head2 transaction_search( $HashRef )
318              
319             =head2 ua( LWP::UserAgent->new( ... ) )
320              
321             This method allows you to provide your own UserAgent. This object must be of
322             the L<LWP::UserAgent> family, so L<WWW::Mechanize> modules will also work.
323              
324             =head2 url
325              
326             The PayPal URL to use for requests. This can be helpful when mocking requests.
327             Defaults to PayPals production or sandbox URL as appropriate.
328              
329             =head1 TESTING
330              
331             The main test will not work out of the box, because obviously it needs some sandbox/live api details before it can proceed. Simply create an C<auth.yml> file in the distribution directory with the following details:
332              
333             ---
334             user: 'api_user'
335             pass: 'api password'
336             sig: 'api signature'
337             branch: 'sandbox or live'
338              
339             If it detects the file missing completely it will just skip every test. Otherwise, it will only fail if any of the required information is missing.
340              
341             =head1 AUTHOR
342              
343             Brad Haywood <brad@geeksware.com>
344              
345             =head1 CREDITS
346              
347             A lot of this module was taken from L<Business::PayPal::NVP> by Scott Wiersdorf.
348             It was only rewritten in order to work properly in L<Catalyst::Model::Adaptor>.
349              
350             =head2 THANKS
351              
352             A huge thanks to Olaf Alders (OALDERS) for all of his useful pull requests!
353              
354             =head1 LICENSE
355              
356             You may distribute this code under the same terms as Perl itself.
357              
358             =cut