package WebGUI::Shop::PayDriver::PayPal::ExpressCheckout; =head1 LEGAL ------------------------------------------------------------------- WebGUI is Copyright 2001-2009 Plain Black Corporation. ------------------------------------------------------------------- Please read the legal notices (docs/legal.txt) and the license (docs/license.txt) that came with this distribution before using this software. ------------------------------------------------------------------- http://www.plainblack.com info@plainblack.com ------------------------------------------------------------------- =cut use strict; use base qw/WebGUI::Shop::PayDriver/; use LWP::UserAgent; use Tie::IxHash; use WebGUI::International; use WebGUI::Form; use URI::Escape; use URI::Split; use URI; use Readonly; use Data::Dumper; =head1 NAME WebGUI::Shop::PayDriver::PayPal =head1 DESCRIPTION Payment driver that talks to PayPal using the Express Checkout API =head1 SYNOPSIS # in webgui config file... "paymentDrivers" : [ "WebGUI::Shop::PayDriver::Cash", "WebGUI::Shop::PayDriver::PayPal", ... ], =head1 METHODS The following methods are available from this class: =cut Readonly my $I18N => 'PayDriver_ExpressCheckout'; #------------------------------------------------------------------- =head2 apiUrl Returns the URL for the PayPal API (or the sandbox, if we are configured to use the sandbox) =cut sub apiUrl { my $self = shift; return $self->get( $self->get('testMode') ? 'apiSandbox' : 'api' ); } #------------------------------------------------------------------- =head2 definition Standard definition method. =cut sub definition { my ( $class, $session, $definition ) = @_; my $i18n = WebGUI::International->new( $session, $I18N ); tie my %fields, 'Tie::IxHash'; my @fieldNames = qw( paypal sandbox api apiSandbox user password currency testMode signature ); foreach my $f (@fieldNames) { $fields{$f} = { fieldType => 'text', label => $i18n->get($f), hoverHelp => $i18n->get("$f help"), }; } $fields{currency}{defaultValue} = 'USD'; $fields{testMode}{fieldType} = 'YesNo'; $fields{sandbox}{defaultValue} = 'https://www.sandbox.paypal.com/webscr'; $fields{apiSandbox}{defaultValue} = 'https://api-3t.sandbox.payPal.com/nvp'; $fields{paypal}{defaultValue} = 'https://www.paypal.com/webscr'; $fields{api}{defaultValue} = 'https://api-3t.payPal.com/nvp'; push @{$definition}, { name => $i18n->get('name'), properties => \%fields, }; return $class->SUPER::definition( $session, $definition ); } ## end sub definition #------------------------------------------------------------------- =head2 getButton Overridden, submits to www_sendToPaypal with the proper parameters. =cut sub getButton { my $self = shift; my $session = $self->session; my $payForm = WebGUI::Form::formHeader($session) . $self->getDoFormTags('sendToPayPal') . WebGUI::Form::submit( $session, { value => $self->get('name') } ) . WebGUI::Form::formFooter($session); return $payForm; } #------------------------------------------------------------------- =head2 payPalForm ( %fields ) Returns a hashref representing a form (suitable for an LWP post) for talking to the PayPal API. Fields can be either name value pairs or a hashref. If it is a hashref, it will be modified in place. =cut sub payPalForm { my $self = shift; my $args = ref $_[0] eq 'HASH' ? shift : {@_}; $args->{VERSION} = '58.0'; $args->{USER} = $self->get('user'); $args->{PWD} = $self->get('password'); $args->{SIGNATURE} = $self->get('signature'); return $args; } #------------------------------------------------------------------- =head2 payPalUrl Returns the URL for the PayPal site (or the sandbox, if we are configured to use the sandbox) =cut sub payPalUrl { my $self = shift; return $self->get( $self->get('testMode') ? 'sandbox' : 'paypal' ); } #------------------------------------------------------------------- =head2 processPayment ( transaction ) Implements the interface defined in WebGUI::Shop::PayDriver. Notably, on error 'message' will be an HTML table representing the parameters that the PayPal API spit back. =cut sub processPayment { my ( $self, $transaction ) = @_; my ( $isSuccess, $gatewayCode, $status, $message ); my $form = $self->payPalForm( METHOD => 'DoExpressCheckoutPayment', PAYERID => $self->session->form->process('PayerId'), TOKEN => $self->session->form->process('token'), AMT => $self->getCart->calculateTotal, CURRENCYCODE => $self->get('currency'), PAYMENTACTION => 'SALE', ); my $response = LWP::UserAgent->new->post( $self->apiUrl, $form ); my $params = $self->responseHash($response); if ($params) { if ( $params->{ACK} !~ /^Success/ ) { my $status = $params->{ACK}; my $message = ''; foreach my $k ( keys %$params ) { $message .= ""; } $message .= '
FieldValue
$k$params->{$k}
'; return ( 0, undef, $status, $message ); } my $status = $params->{PAYMENTSTATUS}; my $i18n = WebGUI::International->new( $self->session, $I18N ); my $message = sprintf $i18n->get('payment status'), $status; return ( 1, $params->{TRANSACTIONID}, $status, $message ); } return ( 0, undef, $response->status_code, $response->status_line ); } ## end sub processPayment #------------------------------------------------------------------- =head2 responseHash (response) Chops up the body of a paypal response into a hashref (or undef if the request failed) =cut sub responseHash { my ( $self, $response ) = @_; return undef unless $response->is_success; local $_ = uri_unescape( $response->content ); return { map { split /=/ } split /[&;]/ }; } #------------------------------------------------------------------- =head2 www_payPalCallback Handler that PayPal redirects to once payment has been confirmed on their end =cut sub www_payPalCallback { my $self = shift; my $transaction = $self->processTransaction; return $transaction->get('isSuccessful') ? $transaction->thankYou : $self->displayPaymentError($transaction); } #------------------------------------------------------------------- =head2 www_sendToPayPal Sets up payPal transaction and redirects the user off to payPal land =cut sub www_sendToPayPal { my $self = shift; my $session = $self->session; my $url = $session->url; my $base = $url->getSiteURL . $url->page; my $returnUrl = URI->new($base); $returnUrl->query_form( { shop => 'pay', method => 'do', do => 'payPalCallback', paymentGatewayId => $self->getId, } ); my $cancelUrl = URI->new($base); $cancelUrl->query_form( { shop => 'cart' } ); my $form = $self->payPalForm( METHOD => 'SetExpressCheckout', AMT => $self->getCart->calculateTotal, CURRENCYCODE => $self->get('currency'), RETURNURL => $returnUrl->as_string, CANCELURL => $cancelUrl->as_string, PAYMENTACTION => 'SALE', ); my $testMode = $self->get('testMode'); my $response = LWP::UserAgent->new->post( $self->apiUrl, $form ); my $params = $self->responseHash($response); my $i18n = WebGUI::International->new( $self->session, $I18N ); my $error; if ($params) { unless ( $params->{ACK} =~ /^Success/ ) { my $log = sprintf "Paypal error: Request/response below: %s\n%s\n", Dumper($form), Dumper($params); $session->log->error($log); $error = $i18n->get('internal paypal error'); } } else { $error = $response->status_line; } if ($error) { my $message = sprintf $i18n->get('api error'), $error; return $session->style->userStyle($message); } my $dest = URI->new( $self->payPalUrl ); $dest->query_form( { cmd => '_express-checkout', token => $params->{TOKEN}, } ); return $session->http->setRedirect($dest); } ## end sub www_sendToPayPal =head1 LIMITATIONS =over 4 =item Doesn't handle recurring payments, although Paypal can do that. =item There is no itemization of the cart for Paypal's records, just one total (could do taxes, shipping, each item as separate things). =item Paypal's shipping information is ignored; this could be changed to accept new shipping info from PayPal, but that's somewhat fragile. We're currently just pretending PayPal is a payment gateway. =back =cut 1;