1 package Business::OnlineThirdPartyPayment::PayPal;
4 use base 'Business::OnlineThirdPartyPayment';
13 our $VERSION = '0.01';
14 our $ENDPOINT_SANDBOX = 'api.sandbox.paypal.com';
15 our $ENDPOINT_LIVE = 'api.paypal.com';
22 $self->build_subs(qw(username password error_object host access_token));
24 $DEBUG = $args{debug};
32 die "PayPal client ID (username) must be configured\n"
33 unless $self->username;
34 die "PayPay client secret (password) must be configured\n"
35 unless $self->password;
37 $self->{cache} = Cache::FileCache->new(
38 { cache_root => File::Spec->tmpdir,
39 namespace => 'BOTP-PayPal' }
41 $self->{cipher} = Crypt::CBC->new( -key => $self->password,
42 -cipher => 'Blowfish' );
44 if ( my $token = $self->cache->get($self->username) ) {
45 $self->access_token( $self->cipher->decrypt($token) );
47 my $ua = LWP::UserAgent->new;
48 my $auth_request = HTTP::Request->new(POST => "$host/v1/oauth2/token");
49 $auth_request->header('Accept' => 'application/json');
50 # documentation says application/json; it lies.
51 $auth_request->header('Content-Type'=>
52 'application/x-www-form-urlencoded');
53 $auth_request->authorization_basic( $self->username, $self->password );
54 $auth_request->content('grant_type=client_credentials');
55 warn "Sending authentication request.\n" if $DEBUG;
56 my $auth_response = $ua->request($auth_request);
57 unless ( $auth_response->is_success ) {
58 die "Authentication failed: ".$auth_response->status_line."\n".
59 $auth_response->content;
61 warn "Authentication response:\n".$auth_response->content."\n\n"
63 my $hash = decode_json($auth_response->content);
64 my $token = $hash->{access_token};
65 $self->access_token($token);
66 $self->cache->set($self->username, $self->cipher->encrypt( $token ),
67 $hash->{expires_in} - 5);
69 return $self->access_token;
72 sub cache { $_[0]->{cache} }
74 sub cipher { $_[0]->{cipher} }
77 my ($self, $path, $content) = @_;
78 my $host = $self->host;
80 if ( $self->test_transaction ) {
81 $host ||= $ENDPOINT_SANDBOX;
83 $host ||= $ENDPOINT_LIVE;
85 $host = 'https://'.$host;
87 my $token = $self->access_token || $self->authenticate($host);
88 my $ua = LWP::UserAgent->new;
90 my $json_request = encode_json($content);
91 warn "REQUEST:\n$json_request\n\n" if $DEBUG >= 2;
93 my $url = $host . $path;
94 warn "Sending to $url\n" if $DEBUG;
96 my $request = HTTP::Request->new(POST => $url);
97 $request->header('Accept' => 'application/json');
98 $request->header('Authorization' => "Bearer $token");
99 $request->header('Content-Type' => 'application/json');
100 $request->content($json_request);
102 my $response = $ua->request($request);
104 die "API request failed: ".$response->status_line."\n".
107 warn "RESPONSE:" . $response->status_line."\n".$response->content."\n\n"
110 if ( $response->is_success ) {
111 $self->is_success(1);
112 return decode_json($response->content);
114 $self->is_success(0);
115 if ( $response->content ) {
116 my $error = decode_json($response->content);
117 $self->error_object($error);
118 my $error_message = sprintf("%s: %s",
119 $error->{'name'}, $error->{'message'});
120 if ( $error->{'details'} ) {
121 foreach (@{ $error->{'details'} }) {
122 $error_message .= sprintf("\n%s:\t%s", $_->{'field'}, $_->{'issue'});
125 $self->error_message($error_message);
128 $self->error_object({});
129 $self->error_message($response->status_line);
138 my $return_url = URI->new($self->return_url)
139 or die "return_url required";
140 my $cancel_url = URI->new($self->cancel_url)
141 or die "cancel_url required";
147 payment_method => 'paypal',
152 total => $content{'amount'},
153 currency => ($content{'currency'} || 'USD'),
155 description => $content{'description'},
159 return_url => $return_url->as_string,
160 cancel_url => $cancel_url->as_string,
164 my $response = $self->rest('/v1/payments/payment', $request);
166 if ( $self->is_success ) {
167 $self->token($response->{'id'});
169 my %links = map { $_->{rel} => $_->{href} } @{ $response->{'links'} };
170 $self->redirect($links{'approval_url'});
171 # other links are "self", which is where we just posted,
172 # and "execute_url", which we can determine from the payment id
179 #my $payer_id = $params{'payer_id'} # documentation is wrong here
180 my $payer_id = $params{'PayerID'}
181 or die "cannot complete payment: missing PayerID"; #payer_id";
183 my $request = { 'payer_id' => $payer_id };
184 $self->order_number($self->token);
185 my $execute_path = '/v1/payments/payment/' . $self->token. '/execute';
186 $self->rest($execute_path, $request);
194 Business::OnlineThirdPartyPayment::PayPal
198 Business::OnlineThirdPartyPayment module for payments from PayPal accounts.
202 Mark Wells <mark@freeside.biz>
204 Based in part on Net::PayPal, by Sherzod B. Ruzmetov <sherzodr@cpan.org>.
208 Copyright (c) 2013 Freeside Internet Services, Inc.
210 All rights reserved. This program is free software; you can redistribute
211 it and/or modify it under the same terms as Perl itself.
215 perl(1). L<Business::OnlineThirdPartyPayment>.