574 lines
20 KiB
Perl
574 lines
20 KiB
Perl
# ====================================================================
|
|
# Gossamer Threads Module Library - http://gossamer-threads.com/
|
|
#
|
|
# GT::Payment::Remote::PayPal
|
|
# Author: Jason Rhinelander
|
|
# CVS Info : 087,071,086,086,085
|
|
# $Id: PayPal.pm,v 1.8 2006/04/08 03:42:05 brewt Exp $
|
|
#
|
|
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
|
|
# ====================================================================
|
|
#
|
|
# Description:
|
|
# PayPal IPN payment processing.
|
|
# IPN information: (PayPal login required)
|
|
# https://www.paypal.com/cgi-bin/webscr?cmd=p/acc/ipn-info
|
|
#
|
|
# Net::SSLeay is required. Windows (ActivePerl) Net::SSLeay packages are
|
|
# available through Gossamer Threads.
|
|
#
|
|
|
|
package GT::Payment::Remote::PayPal;
|
|
use strict;
|
|
use Carp;
|
|
use GT::WWW;
|
|
use GT::WWW::https;
|
|
|
|
# Usage:
|
|
# process(
|
|
# param => $GT_CGI_OBJ,
|
|
# on_valid => \&CODEREF, # Called when everything checks out
|
|
# on_pending => \&CODEREF, # Optional - another IPN request will come in when no longer pending
|
|
# on_failed => \&CODEREF, # "The payment has failed. This will only happen if the payment was made from your customer's bank account"
|
|
# on_denied => \&CODEREF, # "You, the merchant, denied the payment. This will only happen if the payment was previously pending due to one of the "pending reasons" below"
|
|
# on_invalid => \&CODEREF, # This request did NOT come from PayPal
|
|
# on_recurring => \&CODEREF, # A recurring payment
|
|
# on_recurring_signup => \&CODEREF, # A recurring payment signup
|
|
# on_recurring_cancel => \&CODEREF, # A recurring payment cancellation
|
|
# on_recurring_failed => \&CODEREF, # A subscription payment failure
|
|
# on_recurring_eot => \&CODEREF, # A subscription "end of term" notification
|
|
# on_recurring_modify => \&CODEREF, # A subscription modification notification
|
|
# duplicate => \&CODEREF, # Check to make sure this isn't a duplicate (1 = okay, 0/undef = duplicate)
|
|
# email => \&CODEREF, # Called with the specified e-mail - check it against the primary e-mail account, return 1 for valid, 0/undef for error
|
|
# on_error => \&CODEREF # Optional
|
|
# )
|
|
# Only on_error is optional. on_valid will be called if the request is valid,
|
|
# on_invalid is invalid, and on_error if an error occurs (such as an HTTP error,
|
|
# connection problem, etc.)
|
|
sub process {
|
|
shift if $_[0] and UNIVERSAL::isa($_[0], __PACKAGE__);
|
|
|
|
my %opts = @_;
|
|
$opts{param} and UNIVERSAL::isa($opts{param}, 'GT::CGI') or croak 'Usage: ->process(param => $gtcgi, ...)';
|
|
my $in = $opts{param};
|
|
for (qw/on_valid on_failed on_denied duplicate email/) {
|
|
ref $opts{$_} eq 'CODE' or croak "Usage: ->process($_ => \&CODEREF, ...)";
|
|
}
|
|
|
|
for (qw/on_error on_pending on_invalid on_recurring on_recurring_signup on_recurring_cancel
|
|
on_recurring_failed on_recurring_eot on_recurring_modify/) {
|
|
!$opts{$_} or ref $opts{$_} eq 'CODE' or croak "Usage: ->process($_ => \\&CODEREF, ...) (optional)";
|
|
}
|
|
|
|
my $sandbox = $opts{sandbox} ? 'sandbox.' : '';
|
|
my $wwws = GT::WWW->new("https://www.${sandbox}paypal.com/cgi-bin/webscr");
|
|
my @param;
|
|
|
|
for my $p ($in->param) {
|
|
for my $v ($in->param($p)) {
|
|
push @param, $p, $v;
|
|
}
|
|
}
|
|
|
|
# PayPal says:
|
|
# You will also need to append a variable named "cmd" with the value
|
|
# "_notify-validate" (e.g. cmd=_notify-validate) to the POST string.
|
|
$wwws->parameters(@param, cmd => '_notify-validate');
|
|
|
|
my $result = $wwws->post;
|
|
my $status;
|
|
|
|
# PayPal says:
|
|
# PayPal will respond to the post with a single word, "VERIFIED" or
|
|
# "INVALID", in the body of the response. When you receive a VERIFIED
|
|
# response, you need to:
|
|
#
|
|
# * Check that the "payment_status" is "completed"
|
|
# * If the "payment_status" is "completed", check the "txn_id" against
|
|
# the previous PayPal transaction you have processed to ensure it is
|
|
# not a duplicate.
|
|
# * After you have checked the "payment_status" and "txn_id", make sure
|
|
# the "receiver_email" is an email address registered in your PayPal
|
|
# account
|
|
# * Once you have completed the above checks, you may update your
|
|
# database based on the information provided.
|
|
if ($result) {
|
|
my $status = "$result";
|
|
unless ($status eq 'VERIFIED') {
|
|
$opts{on_invalid}->($status) if $opts{on_invalid};
|
|
return;
|
|
}
|
|
|
|
# For certain txn_types payment_status and txn_id aren't available
|
|
my $txn_type = $in->param('txn_type');
|
|
if ($txn_type =~ /^subscr_(?:signup|cancel|failed|eot|modify)$/) {
|
|
if ($txn_type eq 'subscr_signup') {
|
|
$opts{on_recurring_signup}->() if $opts{on_recurring_signup};
|
|
}
|
|
elsif ($txn_type eq 'subscr_cancel') {
|
|
$opts{on_recurring_cancel}->() if $opts{on_recurring_cancel};
|
|
}
|
|
elsif ($txn_type eq 'subscr_failed') {
|
|
$opts{on_recurring_failed}->() if $opts{on_recurring_failed};
|
|
}
|
|
elsif ($txn_type eq 'substr_eot') {
|
|
$opts{on_recurring_eot}->() if $opts{on_recurring_eot};
|
|
}
|
|
elsif ($txn_type eq 'substr_modify') {
|
|
$opts{on_recurring_modify}->() if $opts{on_recurring_modify};
|
|
}
|
|
return;
|
|
}
|
|
|
|
# * Check that the "payment_status" is "completed" [sic; should be "Completed"]
|
|
unless ((my $status = $in->param('payment_status')) eq 'Completed') {
|
|
if ($status eq 'Pending') {
|
|
$opts{on_pending}->() if $opts{on_pending};
|
|
}
|
|
elsif ($status eq 'Failed') {
|
|
$opts{on_failed}->();
|
|
}
|
|
elsif ($status eq 'Denied') {
|
|
$opts{on_denied}->();
|
|
}
|
|
elsif ($status eq 'Refunded') {
|
|
$opts{on_refund}->() if $opts{on_refund};
|
|
}
|
|
elsif ($opts{on_error}) {
|
|
$opts{on_error}->("PayPal sent invalid/unknown payment_status value: '$status'");
|
|
}
|
|
return;
|
|
}
|
|
|
|
my $txn_id = $in->param('txn_id');
|
|
return unless $txn_id;
|
|
|
|
# * If the "payment_status" is "completed", check the "txn_id" against
|
|
# the previous PayPal transaction you have processed to ensure it is
|
|
# not a duplicate.
|
|
$opts{duplicate}->($txn_id) or return;
|
|
|
|
# * After you have checked the "payment_status" and "txn_id", make sure
|
|
# the "receiver_email" is an email address registered in your PayPal
|
|
# account
|
|
$opts{email}->($in->param('receiver_email')) or return; # Ignore if the e-mail addresses don't match
|
|
|
|
if ($txn_type eq 'subscr_payment') {
|
|
$opts{on_recurring}->() if $opts{on_recurring};
|
|
}
|
|
else {
|
|
$opts{on_valid}->();
|
|
}
|
|
}
|
|
elsif ($opts{on_error}) {
|
|
if (defined $result) {
|
|
my $http_status = $result->status;
|
|
$opts{on_error}->("Server returned a non-okay status: " . int($http_status) . " $http_status");
|
|
}
|
|
else {
|
|
$opts{on_error}->("Connection error: " . $wwws->error);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
GT::Payment::Remote::PayPal - PayPal payment handling
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use GT::Payment::Remote::PayPal;
|
|
use GT::CGI;
|
|
|
|
my $in = new GT::CGI;
|
|
|
|
GT::Payment::Remote::PayPal->process(
|
|
param => $in,
|
|
on_valid => \&valid,
|
|
on_pending => \&pending,
|
|
on_failed => \&failed,
|
|
on_denied => \&denied,
|
|
on_invalid => \&invalid,
|
|
on_recurring => \&recurring,
|
|
on_recurring_signup => \&r_signup,
|
|
on_recurring_cancel => \&r_cancel,
|
|
on_recurring_failed => \&r_failed,
|
|
on_recurring_eot => \&r_eot,
|
|
on_recurring_modify => \&r_modify,
|
|
duplicate => \&duplicate,
|
|
email => \&email,
|
|
on_error => \&error
|
|
);
|
|
|
|
sub valid {
|
|
# Update database - the payment has been made successfully.
|
|
}
|
|
sub pending {
|
|
# Optional; store a "payment pending" status if you wish. This is optional
|
|
# because another postback will be made with a completed, failed, or denied
|
|
# status.
|
|
}
|
|
failed {
|
|
# According to PayPal IPN documentation: "The payment has failed. This
|
|
# will only happen if the payment was made from your customer's bank
|
|
# account."
|
|
# Store a "payment failed" status for the order
|
|
}
|
|
sub denied {
|
|
# According to PayPal IPN documentation: "You, the merchant, denied the
|
|
# payment. This will only happen if the payment was previously pending due
|
|
# to one of the "pending reasons" [in pending_reason]"
|
|
}
|
|
sub invalid {
|
|
# This means the request did NOT come from PayPal. You should log the
|
|
# request for follow up.
|
|
}
|
|
sub recurring {
|
|
# This means a recurring payment has been made successfully. Update
|
|
# database.
|
|
}
|
|
sub r_signup {
|
|
# This means a recurring signup has been made (NOT a payment, just a
|
|
# signup).
|
|
}
|
|
sub r_cancel {
|
|
# The user has cancelled their recurring payment
|
|
}
|
|
sub r_failed {
|
|
# A recurring payment has failed (probably declined).
|
|
}
|
|
sub r_eot {
|
|
# A recurring payment has come to its natural conclusion. This only
|
|
# applies to payments with a set number of payments.
|
|
}
|
|
sub r_modify {
|
|
# Something has been modified regarding the recurring payment
|
|
}
|
|
sub duplicate {
|
|
# Check to see if the payment has already been made. If it _has_ been
|
|
# made, you should return undef, otherwise return 1 to indicate that this
|
|
# is not a duplicate postback. The "txn_id" value is passed in, but is
|
|
# also available through $in->param('txn_id').
|
|
}
|
|
sub email {
|
|
# This will be called with an e-mail address. You should check to make
|
|
# sure that the e-mail address entered is the same as the one on the PayPal
|
|
# account. Return true (1) if everything checks out, undef otherwise.
|
|
}
|
|
sub error {
|
|
# An error message is passed in here. This is called when a error such as
|
|
# a connection problem or HTTP problem occurs.
|
|
}
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This module is designed to handle PayPal payment processing using PayPal's IPN
|
|
system. It does very little other than generating and sending a proper
|
|
response to the PayPal server, and calling the provided code reference(s).
|
|
|
|
It is strongly recommended that you familiarize yourself with the PayPal
|
|
"Single Item Purchases Manual" and "IPN Manual" listed in the L</"SEE ALSO">
|
|
section of this document.
|
|
|
|
=head1 REQUIREMENTS
|
|
|
|
GT::WWW with the https protocol, which in turn requires Net::SSLeay. PPM's are
|
|
available from Gossamer Threads for the latest Windows releases of ActiveState
|
|
Perl 5.6.1 and 5.8.0.
|
|
|
|
=head1 process
|
|
|
|
process() is the only function/method provided by this module. It can be
|
|
called as either a function or class method, and takes a hash (not hash
|
|
reference) of arguments as described below. This module requires GT::WWW's
|
|
https interface, which in turn requires Net::SSLeay.
|
|
|
|
process() should be called for PayPal initiated requests. This can be set up
|
|
in your main CGI by looking for PayPal-specific CGI parameters ('txn_type' is a
|
|
good one to look for) or by making a seperate .cgi file exclusively for
|
|
handling IPN postbacks.
|
|
|
|
Additionally, it is strongly advised that database connection, authenticate,
|
|
etc. be performed before calling process() to ensure that the payment is
|
|
recorded successfully. If your CGI script has an error, PayPal will retry the
|
|
postback again
|
|
|
|
Except where indicated, all arguments are required.
|
|
|
|
=head2 param
|
|
|
|
param takes a GT::CGI object from which PayPal IPN variables are read.
|
|
|
|
=head2 on_valid
|
|
|
|
on_valid takes a code reference as value. The code reference will be called
|
|
when a successful payment has been made. Inside this code reference you are
|
|
responsible for setting a "paid" status for the order in question.
|
|
|
|
See the PayPal IPN documentation listed below for information on how to
|
|
identify an order.
|
|
|
|
=head2 on_pending
|
|
|
|
on_pending is called when PayPal sends information on a "Pending" payment.
|
|
This parameter is optional, due to the fact that a "Pending" status means that
|
|
another notification (either "Completed", "Failed", or "Denied") will be made.
|
|
|
|
It is, however, recommended that when a Pending payment is encountered, a note
|
|
be stored in your application that manual intervention is probably required.
|
|
|
|
According to PayPal documentation, there are a few cases where this will
|
|
happen, which can be obtained from the "pending_reason" CGI input variable.
|
|
The possible values and what each means follows (this comes straight from the
|
|
PayPal documentation).
|
|
|
|
=over 4
|
|
|
|
=item "echeck"
|
|
|
|
The payment is pending because it was made by an eCheck, which has not yet
|
|
cleared.
|
|
|
|
=item "multi_currency"
|
|
|
|
You do not have a balance in the currency sent, and you do not have your
|
|
Payment Receiving Preferences set to automatically convert and accept this
|
|
payment. You must manually accept or deny this payment.
|
|
|
|
=item "intl"
|
|
|
|
The payment is pending because you, the merchant, hold an international account
|
|
and do not have a withdrawal mechanism. You must manually accept or deny this
|
|
payment from your Account Overview.
|
|
|
|
=item "verify"
|
|
|
|
The payment is pending because you, the merchant, are not yet verified. You
|
|
must verify your account before you can accept this payment.
|
|
|
|
=item "address"
|
|
|
|
The payment is pending because your customer did not include a confirmed
|
|
shipping address and you, the merchant, have your Payment Receiving Preferences
|
|
set such that you want to manually accept or deny each of these payments. To
|
|
change your preference, go to the "Preferences" section of your "Profile."
|
|
|
|
=item "upgrade"
|
|
|
|
The payment is pending because it was made via credit card and you, the
|
|
merchant, must upgrade your account to Business or Premier status in order to
|
|
receive the funds.
|
|
|
|
=item "unilateral"
|
|
|
|
The payment is pending because it was made to an email address that is not yet
|
|
registered or confirmed.
|
|
|
|
=item "other"
|
|
|
|
The payment is pending for an "other" reason. For more information, contact
|
|
customer service.
|
|
|
|
=back
|
|
|
|
=head2 on_failed
|
|
|
|
Takes a code reference to call in the event of a failed payment notification.
|
|
A failed payment "will only happen if the payment was made from your customer's
|
|
bank account."
|
|
|
|
You should record a failed payment in your application.
|
|
|
|
=head2 on_denied
|
|
|
|
This code reference is called when a "Denied" payment notification is received.
|
|
"This will only happen if the payment was previously pending due to one of the
|
|
'pending reasons'" above.
|
|
|
|
You should record a failed or denied payment in your application.
|
|
|
|
=head2 on_invalid
|
|
|
|
This code reference will be called when an invalid request is made. This
|
|
usually means that the request B<did not> come from PayPal. According to
|
|
PayPal, "if you receive an 'INVALID' notification, it should be treated as
|
|
suspicious and investigated." Thus it is strongly recommended that a record of
|
|
the invalid request be made.
|
|
|
|
=head2 duplicate
|
|
|
|
This code reference is required to prevent duplicate payments. It is called
|
|
for potentially successful requests to ensure that it is not a duplicate
|
|
postback. It is passed the "txn_id" CGI parameter, which is the
|
|
PayPal-generated transaction ID. You should check this parameter against your
|
|
order database. If you have already recorded this payment as successfully
|
|
made, should should return C<undef> from this function, to indicate that the
|
|
duplicate check failed. If the transaction ID is okay (i.e. is not a
|
|
duplicate) return 1 to continue.
|
|
|
|
=head2 recurring
|
|
|
|
A successful recurring payment has been made. You should set a "paid" status
|
|
for the item in question.
|
|
|
|
=head2 recurring_signup
|
|
|
|
=head2 recurring_cancel
|
|
|
|
=head2 recurring_failed
|
|
|
|
=head2 recurring_eot
|
|
|
|
=head2 recurring_modify
|
|
|
|
These are called when various things have happened to the subscription. In
|
|
particular, signup refers to a new subscription, cancel refers to a cancelled
|
|
subscription, failed refers to a failed payment, eot refers to a subscription
|
|
that ended naturally (i.e. an end was set when the subscription was initially
|
|
made), and modify is called when a payment has been modified.
|
|
|
|
=head2 email
|
|
|
|
This code reference, like duplicate, is called to ensure that the payment was
|
|
sent to the correct account. An e-mail address is passed in which must be the
|
|
same as the primary account's e-mail address. If it is the same, return C<1>.
|
|
If it is I<not> the same, you should return C<undef> and store a note asking
|
|
the user to check that the PayPal e-mail address they have provided is the
|
|
correct, primary, PayPal e-mail address.
|
|
|
|
=head2 on_error
|
|
|
|
This code reference is optional, but recommended. It is called when a
|
|
non-PayPal generated error occurs - such as a failure to connect to PayPal. It
|
|
is recommended that you provide this code reference and log any errors that
|
|
occur. The error message is passed in.
|
|
|
|
=head1 INSTRUCTIONS
|
|
|
|
To implement PayPal payment processing, there are a number of steps required in
|
|
addition to this module. Basically, this module handles only the postback
|
|
stage of the PayPal IPN process.
|
|
|
|
Full PayPal single item, subscription, and IPN documentation is available at
|
|
the URL's listed in the L<SEE ALSO|/"SEE ALSO"> section.
|
|
|
|
=head2 Directing customers to PayPal
|
|
|
|
This is done by creating a web form containing the following variables. Your
|
|
form, first of all, must post to C<https://www.paypal.com/cgi-bin/webscr>.
|
|
|
|
Your form should contains various PayPal parameters, as outlined in the PayPal
|
|
manuals linked to in the L<SEE ALSO|/"SEE ALSO"> section.
|
|
|
|
Of particular note is the "notify_url" option, which should be used to specify
|
|
a postback URL for PayPal IPN postbacks.
|
|
The below is simply a list of the required fields, and only those fields that
|
|
are absolutely required are described. For descriptions of each field, check
|
|
the PayPal Single Item Purchases Manual.
|
|
|
|
=over 4
|
|
|
|
=item cmd
|
|
|
|
Must be set to "_xclick".
|
|
|
|
=item business
|
|
|
|
Your PayPal ID (e-mail address). Must be confirmed and linked to your Verified
|
|
Business or Premier account.
|
|
|
|
=item item_name
|
|
|
|
=item item_number
|
|
|
|
=item image_url
|
|
|
|
=item no_shipping
|
|
|
|
|
|
=item return
|
|
|
|
Although optional, this is highly recommend - takes a URL to bring the buyer
|
|
back to after purchasing. If not specified, they'll remain at PayPal.
|
|
|
|
=item rm
|
|
|
|
Return method for the L<return|/return> option. If "1", a GET request without
|
|
the transaction variables will be made, if "2" a POST request WITH the transaction
|
|
variables will be made.
|
|
|
|
=item cancel_return
|
|
|
|
=item no_note
|
|
|
|
=item cn
|
|
|
|
=item cs
|
|
|
|
=item on0
|
|
|
|
=item on1
|
|
|
|
=item os0
|
|
|
|
=item os1
|
|
|
|
=item quantity
|
|
|
|
The quantity of items being purchased. If omitted, defaults to 1 and will not
|
|
be shown in the payment flow.
|
|
|
|
=item undefined_quantity
|
|
|
|
"If set to "1", the user will be able to edit the quantity. This means your
|
|
customer will see a field next to quantity which they must complete. This is
|
|
optional; if omitted or set to "0", the quantity will not be editable by the
|
|
user. Instead, it will default to 1"
|
|
|
|
=item shipping
|
|
|
|
=back
|
|
|
|
=head2 IPN
|
|
|
|
Before PayPal payment notification can occur, you must instruct the user to
|
|
enable Instant Payment Notification (IPN) on their PayPal account. The
|
|
postback URL should be provided and handled by you either by detecting a PayPal
|
|
request in your main .cgi script (recommended), or through the use of an
|
|
additional .cgi script exclusively for PayPal IPN.
|
|
|
|
If adding to your existing script, it is recommended to look for the 'txn_type'
|
|
CGI parameter, which will be set for PayPal IPN postbacks.
|
|
|
|
Once IPN has been set up, you have to set up your application to direct users
|
|
to PayPal in order to initiate a PayPal payment.
|
|
|
|
=head1 SEE ALSO
|
|
|
|
L<https://www.paypal.com/html/single_item.pdf> - Single Item Purchases Manual
|
|
|
|
L<https://www.paypal.com/html/subscriptions.pdf> - Subscriptions and Recurring
|
|
Payments Manual
|
|
|
|
L<https://www.paypal.com/html/ipn.pdf> - IPN Manual
|
|
|
|
=head1 MAINTAINER
|
|
|
|
Jason Rhinelander
|
|
|
|
=head1 COPYRIGHT
|
|
|
|
Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
|
|
http://www.gossamer-threads.com/
|
|
|
|
=head1 VERSION
|
|
|
|
Revision: $Id: PayPal.pm,v 1.8 2006/04/08 03:42:05 brewt Exp $
|
|
|
|
=cut
|