--- /dev/null
+package Business::OnlineThirdPartyPayment::PayPal;
+
+use strict;
+use base 'Business::OnlineThirdPartyPayment';
+use vars qw($VERSION $DEBUG);
+
+use strict;
+use LWP;
+use JSON;
+use Net::PayPal; # for authentication, mostly
+use URI;
+use Cache::FileCache; # for ID strings
+
+$VERSION = '0.01';
+
+$DEBUG = 2;
+
+sub set_defaults {
+ my $self = shift;
+ $self->build_subs(qw(order_number result_code error_message error_object
+ cache_root));
+}
+
+sub client {
+ my $self = shift;
+ my %content = $self->content;
+ $self->{'client'} ||=
+ Net::PayPal->new($content{'login'}, $content{'password'});
+}
+
+sub cache {
+ my $self = shift;
+ $self->{'cache'} ||=
+ Cache::FileCache->new(
+ { namespace => 'PayPal',
+ default_expires_in => 3600,
+ cache_root => $self->cache_root
+ } );
+}
+
+sub submit {
+ my $self = shift;
+ my %content = $self->content;
+ my $action = lc($content{'action'});
+ if ( $action eq 'authorization only' ) {
+ $self->create_payment;
+ } elsif ( $action eq 'post authorization' ) {
+ $self->execute_payment;
+ }
+}
+
+sub rest {
+ # a wrapper for the one in Net::PayPal, with better error handling
+ my ($self, $path, $request) = @_;
+ my $json_request = encode_json($request);
+ warn "REQUEST:\n$json_request\n\n" if $DEBUG >= 2;
+ my $raw_res =
+ $self->client->rest('POST', $path, $json_request, 1);
+ # last argument is "dump_responce" [sic]--tells Net::PayPal to dump the
+ # HTTP::Response object instead of returning (part of) the error status
+ my $res;
+ # deal with certain ambiguities from Data::Dumper
+ { my $VAR1;
+ eval "$raw_res";
+ $res = $VAR1; }
+ if ( !defined($res) || !ref($res) || !$res->isa('HTTP::Response') ) {
+ die "Nonsense output from Net::PayPal REST call:\n$raw_res\n\n";
+ }
+ warn "RESPONSE:" . $res->status_line . "\n" . $res->content . "\n\n"
+ if $DEBUG >= 2;
+
+ if ( $res->is_success ) {
+ $self->is_success(1);
+ return decode_json($res->content);
+ } else {
+ $self->is_success(0);
+ if ( $res->content ) {
+ my $response = decode_json($res->content);
+ $self->error_object($response);
+ my $error = sprintf("%s: %s",
+ $response->{'name'}, $response->{'message'});
+ if ( $response->{'details'} ) {
+ foreach (@{ $response->{'details'} }) {
+ $error .= sprintf("\n%s:\t%s", $_->{'field'}, $_->{'issue'});
+ }
+ }
+ $self->error_message($error);
+ return $response;
+ } else {
+ $self->error_object({});
+ $self->error_message($res->status_line);
+ return {};
+ }
+ }
+}
+
+sub create_payment {
+ my $self = shift;
+ my %content = $self->content;
+ my $ref = $content{'reference'}
+ or die "reference required";
+ my $return_url = URI->new($content{'callback_url'})
+ or die "callback_url required";
+ $return_url->query_form( $return_url->query_form(), 'ref' => $ref );
+ my $cancel_url = URI->new($content{'cancel_url'})
+ or die "cancel_url required";
+ $cancel_url->query_form( $cancel_url->query_form(), 'ref' => $ref );
+
+ my $request =
+ {
+ intent => 'sale',
+ payer => {
+ payment_method => 'paypal',
+ },
+ transactions => [
+ {
+ amount => {
+ total => $content{'amount'},
+ currency => ($content{'currency'} || 'USD'),
+ },
+ description => $content{'description'},
+ },
+ ],
+ redirect_urls => {
+ return_url => $return_url->as_string,
+ cancel_url => $cancel_url->as_string,
+ },
+ };
+
+ my $response = $self->rest('/v1/payments/payment', $request);
+
+ if ( $self->is_success ) {
+ $self->order_number($response->{'id'});
+ $self->cache->set( "REF-$ref" => $response->{'id'} );
+
+ my %links = map { $_->{rel} => $_->{href} } @{ $response->{'links'} };
+ $self->popup_url($links{'approval_url'});
+ # other links are "self", which is where we just posted,
+ # and "execute_url", which we can determine from the payment id
+ }
+}
+
+sub execute_payment {
+ my $self = shift;
+ my %content = $self->content;
+ # at this point the transaction is already set up
+ # (right? the workflow in this is horribly confusing...)
+ if ( !$self->authorization ) {
+ die "No authorization was received for this payment.\n";
+ }
+ my $request = { 'payer_id' => $self->authorization };
+ my $execute_path = '/v1/payments/payment/' . $self->order_number . '/execute';
+ $self->rest($execute_path, $request);
+}
+
+sub reference {
+ my $self = shift;
+ my $data = shift; # hashref of query params included in the callback URL
+
+ $self->authorization($data->{'PayerID'});
+ my $ref = $data->{'ref'};
+ my $id = $self->cache->get("REF-$ref");
+ if (!$id) {
+ $self->error_message("Payment reference '$ref' not found.");
+ $self->is_success(0);
+ }
+ $self->order_number($id);
+ $ref;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Business::OnlineThirdPartyPayment::PayPal
+
+=head1 DESCRIPTION
+
+=head1 AUTHOR
+
+Mark Wells <mark@freeside.biz>
+
+=head1 SEE ALSO
+
+perl(1). L<Business::OnlineThirdPartyPayment>.
+
+=cut
+