538e3ff668457dbd4106d8f77d59eec3b8c2a2fc
[Business-OnlineThirdPartyPayment-PayPal.git] / PayPal.pm
1 package Business::OnlineThirdPartyPayment::PayPal;
2
3 use strict;
4 use base 'Business::OnlineThirdPartyPayment';
5 use vars qw($VERSION $DEBUG);
6
7 use strict;
8 use LWP;
9 use JSON;
10 use Net::PayPal; # for authentication, mostly
11 use URI;
12 use Cache::FileCache; # for ID strings
13
14 $VERSION = '0.01';
15
16 $DEBUG = 2;
17
18 sub set_defaults {
19   my $self = shift;
20   $self->build_subs(qw(order_number result_code error_message error_object
21                        cache_root));
22 }
23
24 sub client {
25   my $self = shift;
26   my %content = $self->content;
27   $self->{'client'} ||= 
28     Net::PayPal->new($content{'login'}, $content{'password'});
29 }
30
31 sub cache {
32   my $self = shift;
33   $self->{'cache'} ||=
34     Cache::FileCache->new(
35       { namespace          => 'PayPal',
36         default_expires_in => 3600,
37         cache_root         => $self->cache_root
38       } );
39 }
40
41 sub submit {
42   my $self = shift;
43   my %content = $self->content;
44   my $action = lc($content{'action'});
45   if ( $action eq 'authorization only' ) {
46     $self->create_payment;
47   } elsif ( $action eq 'post authorization' ) {
48     $self->execute_payment;
49   }
50 }
51
52 sub rest {
53   # a wrapper for the one in Net::PayPal, with better error handling
54   my ($self, $path, $request) = @_;
55   my $json_request = encode_json($request);
56   warn "REQUEST:\n$json_request\n\n" if $DEBUG >= 2;
57   my $raw_res =
58     $self->client->rest('POST', $path, $json_request, 1);
59   # last argument is "dump_responce" [sic]--tells Net::PayPal to dump the 
60   # HTTP::Response object instead of returning (part of) the error status
61   my $res;
62   # deal with certain ambiguities from Data::Dumper
63   { my $VAR1;
64     eval "$raw_res";
65     $res = $VAR1; }
66   if ( !defined($res) || !ref($res) || !$res->isa('HTTP::Response') ) {
67     die "Nonsense output from Net::PayPal REST call:\n$raw_res\n\n";
68   }
69   warn "RESPONSE:" . $res->status_line . "\n" . $res->content .  "\n\n"
70     if $DEBUG >= 2;
71
72   if ( $res->is_success ) {
73     $self->is_success(1);
74     return decode_json($res->content);
75   } else {
76     $self->is_success(0);
77     if ( $res->content ) {
78       my $response = decode_json($res->content);
79       $self->error_object($response);
80       my $error = sprintf("%s: %s",
81                     $response->{'name'}, $response->{'message'});
82       if ( $response->{'details'} ) {
83         foreach (@{ $response->{'details'} }) {
84           $error .= sprintf("\n%s:\t%s", $_->{'field'}, $_->{'issue'});
85         }
86       }
87       $self->error_message($error);
88       return $response;
89     } else {
90       $self->error_object({});
91       $self->error_message($res->status_line);
92       return {};
93     }
94   }
95 }
96
97 sub create_payment {
98   my $self = shift;
99   my %content = $self->content;
100   my $ref = $content{'reference'}
101     or die "reference required";
102   my $return_url = URI->new($content{'callback_url'})
103     or die "callback_url required";
104   $return_url->query_form( $return_url->query_form(), 'ref' => $ref );
105   my $cancel_url = URI->new($content{'cancel_url'})
106     or die "cancel_url required";
107   $cancel_url->query_form( $cancel_url->query_form(), 'ref' => $ref );
108   
109   my $request = 
110     {
111       intent  => 'sale',
112       payer   => {
113         payment_method  => 'paypal',
114       },
115       transactions => [
116         {
117           amount => {
118             total => $content{'amount'},
119             currency => ($content{'currency'} || 'USD'),
120           },
121           description => $content{'description'},
122         },
123       ],
124       redirect_urls => {
125         return_url => $return_url->as_string,
126         cancel_url => $cancel_url->as_string,
127       },
128     };
129
130   my $response = $self->rest('/v1/payments/payment', $request);
131
132   if ( $self->is_success ) {
133     $self->order_number($response->{'id'});
134     $self->cache->set( "REF-$ref" => $response->{'id'} );
135
136     my %links = map { $_->{rel} => $_->{href} } @{ $response->{'links'} };
137     $self->popup_url($links{'approval_url'});
138     # other links are "self", which is where we just posted,
139     # and "execute_url", which we can determine from the payment id
140   }
141 }
142
143 sub execute_payment {
144   my $self = shift;
145   my %content = $self->content;
146   # at this point the transaction is already set up
147   # (right? the workflow in this is horribly confusing...)
148   if ( !$self->authorization ) {
149     die "No authorization was received for this payment.\n";
150   }
151   my $request = { 'payer_id' => $self->authorization };
152   my $execute_path = '/v1/payments/payment/' . $self->order_number . '/execute';
153   $self->rest($execute_path, $request);
154 }
155
156 sub reference {
157   my $self = shift;
158   my $data = shift; # hashref of query params included in the callback URL
159
160   $self->authorization($data->{'PayerID'});
161   my $ref = $data->{'ref'};
162   my $id = $self->cache->get("REF-$ref");
163   if (!$id) {
164     $self->error_message("Payment reference '$ref' not found.");
165     $self->is_success(0);
166   }
167   $self->order_number($id);
168   $ref;
169 }
170
171 1;
172 __END__
173
174 =head1 NAME
175
176 Business::OnlineThirdPartyPayment::PayPal
177
178 =head1 DESCRIPTION
179
180 =head1 AUTHOR
181
182 Mark Wells <mark@freeside.biz>
183
184 =head1 SEE ALSO
185
186 perl(1). L<Business::OnlineThirdPartyPayment>.
187
188 =cut
189