package Business::OnlineThirdPartyPayment::PayPal; use strict; use base 'Business::OnlineThirdPartyPayment'; use strict; use LWP; use JSON; use URI; use Cache::FileCache; use Crypt::CBC; our $VERSION = '0.01'; our $ENDPOINT_SANDBOX = 'api.sandbox.paypal.com'; our $ENDPOINT_LIVE = 'api.paypal.com'; our $DEBUG = 0; sub set_defaults { my $self = shift; my %args = @_; $self->build_subs(qw(username password error_object host access_token)); if ( $args{debug} ) { $DEBUG = $args{debug}; } } sub authenticate { my $self = shift; my $host = shift; die "PayPal client ID (username) must be configured\n" unless $self->username; die "PayPay client secret (password) must be configured\n" unless $self->password; $self->{cache} = Cache::FileCache->new( { cache_root => File::Spec->tmpdir, namespace => 'BOTP-PayPal' } ); $self->{cipher} = Crypt::CBC->new( -key => $self->password, -cipher => 'Blowfish' ); if ( my $token = $self->cache->get($self->username) ) { $self->access_token( $self->cipher->decrypt($token) ); } else { my $ua = LWP::UserAgent->new; my $auth_request = HTTP::Request->new(POST => "$host/v1/oauth2/token"); $auth_request->header('Accept' => 'application/json'); # documentation says application/json; it lies. $auth_request->header('Content-Type'=> 'application/x-www-form-urlencoded'); $auth_request->authorization_basic( $self->username, $self->password ); $auth_request->content('grant_type=client_credentials'); warn "Sending authentication request.\n" if $DEBUG; my $auth_response = $ua->request($auth_request); unless ( $auth_response->is_success ) { die "Authentication failed: ".$auth_response->status_line."\n". $auth_response->content; } warn "Authentication response:\n".$auth_response->content."\n\n" if $DEBUG > 2; my $hash = decode_json($auth_response->content); my $token = $hash->{access_token}; $self->access_token($token); $self->cache->set($self->username, $self->cipher->encrypt( $token ), $hash->{expires_in} - 5); } return $self->access_token; } sub cache { $_[0]->{cache} } sub cipher { $_[0]->{cipher} } sub rest { my ($self, $path, $content) = @_; my $host = $self->host; if ( $self->test_transaction ) { $host ||= $ENDPOINT_SANDBOX; } else { $host ||= $ENDPOINT_LIVE; } $host = 'https://'.$host; my $token = $self->access_token || $self->authenticate($host); my $ua = LWP::UserAgent->new; my $json_request = encode_json($content); warn "REQUEST:\n$json_request\n\n" if $DEBUG >= 2; my $url = $host . $path; warn "Sending to $url\n" if $DEBUG; my $request = HTTP::Request->new(POST => $url); $request->header('Accept' => 'application/json'); $request->header('Authorization' => "Bearer $token"); $request->header('Content-Type' => 'application/json'); $request->content($json_request); my $response = $ua->request($request); if ( !$response ) { die "API request failed: ".$response->status_line."\n". $response->content; } warn "RESPONSE:" . $response->status_line."\n".$response->content."\n\n" if $DEBUG >= 2; if ( $response->is_success ) { $self->is_success(1); return decode_json($response->content); } else { $self->is_success(0); if ( $response->content ) { my $error = decode_json($response->content); $self->error_object($error); my $error_message = sprintf("%s: %s", $error->{'name'}, $error->{'message'}); if ( $error->{'details'} ) { foreach (@{ $error->{'details'} }) { $error_message .= sprintf("\n%s:\t%s", $_->{'field'}, $_->{'issue'}); } } $self->error_message($error_message); return $error; } else { $self->error_object({}); $self->error_message($response->status_line); return {}; } } } sub create { my $self = shift; my %content = @_; my $return_url = URI->new($self->return_url) or die "return_url required"; my $cancel_url = URI->new($self->cancel_url) or die "cancel_url required"; 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->token($response->{'id'}); my %links = map { $_->{rel} => $_->{href} } @{ $response->{'links'} }; $self->redirect($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 { my $self = shift; my %params = @_; #my $payer_id = $params{'payer_id'} # documentation is wrong here my $payer_id = $params{'PayerID'} or die "cannot complete payment: missing PayerID"; #payer_id"; my $request = { 'payer_id' => $payer_id }; $self->order_number($self->token); my $execute_path = '/v1/payments/payment/' . $self->token. '/execute'; $self->rest($execute_path, $request); } 1; __END__ =head1 NAME Business::OnlineThirdPartyPayment::PayPal =head1 DESCRIPTION Business::OnlineThirdPartyPayment module for payments from PayPal accounts. =head1 AUTHOR Mark Wells Based in part on Net::PayPal, by Sherzod B. Ruzmetov . =head1 COPYRIGHT Copyright (c) 2013 Freeside Internet Services, Inc. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO perl(1). L. =cut